utils_tests.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. #!/usr/bin/env python
  2. from unittest import mock
  3. import os
  4. import unittest
  5. import tempfile
  6. import shutil
  7. import stat
  8. import datetime
  9. from mkdocs import utils, exceptions
  10. from mkdocs.structure.files import File
  11. from mkdocs.structure.pages import Page
  12. from mkdocs.tests.base import dedent, load_config
  13. class UtilsTests(unittest.TestCase):
  14. def test_html_path(self):
  15. expected_results = {
  16. 'index.md': 'index.html',
  17. 'api-guide.md': 'api-guide/index.html',
  18. 'api-guide/index.md': 'api-guide/index.html',
  19. 'api-guide/testing.md': 'api-guide/testing/index.html',
  20. }
  21. for file_path, expected_html_path in expected_results.items():
  22. html_path = utils.get_html_path(file_path)
  23. self.assertEqual(html_path, expected_html_path)
  24. def test_url_path(self):
  25. expected_results = {
  26. 'index.md': '/',
  27. 'api-guide.md': '/api-guide/',
  28. 'api-guide/index.md': '/api-guide/',
  29. 'api-guide/testing.md': '/api-guide/testing/',
  30. }
  31. for file_path, expected_html_path in expected_results.items():
  32. html_path = utils.get_url_path(file_path)
  33. self.assertEqual(html_path, expected_html_path)
  34. def test_is_markdown_file(self):
  35. expected_results = {
  36. 'index.md': True,
  37. 'index.MARKDOWN': True,
  38. 'index.txt': False,
  39. 'indexmd': False
  40. }
  41. for path, expected_result in expected_results.items():
  42. is_markdown = utils.is_markdown_file(path)
  43. self.assertEqual(is_markdown, expected_result)
  44. def test_is_html_file(self):
  45. expected_results = {
  46. 'index.htm': True,
  47. 'index.HTML': True,
  48. 'index.txt': False,
  49. 'indexhtml': False
  50. }
  51. for path, expected_result in expected_results.items():
  52. is_html = utils.is_html_file(path)
  53. self.assertEqual(is_html, expected_result)
  54. def test_create_media_urls(self):
  55. expected_results = {
  56. 'https://media.cdn.org/jq.js': [
  57. 'https://media.cdn.org/jq.js',
  58. 'https://media.cdn.org/jq.js',
  59. 'https://media.cdn.org/jq.js'
  60. ],
  61. 'http://media.cdn.org/jquery.js': [
  62. 'http://media.cdn.org/jquery.js',
  63. 'http://media.cdn.org/jquery.js',
  64. 'http://media.cdn.org/jquery.js'
  65. ],
  66. '//media.cdn.org/jquery.js': [
  67. '//media.cdn.org/jquery.js',
  68. '//media.cdn.org/jquery.js',
  69. '//media.cdn.org/jquery.js'
  70. ],
  71. 'media.cdn.org/jquery.js': [
  72. 'media.cdn.org/jquery.js',
  73. 'media.cdn.org/jquery.js',
  74. '../media.cdn.org/jquery.js'
  75. ],
  76. 'local/file/jquery.js': [
  77. 'local/file/jquery.js',
  78. 'local/file/jquery.js',
  79. '../local/file/jquery.js'
  80. ],
  81. 'local\\windows\\file\\jquery.js': [
  82. 'local/windows/file/jquery.js',
  83. 'local/windows/file/jquery.js',
  84. '../local/windows/file/jquery.js'
  85. ],
  86. 'image.png': [
  87. 'image.png',
  88. 'image.png',
  89. '../image.png'
  90. ],
  91. 'style.css?v=20180308c': [
  92. 'style.css?v=20180308c',
  93. 'style.css?v=20180308c',
  94. '../style.css?v=20180308c'
  95. ],
  96. '#some_id': [
  97. '#some_id',
  98. '#some_id',
  99. '#some_id'
  100. ]
  101. }
  102. cfg = load_config(use_directory_urls=False)
  103. pages = [
  104. Page('Home', File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
  105. Page('About', File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
  106. Page('FooBar', File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg)
  107. ]
  108. for i, page in enumerate(pages):
  109. urls = utils.create_media_urls(expected_results.keys(), page)
  110. self.assertEqual([v[i] for v in expected_results.values()], urls)
  111. def test_create_media_urls_use_directory_urls(self):
  112. expected_results = {
  113. 'https://media.cdn.org/jq.js': [
  114. 'https://media.cdn.org/jq.js',
  115. 'https://media.cdn.org/jq.js',
  116. 'https://media.cdn.org/jq.js'
  117. ],
  118. 'http://media.cdn.org/jquery.js': [
  119. 'http://media.cdn.org/jquery.js',
  120. 'http://media.cdn.org/jquery.js',
  121. 'http://media.cdn.org/jquery.js'
  122. ],
  123. '//media.cdn.org/jquery.js': [
  124. '//media.cdn.org/jquery.js',
  125. '//media.cdn.org/jquery.js',
  126. '//media.cdn.org/jquery.js'
  127. ],
  128. 'media.cdn.org/jquery.js': [
  129. 'media.cdn.org/jquery.js',
  130. '../media.cdn.org/jquery.js',
  131. '../../media.cdn.org/jquery.js'
  132. ],
  133. 'local/file/jquery.js': [
  134. 'local/file/jquery.js',
  135. '../local/file/jquery.js',
  136. '../../local/file/jquery.js'
  137. ],
  138. 'local\\windows\\file\\jquery.js': [
  139. 'local/windows/file/jquery.js',
  140. '../local/windows/file/jquery.js',
  141. '../../local/windows/file/jquery.js'
  142. ],
  143. 'image.png': [
  144. 'image.png',
  145. '../image.png',
  146. '../../image.png'
  147. ],
  148. 'style.css?v=20180308c': [
  149. 'style.css?v=20180308c',
  150. '../style.css?v=20180308c',
  151. '../../style.css?v=20180308c'
  152. ],
  153. '#some_id': [
  154. '#some_id',
  155. '#some_id',
  156. '#some_id'
  157. ]
  158. }
  159. cfg = load_config(use_directory_urls=True)
  160. pages = [
  161. Page('Home', File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
  162. Page('About', File('about.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg),
  163. Page('FooBar', File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), cfg)
  164. ]
  165. for i, page in enumerate(pages):
  166. urls = utils.create_media_urls(expected_results.keys(), page)
  167. self.assertEqual([v[i] for v in expected_results.values()], urls)
  168. def test_reduce_list(self):
  169. self.assertEqual(
  170. utils.reduce_list([1, 2, 3, 4, 5, 5, 2, 4, 6, 7, 8]),
  171. [1, 2, 3, 4, 5, 6, 7, 8]
  172. )
  173. def test_get_themes(self):
  174. self.assertEqual(
  175. sorted(utils.get_theme_names()),
  176. ['mkdocs', 'readthedocs'])
  177. @mock.patch('pkg_resources.iter_entry_points', autospec=True)
  178. def test_get_theme_dir(self, mock_iter):
  179. path = 'some/path'
  180. theme = mock.Mock()
  181. theme.name = 'mkdocs2'
  182. theme.dist.key = 'mkdocs2'
  183. theme.load().__file__ = os.path.join(path, '__init__.py')
  184. mock_iter.return_value = iter([theme])
  185. self.assertEqual(utils.get_theme_dir(theme.name), os.path.abspath(path))
  186. def test_get_theme_dir_keyerror(self):
  187. self.assertRaises(KeyError, utils.get_theme_dir, 'nonexistanttheme')
  188. @mock.patch('pkg_resources.iter_entry_points', autospec=True)
  189. def test_get_theme_dir_importerror(self, mock_iter):
  190. theme = mock.Mock()
  191. theme.name = 'mkdocs2'
  192. theme.dist.key = 'mkdocs2'
  193. theme.load.side_effect = ImportError()
  194. mock_iter.return_value = iter([theme])
  195. self.assertRaises(ImportError, utils.get_theme_dir, theme.name)
  196. @mock.patch('pkg_resources.iter_entry_points', autospec=True)
  197. def test_get_themes_warning(self, mock_iter):
  198. theme1 = mock.Mock()
  199. theme1.name = 'mkdocs2'
  200. theme1.dist.key = 'mkdocs2'
  201. theme1.load().__file__ = "some/path1"
  202. theme2 = mock.Mock()
  203. theme2.name = 'mkdocs2'
  204. theme2.dist.key = 'mkdocs3'
  205. theme2.load().__file__ = "some/path2"
  206. mock_iter.return_value = iter([theme1, theme2])
  207. self.assertEqual(
  208. sorted(utils.get_theme_names()),
  209. sorted(['mkdocs2', ]))
  210. @mock.patch('pkg_resources.iter_entry_points', autospec=True)
  211. @mock.patch('pkg_resources.get_entry_map', autospec=True)
  212. def test_get_themes_error(self, mock_get, mock_iter):
  213. theme1 = mock.Mock()
  214. theme1.name = 'mkdocs'
  215. theme1.dist.key = 'mkdocs'
  216. theme1.load().__file__ = "some/path1"
  217. theme2 = mock.Mock()
  218. theme2.name = 'mkdocs'
  219. theme2.dist.key = 'mkdocs2'
  220. theme2.load().__file__ = "some/path2"
  221. mock_iter.return_value = iter([theme1, theme2])
  222. mock_get.return_value = {'mkdocs': theme1, }
  223. self.assertRaises(exceptions.ConfigurationError, utils.get_theme_names)
  224. def test_nest_paths(self):
  225. j = os.path.join
  226. result = utils.nest_paths([
  227. 'index.md',
  228. j('user-guide', 'configuration.md'),
  229. j('user-guide', 'styling-your-docs.md'),
  230. j('user-guide', 'writing-your-docs.md'),
  231. j('about', 'contributing.md'),
  232. j('about', 'license.md'),
  233. j('about', 'release-notes.md'),
  234. ])
  235. self.assertEqual(
  236. result,
  237. [
  238. 'index.md',
  239. {'User guide': [
  240. j('user-guide', 'configuration.md'),
  241. j('user-guide', 'styling-your-docs.md'),
  242. j('user-guide', 'writing-your-docs.md')]},
  243. {'About': [
  244. j('about', 'contributing.md'),
  245. j('about', 'license.md'),
  246. j('about', 'release-notes.md')]}
  247. ]
  248. )
  249. def test_unicode_yaml(self):
  250. yaml_src = dedent(
  251. '''
  252. key: value
  253. key2:
  254. - value
  255. '''
  256. )
  257. config = utils.yaml_load(yaml_src)
  258. self.assertTrue(isinstance(config['key'], str))
  259. self.assertTrue(isinstance(config['key2'][0], str))
  260. def test_copy_files(self):
  261. src_paths = [
  262. 'foo.txt',
  263. 'bar.txt',
  264. 'baz.txt',
  265. ]
  266. dst_paths = [
  267. 'foo.txt',
  268. 'foo/', # ensure src filename is appended
  269. 'foo/bar/baz.txt' # ensure missing dirs are created
  270. ]
  271. expected = [
  272. 'foo.txt',
  273. 'foo/bar.txt',
  274. 'foo/bar/baz.txt',
  275. ]
  276. src_dir = tempfile.mkdtemp()
  277. dst_dir = tempfile.mkdtemp()
  278. try:
  279. for i, src in enumerate(src_paths):
  280. src = os.path.join(src_dir, src)
  281. with open(src, 'w') as f:
  282. f.write('content')
  283. dst = os.path.join(dst_dir, dst_paths[i])
  284. utils.copy_file(src, dst)
  285. self.assertTrue(os.path.isfile(os.path.join(dst_dir, expected[i])))
  286. finally:
  287. shutil.rmtree(src_dir)
  288. shutil.rmtree(dst_dir)
  289. def test_copy_files_without_permissions(self):
  290. src_paths = [
  291. 'foo.txt',
  292. 'bar.txt',
  293. 'baz.txt',
  294. ]
  295. expected = [
  296. 'foo.txt',
  297. 'bar.txt',
  298. 'baz.txt',
  299. ]
  300. src_dir = tempfile.mkdtemp()
  301. dst_dir = tempfile.mkdtemp()
  302. try:
  303. for i, src in enumerate(src_paths):
  304. src = os.path.join(src_dir, src)
  305. with open(src, 'w') as f:
  306. f.write('content')
  307. # Set src file to read-only
  308. os.chmod(src, stat.S_IRUSR)
  309. utils.copy_file(src, dst_dir)
  310. self.assertTrue(os.path.isfile(os.path.join(dst_dir, expected[i])))
  311. self.assertNotEqual(os.stat(src).st_mode, os.stat(os.path.join(dst_dir, expected[i])).st_mode)
  312. # While src was read-only, dst must remain writable
  313. self.assertTrue(os.access(os.path.join(dst_dir, expected[i]), os.W_OK))
  314. finally:
  315. for src in src_paths:
  316. # Undo read-only so we can delete temp files
  317. src = os.path.join(src_dir, src)
  318. if os.path.exists(src):
  319. os.chmod(src, stat.S_IRUSR | stat.S_IWUSR)
  320. shutil.rmtree(src_dir)
  321. shutil.rmtree(dst_dir)
  322. def test_mm_meta_data(self):
  323. doc = dedent(
  324. """
  325. Title: Foo Bar
  326. Date: 2018-07-10
  327. Summary: Line one
  328. Line two
  329. Tags: foo
  330. Tags: bar
  331. Doc body
  332. """
  333. )
  334. self.assertEqual(
  335. utils.meta.get_data(doc),
  336. (
  337. "Doc body",
  338. {
  339. 'title': 'Foo Bar',
  340. 'date': '2018-07-10',
  341. 'summary': 'Line one Line two',
  342. 'tags': 'foo bar'
  343. }
  344. )
  345. )
  346. def test_mm_meta_data_blank_first_line(self):
  347. doc = '\nfoo: bar\nDoc body'
  348. self.assertEqual(utils.meta.get_data(doc), (doc.lstrip(), {}))
  349. def test_yaml_meta_data(self):
  350. doc = dedent(
  351. """
  352. ---
  353. Title: Foo Bar
  354. Date: 2018-07-10
  355. Summary: Line one
  356. Line two
  357. Tags:
  358. - foo
  359. - bar
  360. ---
  361. Doc body
  362. """
  363. )
  364. self.assertEqual(
  365. utils.meta.get_data(doc),
  366. (
  367. "Doc body",
  368. {
  369. 'Title': 'Foo Bar',
  370. 'Date': datetime.date(2018, 7, 10),
  371. 'Summary': 'Line one Line two',
  372. 'Tags': ['foo', 'bar']
  373. }
  374. )
  375. )
  376. def test_yaml_meta_data_not_dict(self):
  377. doc = dedent(
  378. """
  379. ---
  380. - List item
  381. ---
  382. Doc body
  383. """
  384. )
  385. self.assertEqual(utils.meta.get_data(doc), (doc, {}))
  386. def test_yaml_meta_data_invalid(self):
  387. doc = dedent(
  388. """
  389. ---
  390. foo: bar: baz
  391. ---
  392. Doc body
  393. """
  394. )
  395. self.assertEqual(utils.meta.get_data(doc), (doc, {}))
  396. def test_no_meta_data(self):
  397. doc = dedent(
  398. """
  399. Doc body
  400. """
  401. )
  402. self.assertEqual(utils.meta.get_data(doc), (doc, {}))