test_nbextensions.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. # coding: utf-8
  2. """Test installation of notebook extensions"""
  3. # Copyright (c) Jupyter Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. import glob
  6. import os
  7. import sys
  8. import tarfile
  9. import zipfile
  10. from io import BytesIO, StringIO
  11. from os.path import basename, join as pjoin
  12. from traitlets.tests.utils import check_help_all_output
  13. from unittest import TestCase
  14. try:
  15. from unittest.mock import patch
  16. except ImportError:
  17. from mock import patch # py2
  18. import ipython_genutils.testing.decorators as dec
  19. from ipython_genutils import py3compat
  20. from ipython_genutils.tempdir import TemporaryDirectory
  21. from notebook import nbextensions
  22. from notebook.nbextensions import (install_nbextension, check_nbextension,
  23. enable_nbextension, disable_nbextension,
  24. install_nbextension_python, uninstall_nbextension_python,
  25. enable_nbextension_python, disable_nbextension_python, _get_config_dir,
  26. validate_nbextension, validate_nbextension_python
  27. )
  28. from notebook.config_manager import BaseJSONConfigManager
  29. def touch(file_name, mtime=None):
  30. """ensure a file exists, and set its modification time
  31. returns the modification time of the file
  32. """
  33. open(file_name, 'a').close()
  34. # set explicit mtime
  35. if mtime:
  36. atime = os.stat(file_name).st_atime
  37. os.utime(file_name, (atime, mtime))
  38. return os.stat(file_name).st_mtime
  39. def test_help_output():
  40. check_help_all_output('notebook.nbextensions')
  41. check_help_all_output('notebook.nbextensions', ['enable'])
  42. check_help_all_output('notebook.nbextensions', ['disable'])
  43. check_help_all_output('notebook.nbextensions', ['install'])
  44. check_help_all_output('notebook.nbextensions', ['uninstall'])
  45. class TestInstallNBExtension(TestCase):
  46. def tempdir(self):
  47. td = TemporaryDirectory()
  48. self.tempdirs.append(td)
  49. return py3compat.cast_unicode(td.name)
  50. def setUp(self):
  51. # Any TemporaryDirectory objects appended to this list will be cleaned
  52. # up at the end of the test run.
  53. self.tempdirs = []
  54. @self.addCleanup
  55. def cleanup_tempdirs():
  56. for d in self.tempdirs:
  57. d.cleanup()
  58. self.src = self.tempdir()
  59. self.files = files = [
  60. pjoin(u'ƒile'),
  61. pjoin(u'∂ir', u'ƒile1'),
  62. pjoin(u'∂ir', u'∂ir2', u'ƒile2'),
  63. ]
  64. for file_name in files:
  65. fullpath = os.path.join(self.src, file_name)
  66. parent = os.path.dirname(fullpath)
  67. if not os.path.exists(parent):
  68. os.makedirs(parent)
  69. touch(fullpath)
  70. self.test_dir = self.tempdir()
  71. self.data_dir = os.path.join(self.test_dir, 'data')
  72. self.config_dir = os.path.join(self.test_dir, 'config')
  73. self.system_data_dir = os.path.join(self.test_dir, 'system_data')
  74. self.system_path = [self.system_data_dir]
  75. self.system_nbext = os.path.join(self.system_data_dir, 'nbextensions')
  76. # Patch out os.environ so that tests are isolated from the real OS
  77. # environment.
  78. self.patch_env = patch.dict('os.environ', {
  79. 'JUPYTER_CONFIG_DIR': self.config_dir,
  80. 'JUPYTER_DATA_DIR': self.data_dir,
  81. })
  82. self.patch_env.start()
  83. self.addCleanup(self.patch_env.stop)
  84. # Patch out the system path os that we consistently use our own
  85. # temporary directory instead.
  86. self.patch_system_path = patch.object(
  87. nbextensions, 'SYSTEM_JUPYTER_PATH', self.system_path
  88. )
  89. self.patch_system_path.start()
  90. self.addCleanup(self.patch_system_path.stop)
  91. def assert_dir_exists(self, path):
  92. if not os.path.exists(path):
  93. do_exist = os.listdir(os.path.dirname(path))
  94. self.fail(u"%s should exist (found %s)" % (path, do_exist))
  95. def assert_not_dir_exists(self, path):
  96. if os.path.exists(path):
  97. self.fail(u"%s should not exist" % path)
  98. def assert_installed(self, relative_path, user=False):
  99. if user:
  100. nbext = pjoin(self.data_dir, u'nbextensions')
  101. else:
  102. nbext = self.system_nbext
  103. self.assert_dir_exists(
  104. pjoin(nbext, relative_path)
  105. )
  106. def assert_not_installed(self, relative_path, user=False):
  107. if user:
  108. nbext = pjoin(self.data_dir, u'nbextensions')
  109. else:
  110. nbext = self.system_nbext
  111. self.assert_not_dir_exists(
  112. pjoin(nbext, relative_path)
  113. )
  114. def test_create_data_dir(self):
  115. """install_nbextension when data_dir doesn't exist"""
  116. with TemporaryDirectory() as td:
  117. data_dir = os.path.join(td, self.data_dir)
  118. with patch.dict('os.environ', {
  119. 'JUPYTER_DATA_DIR': data_dir,
  120. }):
  121. install_nbextension(self.src, user=True)
  122. self.assert_dir_exists(data_dir)
  123. for file_name in self.files:
  124. self.assert_installed(
  125. pjoin(basename(self.src), file_name),
  126. user=True,
  127. )
  128. def test_create_nbextensions_user(self):
  129. with TemporaryDirectory() as td:
  130. install_nbextension(self.src, user=True)
  131. self.assert_installed(
  132. pjoin(basename(self.src), u'ƒile'),
  133. user=True
  134. )
  135. def test_create_nbextensions_system(self):
  136. with TemporaryDirectory() as td:
  137. self.system_nbext = pjoin(td, u'nbextensions')
  138. with patch.object(nbextensions, 'SYSTEM_JUPYTER_PATH', [td]):
  139. install_nbextension(self.src, user=False)
  140. self.assert_installed(
  141. pjoin(basename(self.src), u'ƒile'),
  142. user=False
  143. )
  144. def test_single_file(self):
  145. file_name = self.files[0]
  146. install_nbextension(pjoin(self.src, file_name))
  147. self.assert_installed(file_name)
  148. def test_single_dir(self):
  149. d = u'∂ir'
  150. install_nbextension(pjoin(self.src, d))
  151. self.assert_installed(self.files[-1])
  152. def test_single_dir_trailing_slash(self):
  153. d = u'∂ir/'
  154. install_nbextension(pjoin(self.src, d))
  155. self.assert_installed(self.files[-1])
  156. if os.name == 'nt':
  157. d = u'∂ir\\'
  158. install_nbextension(pjoin(self.src, d))
  159. self.assert_installed(self.files[-1])
  160. def test_destination_file(self):
  161. file_name = self.files[0]
  162. install_nbextension(pjoin(self.src, file_name), destination = u'ƒiledest')
  163. self.assert_installed(u'ƒiledest')
  164. def test_destination_dir(self):
  165. d = u'∂ir'
  166. install_nbextension(pjoin(self.src, d), destination = u'ƒiledest2')
  167. self.assert_installed(pjoin(u'ƒiledest2', u'∂ir2', u'ƒile2'))
  168. def test_install_nbextension(self):
  169. with self.assertRaises(TypeError):
  170. install_nbextension(glob.glob(pjoin(self.src, '*')))
  171. def test_overwrite_file(self):
  172. with TemporaryDirectory() as d:
  173. fname = u'ƒ.js'
  174. src = pjoin(d, fname)
  175. with open(src, 'w') as f:
  176. f.write('first')
  177. mtime = touch(src)
  178. dest = pjoin(self.system_nbext, fname)
  179. install_nbextension(src)
  180. with open(src, 'w') as f:
  181. f.write('overwrite')
  182. mtime = touch(src, mtime - 100)
  183. install_nbextension(src, overwrite=True)
  184. with open(dest) as f:
  185. self.assertEqual(f.read(), 'overwrite')
  186. def test_overwrite_dir(self):
  187. with TemporaryDirectory() as src:
  188. base = basename(src)
  189. fname = u'ƒ.js'
  190. touch(pjoin(src, fname))
  191. install_nbextension(src)
  192. self.assert_installed(pjoin(base, fname))
  193. os.remove(pjoin(src, fname))
  194. fname2 = u'∂.js'
  195. touch(pjoin(src, fname2))
  196. install_nbextension(src, overwrite=True)
  197. self.assert_installed(pjoin(base, fname2))
  198. self.assert_not_installed(pjoin(base, fname))
  199. def test_update_file(self):
  200. with TemporaryDirectory() as d:
  201. fname = u'ƒ.js'
  202. src = pjoin(d, fname)
  203. with open(src, 'w') as f:
  204. f.write('first')
  205. mtime = touch(src)
  206. install_nbextension(src)
  207. self.assert_installed(fname)
  208. dest = pjoin(self.system_nbext, fname)
  209. os.stat(dest).st_mtime
  210. with open(src, 'w') as f:
  211. f.write('overwrite')
  212. touch(src, mtime + 10)
  213. install_nbextension(src)
  214. with open(dest) as f:
  215. self.assertEqual(f.read(), 'overwrite')
  216. def test_skip_old_file(self):
  217. with TemporaryDirectory() as d:
  218. fname = u'ƒ.js'
  219. src = pjoin(d, fname)
  220. mtime = touch(src)
  221. install_nbextension(src)
  222. self.assert_installed(fname)
  223. dest = pjoin(self.system_nbext, fname)
  224. old_mtime = os.stat(dest).st_mtime
  225. mtime = touch(src, mtime - 100)
  226. install_nbextension(src)
  227. new_mtime = os.stat(dest).st_mtime
  228. self.assertEqual(new_mtime, old_mtime)
  229. def test_quiet(self):
  230. stdout = StringIO()
  231. stderr = StringIO()
  232. with patch.object(sys, 'stdout', stdout), \
  233. patch.object(sys, 'stderr', stderr):
  234. install_nbextension(self.src)
  235. self.assertEqual(stdout.getvalue(), '')
  236. self.assertEqual(stderr.getvalue(), '')
  237. def test_install_zip(self):
  238. path = pjoin(self.src, "myjsext.zip")
  239. with zipfile.ZipFile(path, 'w') as f:
  240. f.writestr("a.js", b"b();")
  241. f.writestr("foo/a.js", b"foo();")
  242. install_nbextension(path)
  243. self.assert_installed("a.js")
  244. self.assert_installed(pjoin("foo", "a.js"))
  245. def test_install_tar(self):
  246. def _add_file(f, fname, buf):
  247. info = tarfile.TarInfo(fname)
  248. info.size = len(buf)
  249. f.addfile(info, BytesIO(buf))
  250. for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")):
  251. path = pjoin(self.src, "myjsext" + ext)
  252. with tarfile.open(path, 'w') as f:
  253. _add_file(f, "b%i.js" % i, b"b();")
  254. _add_file(f, "foo/b%i.js" % i, b"foo();")
  255. install_nbextension(path)
  256. self.assert_installed("b%i.js" % i)
  257. self.assert_installed(pjoin("foo", "b%i.js" % i))
  258. def test_install_url(self):
  259. def fake_urlretrieve(url, dest):
  260. touch(dest)
  261. save_urlretrieve = nbextensions.urlretrieve
  262. nbextensions.urlretrieve = fake_urlretrieve
  263. try:
  264. install_nbextension("http://example.com/path/to/foo.js")
  265. self.assert_installed("foo.js")
  266. install_nbextension("https://example.com/path/to/another/bar.js")
  267. self.assert_installed("bar.js")
  268. install_nbextension("https://example.com/path/to/another/bar.js",
  269. destination = 'foobar.js')
  270. self.assert_installed("foobar.js")
  271. finally:
  272. nbextensions.urlretrieve = save_urlretrieve
  273. def test_check_nbextension(self):
  274. with TemporaryDirectory() as d:
  275. f = u'ƒ.js'
  276. src = pjoin(d, f)
  277. touch(src)
  278. install_nbextension(src, user=True)
  279. assert check_nbextension(f, user=True)
  280. assert check_nbextension([f], user=True)
  281. assert not check_nbextension([f, pjoin('dne', f)], user=True)
  282. @dec.skip_win32
  283. def test_install_symlink(self):
  284. with TemporaryDirectory() as d:
  285. f = u'ƒ.js'
  286. src = pjoin(d, f)
  287. touch(src)
  288. install_nbextension(src, symlink=True)
  289. dest = pjoin(self.system_nbext, f)
  290. assert os.path.islink(dest)
  291. link = os.readlink(dest)
  292. self.assertEqual(link, src)
  293. @dec.skip_win32
  294. def test_overwrite_broken_symlink(self):
  295. with TemporaryDirectory() as d:
  296. f = u'ƒ.js'
  297. f2 = u'ƒ2.js'
  298. src = pjoin(d, f)
  299. src2 = pjoin(d, f2)
  300. touch(src)
  301. install_nbextension(src, symlink=True)
  302. os.rename(src, src2)
  303. install_nbextension(src2, symlink=True, overwrite=True, destination=f)
  304. dest = pjoin(self.system_nbext, f)
  305. assert os.path.islink(dest)
  306. link = os.readlink(dest)
  307. self.assertEqual(link, src2)
  308. @dec.skip_win32
  309. def test_install_symlink_destination(self):
  310. with TemporaryDirectory() as d:
  311. f = u'ƒ.js'
  312. flink = u'ƒlink.js'
  313. src = pjoin(d, f)
  314. touch(src)
  315. install_nbextension(src, symlink=True, destination=flink)
  316. dest = pjoin(self.system_nbext, flink)
  317. assert os.path.islink(dest)
  318. link = os.readlink(dest)
  319. self.assertEqual(link, src)
  320. @dec.skip_win32
  321. def test_install_symlink_bad(self):
  322. with self.assertRaises(ValueError):
  323. install_nbextension("http://example.com/foo.js", symlink=True)
  324. with TemporaryDirectory() as d:
  325. zf = u'ƒ.zip'
  326. zsrc = pjoin(d, zf)
  327. with zipfile.ZipFile(zsrc, 'w') as z:
  328. z.writestr("a.js", b"b();")
  329. with self.assertRaises(ValueError):
  330. install_nbextension(zsrc, symlink=True)
  331. def test_install_destination_bad(self):
  332. with TemporaryDirectory() as d:
  333. zf = u'ƒ.zip'
  334. zsrc = pjoin(d, zf)
  335. with zipfile.ZipFile(zsrc, 'w') as z:
  336. z.writestr("a.js", b"b();")
  337. with self.assertRaises(ValueError):
  338. install_nbextension(zsrc, destination='foo')
  339. def test_nbextension_enable(self):
  340. with TemporaryDirectory() as d:
  341. f = u'ƒ.js'
  342. src = pjoin(d, f)
  343. touch(src)
  344. install_nbextension(src, user=True)
  345. enable_nbextension(section='notebook', require=u'ƒ')
  346. config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
  347. cm = BaseJSONConfigManager(config_dir=config_dir)
  348. enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False)
  349. assert enabled
  350. def test_nbextension_disable(self):
  351. self.test_nbextension_enable()
  352. disable_nbextension(section='notebook', require=u'ƒ')
  353. config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
  354. cm = BaseJSONConfigManager(config_dir=config_dir)
  355. enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False)
  356. assert not enabled
  357. def _mock_extension_spec_meta(self, section='notebook'):
  358. return {
  359. 'section': section,
  360. 'src': 'mockextension',
  361. 'dest': '_mockdestination',
  362. 'require': '_mockdestination/index'
  363. }
  364. def _inject_mock_extension(self, section='notebook'):
  365. outer_file = __file__
  366. meta = self._mock_extension_spec_meta(section)
  367. class mock():
  368. __file__ = outer_file
  369. @staticmethod
  370. def _jupyter_nbextension_paths():
  371. return [meta]
  372. import sys
  373. sys.modules['mockextension'] = mock
  374. def test_nbextensionpy_files(self):
  375. self._inject_mock_extension()
  376. install_nbextension_python('mockextension')
  377. assert check_nbextension('_mockdestination/index.js')
  378. assert check_nbextension(['_mockdestination/index.js'])
  379. def test_nbextensionpy_user_files(self):
  380. self._inject_mock_extension()
  381. install_nbextension_python('mockextension', user=True)
  382. assert check_nbextension('_mockdestination/index.js', user=True)
  383. assert check_nbextension(['_mockdestination/index.js'], user=True)
  384. def test_nbextensionpy_uninstall_files(self):
  385. self._inject_mock_extension()
  386. install_nbextension_python('mockextension', user=True)
  387. uninstall_nbextension_python('mockextension', user=True)
  388. assert not check_nbextension('_mockdestination/index.js')
  389. assert not check_nbextension(['_mockdestination/index.js'])
  390. def test_nbextensionpy_enable(self):
  391. self._inject_mock_extension('notebook')
  392. install_nbextension_python('mockextension', user=True)
  393. enable_nbextension_python('mockextension')
  394. config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
  395. cm = BaseJSONConfigManager(config_dir=config_dir)
  396. enabled = cm.get('notebook').get('load_extensions', {}).get('_mockdestination/index', False)
  397. assert enabled
  398. def test_nbextensionpy_disable(self):
  399. self._inject_mock_extension('notebook')
  400. install_nbextension_python('mockextension', user=True)
  401. enable_nbextension_python('mockextension')
  402. disable_nbextension_python('mockextension', user=True)
  403. config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig')
  404. cm = BaseJSONConfigManager(config_dir=config_dir)
  405. enabled = cm.get('notebook').get('load_extensions', {}).get('_mockdestination/index', False)
  406. assert not enabled
  407. def test_nbextensionpy_validate(self):
  408. self._inject_mock_extension('notebook')
  409. paths = install_nbextension_python('mockextension', user=True)
  410. enable_nbextension_python('mockextension')
  411. meta = self._mock_extension_spec_meta()
  412. warnings = validate_nbextension_python(meta, paths[0])
  413. self.assertEqual([], warnings, warnings)
  414. def test_nbextensionpy_validate_bad(self):
  415. # Break the metadata (correct file will still be copied)
  416. self._inject_mock_extension('notebook')
  417. paths = install_nbextension_python('mockextension', user=True)
  418. enable_nbextension_python('mockextension')
  419. meta = self._mock_extension_spec_meta()
  420. meta.update(require="bad-require")
  421. warnings = validate_nbextension_python(meta, paths[0])
  422. self.assertNotEqual([], warnings, warnings)
  423. def test_nbextension_validate(self):
  424. # Break the metadata (correct file will still be copied)
  425. self._inject_mock_extension('notebook')
  426. install_nbextension_python('mockextension', user=True)
  427. enable_nbextension_python('mockextension')
  428. warnings = validate_nbextension("_mockdestination/index")
  429. self.assertEqual([], warnings, warnings)
  430. def test_nbextension_validate_bad(self):
  431. warnings = validate_nbextension("this-doesn't-exist")
  432. self.assertNotEqual([], warnings, warnings)