pypi.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2014 David Thompson <davet@gnu.org>
  3. ;;; Copyright © 2016 Ricardo Wurmus <rekado@elephly.net>
  4. ;;; Copyright © 2019 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (test-pypi)
  21. #:use-module (guix import pypi)
  22. #:use-module (guix base32)
  23. #:use-module (guix memoization)
  24. #:use-module (gcrypt hash)
  25. #:use-module (guix memoization)
  26. #:use-module (guix tests)
  27. #:use-module (guix build-system python)
  28. #:use-module ((guix build utils) #:select (delete-file-recursively which mkdir-p))
  29. #:use-module (srfi srfi-64)
  30. #:use-module (ice-9 match))
  31. (define test-json
  32. "{
  33. \"info\": {
  34. \"version\": \"1.0.0\",
  35. \"name\": \"foo\",
  36. \"license\": \"GNU LGPL\",
  37. \"summary\": \"summary\",
  38. \"home_page\": \"http://example.com\",
  39. },
  40. \"releases\": {
  41. \"1.0.0\": [
  42. {
  43. \"url\": \"https://example.com/foo-1.0.0.egg\",
  44. \"packagetype\": \"bdist_egg\",
  45. }, {
  46. \"url\": \"https://example.com/foo-1.0.0.tar.gz\",
  47. \"packagetype\": \"sdist\",
  48. }, {
  49. \"url\": \"https://example.com/foo-1.0.0-py2.py3-none-any.whl\",
  50. \"packagetype\": \"bdist_wheel\",
  51. }
  52. ]
  53. }
  54. }")
  55. (define test-source-hash
  56. "")
  57. (define test-specifications
  58. '("Fizzy [foo, bar]"
  59. "PickyThing<1.6,>1.9,!=1.9.6,<2.0a0,==2.4c1"
  60. "SomethingWithMarker[foo]>1.0;python_version<\"2.7\""
  61. "requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < \"2.7\""
  62. "pip @ https://github.com/pypa/pip/archive/1.3.1.zip#\
  63. sha1=da9234ee9982d4bbb3c72346a6de940a148ea686"))
  64. (define test-requires.txt "\
  65. # A comment
  66. # A comment after a space
  67. foo ~= 3
  68. bar != 2
  69. [test]
  70. pytest (>=2.5.0)
  71. ")
  72. ;; Beaker contains only optional dependencies.
  73. (define test-requires.txt-beaker "\
  74. [crypto]
  75. pycryptopp>=0.5.12
  76. [cryptography]
  77. cryptography
  78. [testsuite]
  79. Mock
  80. coverage
  81. ")
  82. (define test-metadata "\
  83. Classifier: Programming Language :: Python :: 3.7
  84. Requires-Dist: baz ~= 3
  85. Requires-Dist: bar != 2
  86. Provides-Extra: test
  87. Requires-Dist: pytest (>=2.5.0) ; extra == 'test'
  88. ")
  89. (define test-metadata-with-extras "
  90. Classifier: Programming Language :: Python :: 3.7
  91. Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
  92. Requires-Dist: wrapt (<2,>=1)
  93. Requires-Dist: bar
  94. Provides-Extra: dev
  95. Requires-Dist: tox ; extra == 'dev'
  96. Requires-Dist: bumpversion (<1) ; extra == 'dev'
  97. ")
  98. ;;; Provides-Extra can appear before Requires-Dist.
  99. (define test-metadata-with-extras-jedi "\
  100. Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
  101. Provides-Extra: testing
  102. Requires-Dist: parso (>=0.3.0)
  103. Provides-Extra: testing
  104. Requires-Dist: pytest (>=3.1.0); extra == 'testing'
  105. ")
  106. (test-begin "pypi")
  107. (test-equal "guix-package->pypi-name, old URL style"
  108. "psutil"
  109. (guix-package->pypi-name
  110. (dummy-package "foo"
  111. (source (dummy-origin
  112. (uri
  113. "https://pypi.org/packages/source/p/psutil/psutil-4.3.0.tar.gz"))))))
  114. (test-equal "guix-package->pypi-name, new URL style"
  115. "certbot"
  116. (guix-package->pypi-name
  117. (dummy-package "foo"
  118. (source (dummy-origin
  119. (uri
  120. "https://pypi.org/packages/a2/3b/4756e6a0ceb14e084042a2a65c615d68d25621c6fd446d0fc10d14c4ce7d/certbot-0.8.1.tar.gz"))))))
  121. (test-equal "guix-package->pypi-name, several URLs"
  122. "cram"
  123. (guix-package->pypi-name
  124. (dummy-package "foo"
  125. (source
  126. (dummy-origin
  127. (uri (list "https://bitheap.org/cram/cram-0.7.tar.gz"
  128. (pypi-uri "cram" "0.7"))))))))
  129. (test-equal "specification->requirement-name"
  130. '("Fizzy" "PickyThing" "SomethingWithMarker" "requests" "pip")
  131. (map specification->requirement-name test-specifications))
  132. (test-equal "parse-requires.txt"
  133. (list '("foo" "bar") '("pytest"))
  134. (mock ((ice-9 ports) call-with-input-file
  135. call-with-input-string)
  136. (parse-requires.txt test-requires.txt)))
  137. (test-equal "parse-requires.txt - Beaker"
  138. (list '() '("Mock" "coverage"))
  139. (mock ((ice-9 ports) call-with-input-file
  140. call-with-input-string)
  141. (parse-requires.txt test-requires.txt-beaker)))
  142. (test-equal "parse-wheel-metadata, with extras"
  143. (list '("wrapt" "bar") '("tox" "bumpversion"))
  144. (mock ((ice-9 ports) call-with-input-file
  145. call-with-input-string)
  146. (parse-wheel-metadata test-metadata-with-extras)))
  147. (test-equal "parse-wheel-metadata, with extras - Jedi"
  148. (list '("parso") '("pytest"))
  149. (mock ((ice-9 ports) call-with-input-file
  150. call-with-input-string)
  151. (parse-wheel-metadata test-metadata-with-extras-jedi)))
  152. (test-assert "pypi->guix-package, no wheel"
  153. ;; Replace network resources with sample data.
  154. (mock ((guix import utils) url-fetch
  155. (lambda (url file-name)
  156. (match url
  157. ("https://example.com/foo-1.0.0.tar.gz"
  158. (begin
  159. ;; Unusual requires.txt location should still be found.
  160. (mkdir-p "foo-1.0.0/src/bizarre.egg-info")
  161. (with-output-to-file "foo-1.0.0/src/bizarre.egg-info/requires.txt"
  162. (lambda ()
  163. (display test-requires.txt)))
  164. (parameterize ((current-output-port (%make-void-port "rw+")))
  165. (system* "tar" "czvf" file-name "foo-1.0.0/"))
  166. (delete-file-recursively "foo-1.0.0")
  167. (set! test-source-hash
  168. (call-with-input-file file-name port-sha256))))
  169. ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
  170. (_ (error "Unexpected URL: " url)))))
  171. (mock ((guix http-client) http-fetch
  172. (lambda (url . rest)
  173. (match url
  174. ("https://pypi.org/pypi/foo/json"
  175. (values (open-input-string test-json)
  176. (string-length test-json)))
  177. ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
  178. (_ (error "Unexpected URL: " url)))))
  179. (match (pypi->guix-package "foo")
  180. (('package
  181. ('name "python-foo")
  182. ('version "1.0.0")
  183. ('source ('origin
  184. ('method 'url-fetch)
  185. ('uri ('pypi-uri "foo" 'version))
  186. ('sha256
  187. ('base32
  188. (? string? hash)))))
  189. ('build-system 'python-build-system)
  190. ('propagated-inputs
  191. ('quasiquote
  192. (("python-bar" ('unquote 'python-bar))
  193. ("python-foo" ('unquote 'python-foo)))))
  194. ('native-inputs
  195. ('quasiquote
  196. (("python-pytest" ('unquote 'python-pytest)))))
  197. ('home-page "http://example.com")
  198. ('synopsis "summary")
  199. ('description "summary")
  200. ('license 'license:lgpl2.0))
  201. (string=? (bytevector->nix-base32-string
  202. test-source-hash)
  203. hash))
  204. (x
  205. (pk 'fail x #f))))))
  206. (test-skip (if (which "zip") 0 1))
  207. (test-assert "pypi->guix-package, wheels"
  208. ;; Replace network resources with sample data.
  209. (mock ((guix import utils) url-fetch
  210. (lambda (url file-name)
  211. (match url
  212. ("https://example.com/foo-1.0.0.tar.gz"
  213. (begin
  214. (mkdir-p "foo-1.0.0/foo.egg-info/")
  215. (with-output-to-file "foo-1.0.0/foo.egg-info/requires.txt"
  216. (lambda ()
  217. (display "wrong data to make sure we're testing wheels ")))
  218. (parameterize ((current-output-port (%make-void-port "rw+")))
  219. (system* "tar" "czvf" file-name "foo-1.0.0/"))
  220. (delete-file-recursively "foo-1.0.0")
  221. (set! test-source-hash
  222. (call-with-input-file file-name port-sha256))))
  223. ("https://example.com/foo-1.0.0-py2.py3-none-any.whl"
  224. (begin
  225. (mkdir "foo-1.0.0.dist-info")
  226. (with-output-to-file "foo-1.0.0.dist-info/METADATA"
  227. (lambda ()
  228. (display test-metadata)))
  229. (let ((zip-file (string-append file-name ".zip")))
  230. ;; zip always adds a "zip" extension to the file it creates,
  231. ;; so we need to rename it.
  232. (system* "zip" "-q" zip-file "foo-1.0.0.dist-info/METADATA")
  233. (rename-file zip-file file-name))
  234. (delete-file-recursively "foo-1.0.0.dist-info")))
  235. (_ (error "Unexpected URL: " url)))))
  236. (mock ((guix http-client) http-fetch
  237. (lambda (url . rest)
  238. (match url
  239. ("https://pypi.org/pypi/foo/json"
  240. (values (open-input-string test-json)
  241. (string-length test-json)))
  242. ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
  243. (_ (error "Unexpected URL: " url)))))
  244. ;; Not clearing the memoization cache here would mean returning the value
  245. ;; computed in the previous test.
  246. (invalidate-memoization! pypi->guix-package)
  247. (match (pypi->guix-package "foo")
  248. (('package
  249. ('name "python-foo")
  250. ('version "1.0.0")
  251. ('source ('origin
  252. ('method 'url-fetch)
  253. ('uri ('pypi-uri "foo" 'version))
  254. ('sha256
  255. ('base32
  256. (? string? hash)))))
  257. ('build-system 'python-build-system)
  258. ('propagated-inputs
  259. ('quasiquote
  260. (("python-bar" ('unquote 'python-bar))
  261. ("python-baz" ('unquote 'python-baz)))))
  262. ('native-inputs
  263. ('quasiquote
  264. (("python-pytest" ('unquote 'python-pytest)))))
  265. ('home-page "http://example.com")
  266. ('synopsis "summary")
  267. ('description "summary")
  268. ('license 'license:lgpl2.0))
  269. (string=? (bytevector->nix-base32-string
  270. test-source-hash)
  271. hash))
  272. (x
  273. (pk 'fail x #f))))))
  274. (test-assert "pypi->guix-package, no usable requirement file."
  275. ;; Replace network resources with sample data.
  276. (mock ((guix import utils) url-fetch
  277. (lambda (url file-name)
  278. (match url
  279. ("https://example.com/foo-1.0.0.tar.gz"
  280. (mkdir-p "foo-1.0.0/foo.egg-info/")
  281. (parameterize ((current-output-port (%make-void-port "rw+")))
  282. (system* "tar" "czvf" file-name "foo-1.0.0/"))
  283. (delete-file-recursively "foo-1.0.0")
  284. (set! test-source-hash
  285. (call-with-input-file file-name port-sha256)))
  286. ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
  287. (_ (error "Unexpected URL: " url)))))
  288. (mock ((guix http-client) http-fetch
  289. (lambda (url . rest)
  290. (match url
  291. ("https://pypi.org/pypi/foo/json"
  292. (values (open-input-string test-json)
  293. (string-length test-json)))
  294. ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
  295. (_ (error "Unexpected URL: " url)))))
  296. ;; Not clearing the memoization cache here would mean returning the value
  297. ;; computed in the previous test.
  298. (invalidate-memoization! pypi->guix-package)
  299. (match (pypi->guix-package "foo")
  300. (('package
  301. ('name "python-foo")
  302. ('version "1.0.0")
  303. ('source ('origin
  304. ('method 'url-fetch)
  305. ('uri ('pypi-uri "foo" 'version))
  306. ('sha256
  307. ('base32
  308. (? string? hash)))))
  309. ('build-system 'python-build-system)
  310. ('home-page "http://example.com")
  311. ('synopsis "summary")
  312. ('description "summary")
  313. ('license 'license:lgpl2.0))
  314. (string=? (bytevector->nix-base32-string
  315. test-source-hash)
  316. hash))
  317. (x
  318. (pk 'fail x #f))))))
  319. (test-end "pypi")