test_extensions.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. import json
  4. from unittest.mock import Mock, patch
  5. import pytest
  6. from traitlets.config import Config, Configurable
  7. from jupyterlab.extensions import PyPIExtensionManager, ReadOnlyExtensionManager
  8. from jupyterlab.extensions.manager import ExtensionManager, ExtensionPackage, PluginManager
  9. from . import fake_client_factory
  10. @pytest.mark.parametrize(
  11. "version, expected",
  12. (
  13. ("1", "1"),
  14. ("1.0", "1.0"),
  15. ("1.0.0", "1.0.0"),
  16. ("1.0.0a52", "1.0.0-alpha.52"),
  17. ("1.0.0b3", "1.0.0-beta.3"),
  18. ("1.0.0rc22", "1.0.0-rc.22"),
  19. ("1.0.0rc23.post2", "1.0.0-rc.23"),
  20. ("1.0.0rc24.dev2", "1.0.0-rc.24"),
  21. ("1.0.0rc25.post4.dev2", "1.0.0-rc.25"),
  22. ),
  23. )
  24. def test_ExtensionManager_get_semver_version(version, expected):
  25. assert ExtensionManager.get_semver_version(version) == expected
  26. async def test_ExtensionManager_list_extensions_installed(monkeypatch):
  27. extension1 = ExtensionPackage("extension1", "Extension 1 description", "", "prebuilt")
  28. async def mock_installed(*args, **kwargs):
  29. return {"extension1": extension1}
  30. monkeypatch.setattr(ReadOnlyExtensionManager, "_get_installed_extensions", mock_installed)
  31. manager = ReadOnlyExtensionManager()
  32. extensions = await manager.list_extensions()
  33. assert extensions == ([extension1], 1)
  34. async def test_ExtensionManager_list_extensions_query(monkeypatch):
  35. extension1 = ExtensionPackage("extension1", "Extension 1 description", "", "prebuilt")
  36. extension2 = ExtensionPackage("extension2", "Extension 2 description", "", "prebuilt")
  37. async def mock_list(*args, **kwargs):
  38. return {"extension1": extension1, "extension2": extension2}, None
  39. monkeypatch.setattr(ReadOnlyExtensionManager, "list_packages", mock_list)
  40. manager = ReadOnlyExtensionManager()
  41. extensions = await manager.list_extensions("ext")
  42. assert extensions == ([extension1, extension2], 1)
  43. @patch("tornado.httpclient.AsyncHTTPClient", new_callable=fake_client_factory)
  44. async def test_ExtensionManager_list_extensions_query_allow(mock_client, monkeypatch):
  45. extension1 = ExtensionPackage("extension1", "Extension 1 description", "", "prebuilt")
  46. extension2 = ExtensionPackage("extension2", "Extension 2 description", "", "prebuilt")
  47. mock_client.body = json.dumps({"allowed_extensions": [{"name": "extension1"}]}).encode()
  48. async def mock_list(*args, **kwargs):
  49. return {"extension1": extension1, "extension2": extension2}, None
  50. monkeypatch.setattr(ReadOnlyExtensionManager, "list_packages", mock_list)
  51. manager = ReadOnlyExtensionManager(
  52. ext_options={"allowed_extensions_uris": {"http://dummy-allowed-extension"}},
  53. )
  54. extensions = await manager.list_extensions("ext")
  55. assert extensions == ([extension1], 1)
  56. @patch("tornado.httpclient.AsyncHTTPClient", new_callable=fake_client_factory)
  57. async def test_ExtensionManager_list_extensions_query_block(mock_client, monkeypatch):
  58. extension1 = ExtensionPackage("extension1", "Extension 1 description", "", "prebuilt")
  59. extension2 = ExtensionPackage("extension2", "Extension 2 description", "", "prebuilt")
  60. mock_client.body = json.dumps({"blocked_extensions": [{"name": "extension1"}]}).encode()
  61. async def mock_list(*args, **kwargs):
  62. return {"extension1": extension1, "extension2": extension2}, None
  63. monkeypatch.setattr(ReadOnlyExtensionManager, "list_packages", mock_list)
  64. manager = ReadOnlyExtensionManager(
  65. ext_options={"blocked_extensions_uris": {"http://dummy-blocked-extension"}}
  66. )
  67. extensions = await manager.list_extensions("ext")
  68. assert extensions == ([extension2], 1)
  69. @patch("tornado.httpclient.AsyncHTTPClient", new_callable=fake_client_factory)
  70. async def test_ExtensionManager_list_extensions_query_allow_block(mock_client, monkeypatch):
  71. extension1 = ExtensionPackage("extension1", "Extension 1 description", "", "prebuilt")
  72. extension2 = ExtensionPackage("extension2", "Extension 2 description", "", "prebuilt")
  73. mock_client.body = json.dumps(
  74. {
  75. "allowed_extensions": [{"name": "extension1"}],
  76. "blocked_extensions": [{"name": "extension1"}],
  77. }
  78. ).encode()
  79. async def mock_list(*args, **kwargs):
  80. return {"extension1": extension1, "extension2": extension2}, None
  81. monkeypatch.setattr(ReadOnlyExtensionManager, "list_packages", mock_list)
  82. manager = ReadOnlyExtensionManager(
  83. ext_options={
  84. "allowed_extensions_uris": {"http://dummy-allowed-extension"},
  85. "blocked_extensions_uris": {"http://dummy-blocked-extension"},
  86. }
  87. )
  88. extensions = await manager.list_extensions("ext")
  89. assert extensions == ([extension1], 1)
  90. async def test_ExtensionManager_install():
  91. manager = ReadOnlyExtensionManager()
  92. result = await manager.install("extension1")
  93. assert result.status == "error"
  94. assert result.message == "Extension installation not supported."
  95. async def test_ExtensionManager_uninstall():
  96. manager = ReadOnlyExtensionManager()
  97. result = await manager.uninstall("extension1")
  98. assert result.status == "error"
  99. assert result.message == "Extension removal not supported."
  100. @patch("jupyterlab.extensions.pypi.xmlrpc.client")
  101. async def test_ExtensionManager_list_extensions_query_sort(mocked_rpcclient):
  102. extension_data = [
  103. {
  104. "name": "jupyterlab-apod",
  105. "project_urls": {
  106. "Homepage": "https://github.com/jupyterlab/jupyterlab_apod",
  107. },
  108. },
  109. {
  110. "name": "jupyterlab-gitlab",
  111. "project_urls": {
  112. "Homepage": "https://github.com/jupyterlab-contrib/jupyterlab-gitlab/issues",
  113. },
  114. },
  115. {
  116. "name": "jupyterlab-git",
  117. "project_url": "https://github.com/jupyterlab/jupyterlab-git",
  118. },
  119. {
  120. "name": "jupyterlab-rainbow-brackets",
  121. "project_url": "https://github.com/krassowski/jupyterlab-rainbow-brackets",
  122. },
  123. {"name": "nbdime", "home_page": "https://github.com/jupyter/nbdime"},
  124. {
  125. "name": "rise",
  126. "project_urls": {
  127. "Source Code": "https://github.com/jupyterlab-contrib/rise",
  128. },
  129. },
  130. ]
  131. proxy = Mock(
  132. browse=Mock(return_value=[[extension["name"], "1.0.0"] for extension in extension_data]),
  133. )
  134. mocked_rpcclient.ServerProxy = Mock(return_value=proxy)
  135. manager = PyPIExtensionManager()
  136. extensions = {
  137. extension["name"]: {"version": "1.0.0", **extension} for extension in extension_data
  138. }
  139. async def mock_pkg_metadata(name, l, b): # noqa
  140. return extensions[name]
  141. manager._fetch_package_metadata = mock_pkg_metadata
  142. first_page, pages_count = await manager.list_extensions("", per_page=3)
  143. assert [extension.name for extension in first_page] == [
  144. # jupyter/jupyterlab
  145. "jupyterlab-git",
  146. "nbdime",
  147. # jupyterlab-contrib
  148. "jupyterlab-gitlab",
  149. ]
  150. assert pages_count == 2
  151. second_page, pages_count = await manager.list_extensions("", page=2, per_page=3)
  152. assert [extension.name for extension in second_page] == [
  153. # jupyterlab-contrib
  154. "rise",
  155. # other third-party
  156. "jupyterlab-rainbow-brackets",
  157. # example extensions
  158. "jupyterlab-apod",
  159. ]
  160. @patch("jupyterlab.extensions.pypi.xmlrpc.client")
  161. async def test_PyPiExtensionManager_list_extensions_query(mocked_rpcclient):
  162. extension1 = ExtensionPackage(
  163. name="jupyterlab-git",
  164. description="A JupyterLab extension for version control using git",
  165. homepage_url="https://github.com/jupyterlab/jupyterlab-git",
  166. pkg_type="prebuilt",
  167. latest_version="0.37.1",
  168. author="Jupyter Development Team",
  169. license="BSD-3-Clause",
  170. package_manager_url="https://pypi.org/project/jupyterlab-git/",
  171. )
  172. extension2 = ExtensionPackage(
  173. name="jupyterlab-github",
  174. description="JupyterLab viewer for GitHub repositories",
  175. homepage_url="https://github.com/jupyterlab/jupyterlab-github/blob/main/README.md",
  176. pkg_type="prebuilt",
  177. latest_version="3.0.1",
  178. author="Ian Rose",
  179. license="BSD-3-Clause",
  180. bug_tracker_url="https://github.com/jupyterlab/jupyterlab-github/issues",
  181. package_manager_url="https://pypi.org/project/jupyterlab-github/",
  182. repository_url="https://github.com/jupyterlab/jupyterlab-github",
  183. )
  184. proxy = Mock(
  185. browse=Mock(
  186. return_value=[
  187. ["jupyterlab-git", "0.33.0"],
  188. ["jupyterlab-git", "0.34.0"],
  189. ["jupyterlab-git", "0.34.1"],
  190. ["jupyterlab-git", "0.37.0"],
  191. ["jupyterlab-git", "0.37.1"],
  192. ["jupyterlab-github", "3.0.0"],
  193. ["jupyterlab-github", "3.0.1"],
  194. ]
  195. ),
  196. )
  197. mocked_rpcclient.ServerProxy = Mock(return_value=proxy)
  198. manager = PyPIExtensionManager()
  199. async def mock_pkg_metadata(n, l, b): # noqa
  200. return (
  201. {
  202. "name": "jupyterlab-git",
  203. "version": "0.37.1",
  204. "stable_version": None,
  205. "bugtrack_url": None,
  206. "package_url": "https://pypi.org/project/jupyterlab-git/",
  207. "release_url": "https://pypi.org/project/jupyterlab-git/0.37.1/",
  208. "docs_url": None,
  209. "home_page": "https://github.com/jupyterlab/jupyterlab-git",
  210. "download_url": "",
  211. "project_url": "",
  212. "project_urls": {},
  213. "author": "Jupyter Development Team",
  214. "author_email": "",
  215. "maintainer": "",
  216. "maintainer_email": "",
  217. "summary": "A JupyterLab extension for version control using git",
  218. "license": "BSD-3-Clause",
  219. "keywords": "Jupyter,JupyterLab,JupyterLab3,jupyterlab-extension,Git",
  220. "platform": "Linux",
  221. "classifiers": [
  222. "Framework :: Jupyter",
  223. "Framework :: Jupyter :: JupyterLab",
  224. "Framework :: Jupyter :: JupyterLab :: 3",
  225. "Framework :: Jupyter :: JupyterLab :: Extensions",
  226. "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
  227. "Intended Audience :: Developers",
  228. "Intended Audience :: Science/Research",
  229. "License :: OSI Approved :: BSD License",
  230. "Programming Language :: Python",
  231. "Programming Language :: Python :: 3",
  232. "Programming Language :: Python :: 3.10",
  233. "Programming Language :: Python :: 3.6",
  234. "Programming Language :: Python :: 3.7",
  235. "Programming Language :: Python :: 3.8",
  236. "Programming Language :: Python :: 3.9",
  237. ],
  238. "requires": [],
  239. "requires_dist": [
  240. "jupyter-server",
  241. "nbdime (~=3.1)",
  242. "nbformat",
  243. "packaging",
  244. "pexpect",
  245. "coverage ; extra == 'dev'",
  246. "jupyterlab (~=3.0) ; extra == 'dev'",
  247. "pre-commit ; extra == 'dev'",
  248. "pytest ; extra == 'dev'",
  249. "pytest-asyncio ; extra == 'dev'",
  250. "pytest-cov ; extra == 'dev'",
  251. "pytest-tornasync ; extra == 'dev'",
  252. "coverage ; extra == 'tests'",
  253. "jupyterlab (~=3.0) ; extra == 'tests'",
  254. "pre-commit ; extra == 'tests'",
  255. "pytest ; extra == 'tests'",
  256. "pytest-asyncio ; extra == 'tests'",
  257. "pytest-cov ; extra == 'tests'",
  258. "pytest-tornasync ; extra == 'tests'",
  259. "hybridcontents ; extra == 'tests'",
  260. "jupytext ; extra == 'tests'",
  261. ],
  262. "provides": [],
  263. "provides_dist": [],
  264. "obsoletes": [],
  265. "obsoletes_dist": [],
  266. "requires_python": "<4,>=3.6",
  267. "requires_external": [],
  268. "_pypi_ordering": 55,
  269. "downloads": {"last_day": -1, "last_week": -1, "last_month": -1},
  270. "cheesecake_code_kwalitee_id": None,
  271. "cheesecake_documentation_id": None,
  272. "cheesecake_installability_id": None,
  273. }
  274. if n == "jupyterlab-git"
  275. else {
  276. "name": "jupyterlab-github",
  277. "version": "3.0.1",
  278. "stable_version": None,
  279. "bugtrack_url": None,
  280. "package_url": "https://pypi.org/project/jupyterlab-github/",
  281. "release_url": "https://pypi.org/project/jupyterlab-github/3.0.1/",
  282. "docs_url": None,
  283. "home_page": "",
  284. "download_url": "",
  285. "project_url": "",
  286. "project_urls": {
  287. "Homepage": "https://github.com/jupyterlab/jupyterlab-github/blob/main/README.md",
  288. "Bug Tracker": "https://github.com/jupyterlab/jupyterlab-github/issues",
  289. "Source Code": "https://github.com/jupyterlab/jupyterlab-github",
  290. },
  291. "author": "Ian Rose",
  292. "author_email": "jupyter@googlegroups.com",
  293. "maintainer": "",
  294. "maintainer_email": "",
  295. "summary": "JupyterLab viewer for GitHub repositories",
  296. "license": "BSD-3-Clause",
  297. "keywords": "Jupyter,JupyterLab,JupyterLab3",
  298. "platform": "Linux",
  299. "classifiers": [
  300. "Framework :: Jupyter",
  301. "Framework :: Jupyter :: JupyterLab",
  302. "Framework :: Jupyter :: JupyterLab :: 3",
  303. "Framework :: Jupyter :: JupyterLab :: Extensions",
  304. "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt",
  305. "License :: OSI Approved :: BSD License",
  306. "Programming Language :: Python",
  307. "Programming Language :: Python :: 3",
  308. "Programming Language :: Python :: 3.6",
  309. "Programming Language :: Python :: 3.7",
  310. "Programming Language :: Python :: 3.8",
  311. "Programming Language :: Python :: 3.9",
  312. ],
  313. "requires": [],
  314. "requires_dist": ["jupyterlab (~=3.0)"],
  315. "provides": [],
  316. "provides_dist": [],
  317. "obsoletes": [],
  318. "obsoletes_dist": [],
  319. "requires_python": ">=3.6",
  320. "requires_external": [],
  321. "_pypi_ordering": 12,
  322. "downloads": {"last_day": -1, "last_week": -1, "last_month": -1},
  323. "cheesecake_code_kwalitee_id": None,
  324. "cheesecake_documentation_id": None,
  325. "cheesecake_installability_id": None,
  326. }
  327. )
  328. manager._fetch_package_metadata = mock_pkg_metadata
  329. extensions = await manager.list_extensions("git")
  330. assert extensions == ([extension1, extension2], 1)
  331. async def test_PyPiExtensionManager_custom_server_url():
  332. BASE_URL = "https://mylocal.pypi.server/pypi" # noqa
  333. parent = Configurable(config=Config({"PyPIExtensionManager": {"base_url": BASE_URL}}))
  334. manager = PyPIExtensionManager(parent=parent)
  335. assert manager.base_url == BASE_URL
  336. LEVELS = ["user", "sys_prefix", "system"]
  337. @pytest.mark.parametrize("level", LEVELS)
  338. async def test_PyPiExtensionManager_custom_level(level):
  339. parent = Configurable(config=Config({"PyPIExtensionManager": {"level": level}}))
  340. manager = PyPIExtensionManager(parent=parent)
  341. assert manager.level == level
  342. @pytest.mark.parametrize("level", LEVELS)
  343. async def test_PyPiExtensionManager_inherits_custom_level(level):
  344. parent = Configurable(config=Config({"PluginManager": {"level": level}}))
  345. manager = PyPIExtensionManager(parent=parent)
  346. assert manager.level == level
  347. @pytest.mark.parametrize("level", LEVELS)
  348. async def test_PluginManager_custom_level(level):
  349. parent = Configurable(config=Config({"PluginManager": {"level": level}}))
  350. manager = PluginManager(parent=parent)
  351. assert manager.level == level
  352. async def test_PluginManager_default_level():
  353. manager = PluginManager()
  354. assert manager.level == "sys_prefix"