import: pypi: Parse wheel METADATA instead of metadata.json.
With newer Wheel releases, there is no more metadata.json file; the METADATA file should be used instead (see: https://github.com/pypa/wheel/issues/195). This change updates our PyPI importer so that it uses the latter. * guix/import/pypi.scm (define-module): Remove unnecessary modules and export the PARSE-WHEEL-METADATA procedure. (parse-wheel-metadata): Add procedure. (guess-requirements): Use it. * tests/pypi.scm (test-metadata): Test it.
This commit is contained in:
parent
c799ad7276
commit
f0190a5dcd
|
@ -21,9 +21,7 @@
|
||||||
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
|
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
(define-module (guix import pypi)
|
(define-module (guix import pypi)
|
||||||
#:use-module (ice-9 binary-ports)
|
|
||||||
#:use-module (ice-9 match)
|
#:use-module (ice-9 match)
|
||||||
#:use-module (ice-9 pretty-print)
|
|
||||||
#:use-module (ice-9 regex)
|
#:use-module (ice-9 regex)
|
||||||
#:use-module (ice-9 receive)
|
#:use-module (ice-9 receive)
|
||||||
#:use-module ((ice-9 rdelim) #:select (read-line))
|
#:use-module ((ice-9 rdelim) #:select (read-line))
|
||||||
|
@ -31,9 +29,6 @@
|
||||||
#:use-module (srfi srfi-26)
|
#:use-module (srfi srfi-26)
|
||||||
#:use-module (srfi srfi-34)
|
#:use-module (srfi srfi-34)
|
||||||
#:use-module (srfi srfi-35)
|
#:use-module (srfi srfi-35)
|
||||||
#:use-module (rnrs bytevectors)
|
|
||||||
#:use-module (json)
|
|
||||||
#:use-module (web uri)
|
|
||||||
#:use-module (guix ui)
|
#:use-module (guix ui)
|
||||||
#:use-module (guix utils)
|
#:use-module (guix utils)
|
||||||
#:use-module ((guix build utils)
|
#:use-module ((guix build utils)
|
||||||
|
@ -49,6 +44,7 @@
|
||||||
#:use-module ((guix licenses) #:prefix license:)
|
#:use-module ((guix licenses) #:prefix license:)
|
||||||
#:use-module (guix build-system python)
|
#:use-module (guix build-system python)
|
||||||
#:export (parse-requires.txt
|
#:export (parse-requires.txt
|
||||||
|
parse-wheel-metadata
|
||||||
specification->requirement-name
|
specification->requirement-name
|
||||||
guix-package->pypi-name
|
guix-package->pypi-name
|
||||||
pypi-recursive-import
|
pypi-recursive-import
|
||||||
|
@ -177,18 +173,49 @@ requirement names."
|
||||||
;; Stop when a section is encountered, as sections contain optional
|
;; Stop when a section is encountered, as sections contain optional
|
||||||
;; (extra) requirements. Non-optional requirements must appear
|
;; (extra) requirements. Non-optional requirements must appear
|
||||||
;; before any section is defined.
|
;; before any section is defined.
|
||||||
(if (or (eof-object? line) (section-header? line))
|
(cond
|
||||||
;; Duplicates can occur, since the same requirement can be
|
((or (eof-object? line) (section-header? line))
|
||||||
;; listed multiple times with different conditional markers, e.g.
|
;; Duplicates can occur, since the same requirement can be
|
||||||
;; pytest >= 3 ; python_version >= "3.3"
|
;; listed multiple times with different conditional markers, e.g.
|
||||||
;; pytest < 3 ; python_version < "3.3"
|
;; pytest >= 3 ; python_version >= "3.3"
|
||||||
(reverse (delete-duplicates result))
|
;; pytest < 3 ; python_version < "3.3"
|
||||||
(cond
|
(reverse (delete-duplicates result)))
|
||||||
((or (string-null? line) (comment? line))
|
((or (string-null? line) (comment? line))
|
||||||
(loop result))
|
(loop result))
|
||||||
(else
|
(else
|
||||||
(loop (cons (specification->requirement-name line)
|
(loop (cons (specification->requirement-name line)
|
||||||
result))))))))))
|
result)))))))))
|
||||||
|
|
||||||
|
(define (parse-wheel-metadata metadata)
|
||||||
|
"Given METADATA, a Wheel metadata file, return a list of requirement names."
|
||||||
|
;; METADATA is a RFC-2822-like, header based file.
|
||||||
|
|
||||||
|
(define (requires-dist-header? line)
|
||||||
|
;; Return #t if the given LINE is a Requires-Dist header.
|
||||||
|
(string-match "^Requires-Dist: " line))
|
||||||
|
|
||||||
|
(define (requires-dist-value line)
|
||||||
|
(string-drop line (string-length "Requires-Dist: ")))
|
||||||
|
|
||||||
|
(define (extra? line)
|
||||||
|
;; Return #t if the given LINE is an "extra" requirement.
|
||||||
|
(string-match "extra == '(.*)'" line))
|
||||||
|
|
||||||
|
(call-with-input-file metadata
|
||||||
|
(lambda (port)
|
||||||
|
(let loop ((requirements '()))
|
||||||
|
(let ((line (read-line port)))
|
||||||
|
;; Stop at the first 'Provides-Extra' section: the non-optional
|
||||||
|
;; requirements appear before the optional ones.
|
||||||
|
(cond
|
||||||
|
((eof-object? line)
|
||||||
|
(reverse (delete-duplicates requirements)))
|
||||||
|
((and (requires-dist-header? line) (not (extra? line)))
|
||||||
|
(loop (cons (specification->requirement-name
|
||||||
|
(requires-dist-value line))
|
||||||
|
requirements)))
|
||||||
|
(else
|
||||||
|
(loop requirements))))))))
|
||||||
|
|
||||||
(define (guess-requirements source-url wheel-url archive)
|
(define (guess-requirements source-url wheel-url archive)
|
||||||
"Given SOURCE-URL, WHEEL-URL and a ARCHIVE of the package, return a list
|
"Given SOURCE-URL, WHEEL-URL and a ARCHIVE of the package, return a list
|
||||||
|
@ -197,25 +224,18 @@ be extracted in a temporary directory."
|
||||||
|
|
||||||
(define (read-wheel-metadata wheel-archive)
|
(define (read-wheel-metadata wheel-archive)
|
||||||
;; Given WHEEL-ARCHIVE, a ZIP Python wheel archive, return the package's
|
;; Given WHEEL-ARCHIVE, a ZIP Python wheel archive, return the package's
|
||||||
;; requirements.
|
;; requirements, or #f if the metadata file contained therein couldn't be
|
||||||
|
;; extracted.
|
||||||
(let* ((dirname (wheel-url->extracted-directory wheel-url))
|
(let* ((dirname (wheel-url->extracted-directory wheel-url))
|
||||||
(json-file (string-append dirname "/metadata.json")))
|
(metadata (string-append dirname "/METADATA")))
|
||||||
(and (zero? (system* "unzip" "-q" wheel-archive json-file))
|
(call-with-temporary-directory
|
||||||
(dynamic-wind
|
(lambda (dir)
|
||||||
(const #t)
|
(if (zero? (system* "unzip" "-q" wheel-archive "-d" dir metadata))
|
||||||
(lambda ()
|
(parse-wheel-metadata (string-append dir "/" metadata))
|
||||||
(call-with-input-file json-file
|
(begin
|
||||||
(lambda (port)
|
(warning
|
||||||
(let* ((metadata (json->scm port))
|
(G_ "Failed to extract file: ~a from wheel.~%") metadata)
|
||||||
(run_requires (hash-ref metadata "run_requires"))
|
#f))))))
|
||||||
(requirements (if run_requires
|
|
||||||
(hash-ref (list-ref run_requires 0)
|
|
||||||
"requires")
|
|
||||||
'())))
|
|
||||||
(map specification->requirement-name requirements)))))
|
|
||||||
(lambda ()
|
|
||||||
(delete-file json-file)
|
|
||||||
(rmdir dirname))))))
|
|
||||||
|
|
||||||
(define (guess-requirements-from-wheel)
|
(define (guess-requirements-from-wheel)
|
||||||
;; Return the package's requirements using the wheel, or #f if an error
|
;; Return the package's requirements using the wheel, or #f if an error
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#:use-module (guix base32)
|
#:use-module (guix base32)
|
||||||
#:use-module (guix memoization)
|
#:use-module (guix memoization)
|
||||||
#:use-module (gcrypt hash)
|
#:use-module (gcrypt hash)
|
||||||
|
#:use-module (guix memoization)
|
||||||
#:use-module (guix tests)
|
#:use-module (guix tests)
|
||||||
#:use-module (guix build-system python)
|
#:use-module (guix build-system python)
|
||||||
#:use-module ((guix build utils) #:select (delete-file-recursively which mkdir-p))
|
#:use-module ((guix build utils) #:select (delete-file-recursively which mkdir-p))
|
||||||
|
@ -79,17 +80,33 @@ bar != 2
|
||||||
pytest (>=2.5.0)
|
pytest (>=2.5.0)
|
||||||
")
|
")
|
||||||
|
|
||||||
(define test-metadata
|
(define test-metadata "\
|
||||||
"{
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
\"run_requires\": [
|
Requires-Dist: baz ~= 3
|
||||||
{
|
Requires-Dist: bar != 2
|
||||||
\"requires\": [
|
Provides-Extra: test
|
||||||
\"bar\",
|
pytest (>=2.5.0)
|
||||||
\"baz (>13.37)\"
|
")
|
||||||
]
|
|
||||||
}
|
(define test-metadata-with-extras "
|
||||||
]
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
}")
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||||
|
Requires-Dist: wrapt (<2,>=1)
|
||||||
|
Requires-Dist: bar
|
||||||
|
|
||||||
|
Provides-Extra: dev
|
||||||
|
Requires-Dist: tox ; extra == 'dev'
|
||||||
|
Requires-Dist: bumpversion (<1) ; extra == 'dev'
|
||||||
|
")
|
||||||
|
|
||||||
|
;;; Provides-Extra can appear before Requires-Dist.
|
||||||
|
(define test-metadata-with-extras-jedi "\
|
||||||
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
||||||
|
Provides-Extra: testing
|
||||||
|
Requires-Dist: parso (>=0.3.0)
|
||||||
|
Provides-Extra: testing
|
||||||
|
Requires-Dist: pytest (>=3.1.0); extra == 'testing'
|
||||||
|
")
|
||||||
|
|
||||||
(test-begin "pypi")
|
(test-begin "pypi")
|
||||||
|
|
||||||
|
@ -128,6 +145,18 @@ pytest (>=2.5.0)
|
||||||
call-with-input-string)
|
call-with-input-string)
|
||||||
(parse-requires.txt test-requires-with-sections)))
|
(parse-requires.txt test-requires-with-sections)))
|
||||||
|
|
||||||
|
(test-equal "parse-wheel-metadata, with extras"
|
||||||
|
'("wrapt" "bar")
|
||||||
|
(mock ((ice-9 ports) call-with-input-file
|
||||||
|
call-with-input-string)
|
||||||
|
(parse-wheel-metadata test-metadata-with-extras)))
|
||||||
|
|
||||||
|
(test-equal "parse-wheel-metadata, with extras - Jedi"
|
||||||
|
'("parso")
|
||||||
|
(mock ((ice-9 ports) call-with-input-file
|
||||||
|
call-with-input-string)
|
||||||
|
(parse-wheel-metadata test-metadata-with-extras-jedi)))
|
||||||
|
|
||||||
(test-assert "pypi->guix-package"
|
(test-assert "pypi->guix-package"
|
||||||
;; Replace network resources with sample data.
|
;; Replace network resources with sample data.
|
||||||
(mock ((guix import utils) url-fetch
|
(mock ((guix import utils) url-fetch
|
||||||
|
@ -191,7 +220,7 @@ pytest (>=2.5.0)
|
||||||
(mkdir-p "foo-1.0.0/foo.egg-info/")
|
(mkdir-p "foo-1.0.0/foo.egg-info/")
|
||||||
(with-output-to-file "foo-1.0.0/foo.egg-info/requires.txt"
|
(with-output-to-file "foo-1.0.0/foo.egg-info/requires.txt"
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(display test-requires.txt)))
|
(display "wrong data to make sure we're testing wheels ")))
|
||||||
(parameterize ((current-output-port (%make-void-port "rw+")))
|
(parameterize ((current-output-port (%make-void-port "rw+")))
|
||||||
(system* "tar" "czvf" file-name "foo-1.0.0/"))
|
(system* "tar" "czvf" file-name "foo-1.0.0/"))
|
||||||
(delete-file-recursively "foo-1.0.0")
|
(delete-file-recursively "foo-1.0.0")
|
||||||
|
@ -200,13 +229,13 @@ pytest (>=2.5.0)
|
||||||
("https://example.com/foo-1.0.0-py2.py3-none-any.whl"
|
("https://example.com/foo-1.0.0-py2.py3-none-any.whl"
|
||||||
(begin
|
(begin
|
||||||
(mkdir "foo-1.0.0.dist-info")
|
(mkdir "foo-1.0.0.dist-info")
|
||||||
(with-output-to-file "foo-1.0.0.dist-info/metadata.json"
|
(with-output-to-file "foo-1.0.0.dist-info/METADATA"
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(display test-metadata)))
|
(display test-metadata)))
|
||||||
(let ((zip-file (string-append file-name ".zip")))
|
(let ((zip-file (string-append file-name ".zip")))
|
||||||
;; zip always adds a "zip" extension to the file it creates,
|
;; zip always adds a "zip" extension to the file it creates,
|
||||||
;; so we need to rename it.
|
;; so we need to rename it.
|
||||||
(system* "zip" zip-file "foo-1.0.0.dist-info/metadata.json")
|
(system* "zip" zip-file "foo-1.0.0.dist-info/METADATA")
|
||||||
(rename-file zip-file file-name))
|
(rename-file zip-file file-name))
|
||||||
(delete-file-recursively "foo-1.0.0.dist-info")))
|
(delete-file-recursively "foo-1.0.0.dist-info")))
|
||||||
(_ (error "Unexpected URL: " url)))))
|
(_ (error "Unexpected URL: " url)))))
|
||||||
|
@ -218,6 +247,9 @@ pytest (>=2.5.0)
|
||||||
(string-length test-json)))
|
(string-length test-json)))
|
||||||
("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
|
("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
|
||||||
(_ (error "Unexpected URL: " url)))))
|
(_ (error "Unexpected URL: " url)))))
|
||||||
|
;; 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")
|
(match (pypi->guix-package "foo")
|
||||||
(('package
|
(('package
|
||||||
('name "python-foo")
|
('name "python-foo")
|
||||||
|
|
Loading…
Reference in New Issue