Subject: [bug#51493] [PATCH 1/5] import: pypi: Allow imports of a specific version.

From: Ludovic Courtès <[email-masked]>
Date: Fri, 29 Oct 2021 23:35:35 +0200

* guix/import/pypi.scm (latest-version): New procedure.
(latest-source-release): Rename to...
(source-release): ... this.  Add 'version' parameter.
(latest-wheel-release): Rename to...
(wheel-release): ... this.  Add 'version' parameter.
(pypi->guix-package): Honor 'version' parameter.
(pypi-recursive-import): Add 'version' parameter and honor it.
* guix/scripts/import/pypi.scm (guix-import-pypi): Expect a spec.  Pass
it to 'package-name->name+version'.  Pass the 'version' parameter.
* tests/pypi.scm ("pypi->guix-package, no wheel"): Exercise
the #:version parameter.
* doc/guix.texi (Invoking guix import): Document it.
---
 doc/guix.texi                | 10 ++++++--
 guix/import/pypi.scm         | 47 +++++++++++++++++++-----------------
 guix/scripts/import/pypi.scm | 32 ++++++++++++------------
 tests/pypi.scm               | 12 ++++++---
 4 files changed, 59 insertions(+), 42 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 22215214e0..b742a4808a 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -11723,13 +11723,19 @@ information, including package dependencies.  For maximum efficiency, it
 is recommended to install the @command{unzip} utility, so that the
 importer can unzip Python wheels and gather data from them.
 
-The command below imports metadata for the @code{itsdangerous} Python
-package:
+The command below imports metadata for the latest version of the
+@code{itsdangerous} Python package:
 
 @example
 guix import pypi itsdangerous
 @end example
 
+You can also ask for a specific version:
+
+@example
+guix import pypi itsdangerous@@1.1.0
+@end example
+
 @table @code
 @item --recursive
 @itemx -r
diff --git a/guix/import/pypi.scm b/guix/import/pypi.scm
index f908136481..418a3556ec 100644
--- a/guix/import/pypi.scm
+++ b/guix/import/pypi.scm
@@ -128,27 +128,30 @@ (define-condition-type &missing-source-error &error
   missing-source-error?
   (package  missing-source-error-package))
 
-(define (latest-source-release pypi-package)
-  "Return the latest source release for PYPI-PACKAGE."
-  (let ((releases (assoc-ref (pypi-project-releases pypi-package)
-                             (project-info-version
-                              (pypi-project-info pypi-package)))))
+(define (latest-version project)
+  "Return the latest version of PROJECT, a <pypi-project> record."
+  (project-info-version (pypi-project-info project)))
+
+(define* (source-release pypi-package
+                         #:optional (version (latest-version pypi-package)))
+  "Return the source release of VERSION for PYPI-PACKAGE, a <pypi-project>
+record, by default the latest version."
+  (let ((releases (or (assoc-ref (pypi-project-releases pypi-package) version)
+                      '())))
     (or (find (lambda (release)
                 (string=? "sdist" (distribution-package-type release)))
               releases)
         (raise (condition (&missing-source-error
                            (package pypi-package)))))))
 
-(define (latest-wheel-release pypi-package)
+(define* (wheel-release pypi-package
+                        #:optional (version (latest-version pypi-package)))
   "Return the url of the wheel for the latest release of pypi-package,
 or #f if there isn't any."
-  (let ((releases (assoc-ref (pypi-project-releases pypi-package)
-                             (project-info-version
-                              (pypi-project-info pypi-package)))))
-    (or (find (lambda (release)
-                (string=? "bdist_wheel" (distribution-package-type release)))
-              releases)
-        #f)))
+  (let ((releases (assoc-ref (pypi-project-releases pypi-package) version)))
+    (find (lambda (release)
+            (string=? "bdist_wheel" (distribution-package-type release)))
+          releases)))
 
 (define (python->package-name name)
   "Given the NAME of a package on PyPI, return a Guix-compliant name for the
@@ -484,18 +487,17 @@ (define pypi->guix-package
      "Fetch the metadata for PACKAGE-NAME from pypi.org, and return the
 `package' s-expression corresponding to that package, or #f on failure."
      (let* ((project (pypi-fetch package-name))
-            (info    (and project (pypi-project-info project))))
+            (info    (and=> project pypi-project-info))
+            (version (or version (and=> project latest-version))))
        (and project
             (guard (c ((missing-source-error? c)
                        (let ((package (missing-source-error-package c)))
                          (leave (G_ "no source release for pypi package ~a ~a~%")
-                                (project-info-name info)
-                                (project-info-version info)))))
-              (make-pypi-sexp (project-info-name info)
-                              (project-info-version info)
-                              (and=> (latest-source-release project)
+                                (project-info-name info) version))))
+              (make-pypi-sexp (project-info-name info) version
+                              (and=> (source-release project version)
                                      distribution-url)
-                              (and=> (latest-wheel-release project)
+                              (and=> (wheel-release project version)
                                      distribution-url)
                               (project-info-home-page info)
                               (project-info-summary info)
@@ -503,8 +505,9 @@ (define pypi->guix-package
                               (string->license
                                (project-info-license info)))))))))
 
-(define (pypi-recursive-import package-name)
+(define* (pypi-recursive-import package-name #:optional version)
   (recursive-import package-name
+                    #:version version
                     #:repo->guix-package pypi->guix-package
                     #:guix-name python->package-name))
 
@@ -538,7 +541,7 @@ (define (latest-release package)
            (let* ((info    (pypi-project-info pypi-package))
                   (version (project-info-version info))
                   (url     (distribution-url
-                            (latest-source-release pypi-package))))
+                            (source-release pypi-package))))
              (upstream-source
               (urls (list url))
               (input-changes
diff --git a/guix/scripts/import/pypi.scm b/guix/scripts/import/pypi.scm
index 9170a0b359..a52cd95c93 100644
--- a/guix/scripts/import/pypi.scm
+++ b/guix/scripts/import/pypi.scm
@@ -27,6 +27,7 @@ (define-module (guix scripts import pypi)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-11)
   #:use-module (srfi srfi-37)
+  #:use-module (srfi srfi-71)
   #:use-module (ice-9 match)
   #:use-module (ice-9 format)
   #:export (guix-import-pypi))
@@ -83,21 +84,22 @@ (define (parse-options)
                             (_ #f))
                            (reverse opts))))
     (match args
-      ((package-name)
-       (if (assoc-ref opts 'recursive)
-           ;; Recursive import
-           (map (match-lambda
-                  ((and ('package ('name name) . rest) pkg)
-                   `(define-public ,(string->symbol name)
-                      ,pkg))
-                  (_ #f))
-                (pypi-recursive-import package-name))
-           ;; Single import
-           (let ((sexp (pypi->guix-package package-name)))
-             (unless sexp
-               (leave (G_ "failed to download meta-data for package '~a'~%")
-                      package-name))
-             sexp)))
+      ((spec)
+       (let ((name version (package-name->name+version spec)))
+         (if (assoc-ref opts 'recursive)
+             ;; Recursive import
+             (map (match-lambda
+                    ((and ('package ('name name) . rest) pkg)
+                     `(define-public ,(string->symbol name)
+                        ,pkg))
+                    (_ #f))
+                  (pypi-recursive-import name version))
+             ;; Single import
+             (let ((sexp (pypi->guix-package name #:version version)))
+               (unless sexp
+                 (leave (G_ "failed to download meta-data for package '~a'~%")
+                        name))
+               sexp))))
       (()
        (leave (G_ "too few arguments~%")))
       ((many ...)
diff --git a/tests/pypi.scm b/tests/pypi.scm
index 70f4298a90..ad869ac31f 100644
--- a/tests/pypi.scm
+++ b/tests/pypi.scm
@@ -260,9 +260,15 @@ (define test-metadata-with-extras-jedi "\
                      ('synopsis "summary")
                      ('description "summary")
                      ('license 'license:lgpl2.0))
-                   (string=? (bytevector->nix-base32-string
-                              test-source-hash)
-                             hash))
+                   (and (string=? (bytevector->nix-base32-string
+                                   test-source-hash)
+                                  hash)
+                        (equal? (pypi->guix-package "foo" #:version "1.0.0")
+                                (pypi->guix-package "foo"))
+                        (catch 'quit
+                          (lambda ()
+                            (pypi->guix-package "foo" #:version "42"))
+                          (const #t))))
                   (x
                    (pk 'fail x #f))))))
 
-- 
2.33.0