m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1495 lines
67 KiB

6 months ago
  1. #!/usr/bin/env python
  2. # vim: fileencoding=utf-8 :
  3. # Tests for the `humanfriendly' package.
  4. #
  5. # Author: Peter Odding <peter.odding@paylogic.eu>
  6. # Last Change: June 11, 2021
  7. # URL: https://humanfriendly.readthedocs.io
  8. """Test suite for the `humanfriendly` package."""
  9. # Standard library modules.
  10. import datetime
  11. import math
  12. import os
  13. import random
  14. import re
  15. import subprocess
  16. import sys
  17. import time
  18. import types
  19. import unittest
  20. import warnings
  21. # Modules included in our package.
  22. from humanfriendly import (
  23. InvalidDate,
  24. InvalidLength,
  25. InvalidSize,
  26. InvalidTimespan,
  27. Timer,
  28. coerce_boolean,
  29. coerce_pattern,
  30. format_length,
  31. format_number,
  32. format_path,
  33. format_size,
  34. format_timespan,
  35. parse_date,
  36. parse_length,
  37. parse_path,
  38. parse_size,
  39. parse_timespan,
  40. prompts,
  41. round_number,
  42. )
  43. from humanfriendly.case import CaseInsensitiveDict, CaseInsensitiveKey
  44. from humanfriendly.cli import main
  45. from humanfriendly.compat import StringIO
  46. from humanfriendly.decorators import cached
  47. from humanfriendly.deprecation import DeprecationProxy, define_aliases, deprecated_args, get_aliases
  48. from humanfriendly.prompts import (
  49. TooManyInvalidReplies,
  50. prompt_for_confirmation,
  51. prompt_for_choice,
  52. prompt_for_input,
  53. )
  54. from humanfriendly.sphinx import (
  55. deprecation_note_callback,
  56. man_role,
  57. pypi_role,
  58. setup,
  59. special_methods_callback,
  60. usage_message_callback,
  61. )
  62. from humanfriendly.tables import (
  63. format_pretty_table,
  64. format_robust_table,
  65. format_rst_table,
  66. format_smart_table,
  67. )
  68. from humanfriendly.terminal import (
  69. ANSI_CSI,
  70. ANSI_ERASE_LINE,
  71. ANSI_HIDE_CURSOR,
  72. ANSI_RESET,
  73. ANSI_SGR,
  74. ANSI_SHOW_CURSOR,
  75. ansi_strip,
  76. ansi_style,
  77. ansi_width,
  78. ansi_wrap,
  79. clean_terminal_output,
  80. connected_to_terminal,
  81. find_terminal_size,
  82. get_pager_command,
  83. message,
  84. output,
  85. show_pager,
  86. terminal_supports_colors,
  87. warning,
  88. )
  89. from humanfriendly.terminal.html import html_to_ansi
  90. from humanfriendly.terminal.spinners import AutomaticSpinner, Spinner
  91. from humanfriendly.testing import (
  92. CallableTimedOut,
  93. CaptureOutput,
  94. MockedProgram,
  95. PatchedAttribute,
  96. PatchedItem,
  97. TemporaryDirectory,
  98. TestCase,
  99. retry,
  100. run_cli,
  101. skip_on_raise,
  102. touch,
  103. )
  104. from humanfriendly.text import (
  105. compact,
  106. compact_empty_lines,
  107. concatenate,
  108. dedent,
  109. generate_slug,
  110. pluralize,
  111. random_string,
  112. trim_empty_lines,
  113. )
  114. from humanfriendly.usage import (
  115. find_meta_variables,
  116. format_usage,
  117. parse_usage,
  118. render_usage,
  119. )
  120. # Test dependencies.
  121. from mock import MagicMock
  122. class HumanFriendlyTestCase(TestCase):
  123. """Container for the `humanfriendly` test suite."""
  124. def test_case_insensitive_dict(self):
  125. """Test the CaseInsensitiveDict class."""
  126. # Test support for the dict(iterable) signature.
  127. assert len(CaseInsensitiveDict([('key', True), ('KEY', False)])) == 1
  128. # Test support for the dict(iterable, **kw) signature.
  129. assert len(CaseInsensitiveDict([('one', True), ('ONE', False)], one=False, two=True)) == 2
  130. # Test support for the dict(mapping) signature.
  131. assert len(CaseInsensitiveDict(dict(key=True, KEY=False))) == 1
  132. # Test support for the dict(mapping, **kw) signature.
  133. assert len(CaseInsensitiveDict(dict(one=True, ONE=False), one=False, two=True)) == 2
  134. # Test support for the dict(**kw) signature.
  135. assert len(CaseInsensitiveDict(one=True, ONE=False, two=True)) == 2
  136. # Test support for dict.fromkeys().
  137. obj = CaseInsensitiveDict.fromkeys(["One", "one", "ONE", "Two", "two", "TWO"])
  138. assert len(obj) == 2
  139. # Test support for dict.get().
  140. obj = CaseInsensitiveDict(existing_key=42)
  141. assert obj.get('Existing_Key') == 42
  142. # Test support for dict.pop().
  143. obj = CaseInsensitiveDict(existing_key=42)
  144. assert obj.pop('Existing_Key') == 42
  145. assert len(obj) == 0
  146. # Test support for dict.setdefault().
  147. obj = CaseInsensitiveDict(existing_key=42)
  148. assert obj.setdefault('Existing_Key') == 42
  149. obj.setdefault('other_key', 11)
  150. assert obj['Other_Key'] == 11
  151. # Test support for dict.__contains__().
  152. obj = CaseInsensitiveDict(existing_key=42)
  153. assert 'Existing_Key' in obj
  154. # Test support for dict.__delitem__().
  155. obj = CaseInsensitiveDict(existing_key=42)
  156. del obj['Existing_Key']
  157. assert len(obj) == 0
  158. # Test support for dict.__getitem__().
  159. obj = CaseInsensitiveDict(existing_key=42)
  160. assert obj['Existing_Key'] == 42
  161. # Test support for dict.__setitem__().
  162. obj = CaseInsensitiveDict(existing_key=42)
  163. obj['Existing_Key'] = 11
  164. assert obj['existing_key'] == 11
  165. def test_case_insensitive_key(self):
  166. """Test the CaseInsensitiveKey class."""
  167. # Test the __eq__() special method.
  168. polite = CaseInsensitiveKey("Please don't shout")
  169. rude = CaseInsensitiveKey("PLEASE DON'T SHOUT")
  170. assert polite == rude
  171. # Test the __hash__() special method.
  172. mapping = {}
  173. mapping[polite] = 1
  174. mapping[rude] = 2
  175. assert len(mapping) == 1
  176. def test_capture_output(self):
  177. """Test the CaptureOutput class."""
  178. with CaptureOutput() as capturer:
  179. sys.stdout.write("Something for stdout.\n")
  180. sys.stderr.write("And for stderr.\n")
  181. assert capturer.stdout.get_lines() == ["Something for stdout."]
  182. assert capturer.stderr.get_lines() == ["And for stderr."]
  183. def test_skip_on_raise(self):
  184. """Test the skip_on_raise() decorator."""
  185. def test_fn():
  186. raise NotImplementedError()
  187. decorator_fn = skip_on_raise(NotImplementedError)
  188. decorated_fn = decorator_fn(test_fn)
  189. self.assertRaises(NotImplementedError, test_fn)
  190. self.assertRaises(unittest.SkipTest, decorated_fn)
  191. def test_retry_raise(self):
  192. """Test :func:`~humanfriendly.testing.retry()` based on assertion errors."""
  193. # Define a helper function that will raise an assertion error on the
  194. # first call and return a string on the second call.
  195. def success_helper():
  196. if not hasattr(success_helper, 'was_called'):
  197. setattr(success_helper, 'was_called', True)
  198. assert False
  199. else:
  200. return 'yes'
  201. assert retry(success_helper) == 'yes'
  202. # Define a helper function that always raises an assertion error.
  203. def failure_helper():
  204. assert False
  205. with self.assertRaises(AssertionError):
  206. retry(failure_helper, timeout=1)
  207. def test_retry_return(self):
  208. """Test :func:`~humanfriendly.testing.retry()` based on return values."""
  209. # Define a helper function that will return False on the first call and
  210. # return a number on the second call.
  211. def success_helper():
  212. if not hasattr(success_helper, 'was_called'):
  213. # On the first call we return False.
  214. setattr(success_helper, 'was_called', True)
  215. return False
  216. else:
  217. # On the second call we return a number.
  218. return 42
  219. assert retry(success_helper) == 42
  220. with self.assertRaises(CallableTimedOut):
  221. retry(lambda: False, timeout=1)
  222. def test_mocked_program(self):
  223. """Test :class:`humanfriendly.testing.MockedProgram`."""
  224. name = random_string()
  225. script = dedent('''
  226. # This goes to stdout.
  227. tr a-z A-Z
  228. # This goes to stderr.
  229. echo Fake warning >&2
  230. ''')
  231. with MockedProgram(name=name, returncode=42, script=script) as directory:
  232. assert os.path.isdir(directory)
  233. assert os.path.isfile(os.path.join(directory, name))
  234. program = subprocess.Popen(name, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  235. stdout, stderr = program.communicate(input=b'hello world\n')
  236. assert program.returncode == 42
  237. assert stdout == b'HELLO WORLD\n'
  238. assert stderr == b'Fake warning\n'
  239. def test_temporary_directory(self):
  240. """Test :class:`humanfriendly.testing.TemporaryDirectory`."""
  241. with TemporaryDirectory() as directory:
  242. assert os.path.isdir(directory)
  243. temporary_file = os.path.join(directory, 'some-file')
  244. with open(temporary_file, 'w') as handle:
  245. handle.write("Hello world!")
  246. assert not os.path.exists(temporary_file)
  247. assert not os.path.exists(directory)
  248. def test_touch(self):
  249. """Test :func:`humanfriendly.testing.touch()`."""
  250. with TemporaryDirectory() as directory:
  251. # Create a file in the temporary directory.
  252. filename = os.path.join(directory, random_string())
  253. assert not os.path.isfile(filename)
  254. touch(filename)
  255. assert os.path.isfile(filename)
  256. # Create a file in a subdirectory.
  257. filename = os.path.join(directory, random_string(), random_string())
  258. assert not os.path.isfile(filename)
  259. touch(filename)
  260. assert os.path.isfile(filename)
  261. def test_patch_attribute(self):
  262. """Test :class:`humanfriendly.testing.PatchedAttribute`."""
  263. class Subject(object):
  264. my_attribute = 42
  265. instance = Subject()
  266. assert instance.my_attribute == 42
  267. with PatchedAttribute(instance, 'my_attribute', 13) as return_value:
  268. assert return_value is instance
  269. assert instance.my_attribute == 13
  270. assert instance.my_attribute == 42
  271. def test_patch_item(self):
  272. """Test :class:`humanfriendly.testing.PatchedItem`."""
  273. instance = dict(my_item=True)
  274. assert instance['my_item'] is True
  275. with PatchedItem(instance, 'my_item', False) as return_value:
  276. assert return_value is instance
  277. assert instance['my_item'] is False
  278. assert instance['my_item'] is True
  279. def test_run_cli_intercepts_exit(self):
  280. """Test that run_cli() intercepts SystemExit."""
  281. returncode, output = run_cli(lambda: sys.exit(42))
  282. self.assertEqual(returncode, 42)
  283. def test_run_cli_intercepts_error(self):
  284. """Test that run_cli() intercepts exceptions."""
  285. returncode, output = run_cli(self.run_cli_raise_other)
  286. self.assertEqual(returncode, 1)
  287. def run_cli_raise_other(self):
  288. """run_cli() sample that raises an exception."""
  289. raise ValueError()
  290. def test_run_cli_intercepts_output(self):
  291. """Test that run_cli() intercepts output."""
  292. expected_output = random_string() + "\n"
  293. returncode, output = run_cli(lambda: sys.stdout.write(expected_output))
  294. self.assertEqual(returncode, 0)
  295. self.assertEqual(output, expected_output)
  296. def test_caching_decorator(self):
  297. """Test the caching decorator."""
  298. # Confirm that the caching decorator works.
  299. a = cached(lambda: random.random())
  300. b = cached(lambda: random.random())
  301. assert a() == a()
  302. assert b() == b()
  303. # Confirm that functions have their own cache.
  304. assert a() != b()
  305. def test_compact(self):
  306. """Test :func:`humanfriendly.text.compact()`."""
  307. assert compact(' a \n\n b ') == 'a b'
  308. assert compact('''
  309. %s template notation
  310. ''', 'Simple') == 'Simple template notation'
  311. assert compact('''
  312. More {type} template notation
  313. ''', type='readable') == 'More readable template notation'
  314. def test_compact_empty_lines(self):
  315. """Test :func:`humanfriendly.text.compact_empty_lines()`."""
  316. # Simple strings pass through untouched.
  317. assert compact_empty_lines('foo') == 'foo'
  318. # Horizontal whitespace remains untouched.
  319. assert compact_empty_lines('\tfoo') == '\tfoo'
  320. # Line breaks should be preserved.
  321. assert compact_empty_lines('foo\nbar') == 'foo\nbar'
  322. # Vertical whitespace should be preserved.
  323. assert compact_empty_lines('foo\n\nbar') == 'foo\n\nbar'
  324. # Vertical whitespace should be compressed.
  325. assert compact_empty_lines('foo\n\n\nbar') == 'foo\n\nbar'
  326. assert compact_empty_lines('foo\n\n\n\nbar') == 'foo\n\nbar'
  327. assert compact_empty_lines('foo\n\n\n\n\nbar') == 'foo\n\nbar'
  328. def test_dedent(self):
  329. """Test :func:`humanfriendly.text.dedent()`."""
  330. assert dedent('\n line 1\n line 2\n\n') == 'line 1\n line 2\n'
  331. assert dedent('''
  332. Dedented, %s text
  333. ''', 'interpolated') == 'Dedented, interpolated text\n'
  334. assert dedent('''
  335. Dedented, {op} text
  336. ''', op='formatted') == 'Dedented, formatted text\n'
  337. def test_pluralization(self):
  338. """Test :func:`humanfriendly.text.pluralize()`."""
  339. assert pluralize(1, 'word') == '1 word'
  340. assert pluralize(2, 'word') == '2 words'
  341. assert pluralize(1, 'box', 'boxes') == '1 box'
  342. assert pluralize(2, 'box', 'boxes') == '2 boxes'
  343. def test_generate_slug(self):
  344. """Test :func:`humanfriendly.text.generate_slug()`."""
  345. # Test the basic functionality.
  346. self.assertEqual('some-random-text', generate_slug('Some Random Text!'))
  347. # Test that previous output doesn't change.
  348. self.assertEqual('some-random-text', generate_slug('some-random-text'))
  349. # Test that inputs which can't be converted to a slug raise an exception.
  350. with self.assertRaises(ValueError):
  351. generate_slug(' ')
  352. with self.assertRaises(ValueError):
  353. generate_slug('-')
  354. def test_boolean_coercion(self):
  355. """Test :func:`humanfriendly.coerce_boolean()`."""
  356. for value in [True, 'TRUE', 'True', 'true', 'on', 'yes', '1']:
  357. self.assertEqual(True, coerce_boolean(value))
  358. for value in [False, 'FALSE', 'False', 'false', 'off', 'no', '0']:
  359. self.assertEqual(False, coerce_boolean(value))
  360. with self.assertRaises(ValueError):
  361. coerce_boolean('not a boolean')
  362. def test_pattern_coercion(self):
  363. """Test :func:`humanfriendly.coerce_pattern()`."""
  364. empty_pattern = re.compile('')
  365. # Make sure strings are converted to compiled regular expressions.
  366. assert isinstance(coerce_pattern('foobar'), type(empty_pattern))
  367. # Make sure compiled regular expressions pass through untouched.
  368. assert empty_pattern is coerce_pattern(empty_pattern)
  369. # Make sure flags are respected.
  370. pattern = coerce_pattern('foobar', re.IGNORECASE)
  371. assert pattern.match('FOOBAR')
  372. # Make sure invalid values raise the expected exception.
  373. with self.assertRaises(ValueError):
  374. coerce_pattern([])
  375. def test_format_timespan(self):
  376. """Test :func:`humanfriendly.format_timespan()`."""
  377. minute = 60
  378. hour = minute * 60
  379. day = hour * 24
  380. week = day * 7
  381. year = week * 52
  382. assert '1 nanosecond' == format_timespan(0.000000001, detailed=True)
  383. assert '500 nanoseconds' == format_timespan(0.0000005, detailed=True)
  384. assert '1 microsecond' == format_timespan(0.000001, detailed=True)
  385. assert '500 microseconds' == format_timespan(0.0005, detailed=True)
  386. assert '1 millisecond' == format_timespan(0.001, detailed=True)
  387. assert '500 milliseconds' == format_timespan(0.5, detailed=True)
  388. assert '0.5 seconds' == format_timespan(0.5, detailed=False)
  389. assert '0 seconds' == format_timespan(0)
  390. assert '0.54 seconds' == format_timespan(0.54321)
  391. assert '1 second' == format_timespan(1)
  392. assert '3.14 seconds' == format_timespan(math.pi)
  393. assert '1 minute' == format_timespan(minute)
  394. assert '1 minute and 20 seconds' == format_timespan(80)
  395. assert '2 minutes' == format_timespan(minute * 2)
  396. assert '1 hour' == format_timespan(hour)
  397. assert '2 hours' == format_timespan(hour * 2)
  398. assert '1 day' == format_timespan(day)
  399. assert '2 days' == format_timespan(day * 2)
  400. assert '1 week' == format_timespan(week)
  401. assert '2 weeks' == format_timespan(week * 2)
  402. assert '1 year' == format_timespan(year)
  403. assert '2 years' == format_timespan(year * 2)
  404. assert '6 years, 5 weeks, 4 days, 3 hours, 2 minutes and 500 milliseconds' == \
  405. format_timespan(year * 6 + week * 5 + day * 4 + hour * 3 + minute * 2 + 0.5, detailed=True)
  406. assert '1 year, 2 weeks and 3 days' == \
  407. format_timespan(year + week * 2 + day * 3 + hour * 12)
  408. # Make sure milliseconds are never shown separately when detailed=False.
  409. # https://github.com/xolox/python-humanfriendly/issues/10
  410. assert '1 minute, 1 second and 100 milliseconds' == format_timespan(61.10, detailed=True)
  411. assert '1 minute and 1.1 seconds' == format_timespan(61.10, detailed=False)
  412. # Test for loss of precision as reported in issue 11:
  413. # https://github.com/xolox/python-humanfriendly/issues/11
  414. assert '1 minute and 0.3 seconds' == format_timespan(60.300)
  415. assert '5 minutes and 0.3 seconds' == format_timespan(300.300)
  416. assert '1 second and 15 milliseconds' == format_timespan(1.015, detailed=True)
  417. assert '10 seconds and 15 milliseconds' == format_timespan(10.015, detailed=True)
  418. assert '1 microsecond and 50 nanoseconds' == format_timespan(0.00000105, detailed=True)
  419. # Test the datetime.timedelta support:
  420. # https://github.com/xolox/python-humanfriendly/issues/27
  421. now = datetime.datetime.now()
  422. then = now - datetime.timedelta(hours=23)
  423. assert '23 hours' == format_timespan(now - then)
  424. def test_parse_timespan(self):
  425. """Test :func:`humanfriendly.parse_timespan()`."""
  426. self.assertEqual(0, parse_timespan('0'))
  427. self.assertEqual(0, parse_timespan('0s'))
  428. self.assertEqual(0.000000001, parse_timespan('1ns'))
  429. self.assertEqual(0.000000051, parse_timespan('51ns'))
  430. self.assertEqual(0.000001, parse_timespan('1us'))
  431. self.assertEqual(0.000052, parse_timespan('52us'))
  432. self.assertEqual(0.001, parse_timespan('1ms'))
  433. self.assertEqual(0.001, parse_timespan('1 millisecond'))
  434. self.assertEqual(0.5, parse_timespan('500 milliseconds'))
  435. self.assertEqual(0.5, parse_timespan('0.5 seconds'))
  436. self.assertEqual(5, parse_timespan('5s'))
  437. self.assertEqual(5, parse_timespan('5 seconds'))
  438. self.assertEqual(60 * 2, parse_timespan('2m'))
  439. self.assertEqual(60 * 2, parse_timespan('2 minutes'))
  440. self.assertEqual(60 * 3, parse_timespan('3 min'))
  441. self.assertEqual(60 * 3, parse_timespan('3 mins'))
  442. self.assertEqual(60 * 60 * 3, parse_timespan('3 h'))
  443. self.assertEqual(60 * 60 * 3, parse_timespan('3 hours'))
  444. self.assertEqual(60 * 60 * 24 * 4, parse_timespan('4d'))
  445. self.assertEqual(60 * 60 * 24 * 4, parse_timespan('4 days'))
  446. self.assertEqual(60 * 60 * 24 * 7 * 5, parse_timespan('5 w'))
  447. self.assertEqual(60 * 60 * 24 * 7 * 5, parse_timespan('5 weeks'))
  448. with self.assertRaises(InvalidTimespan):
  449. parse_timespan('1z')
  450. def test_parse_date(self):
  451. """Test :func:`humanfriendly.parse_date()`."""
  452. self.assertEqual((2013, 6, 17, 0, 0, 0), parse_date('2013-06-17'))
  453. self.assertEqual((2013, 6, 17, 2, 47, 42), parse_date('2013-06-17 02:47:42'))
  454. self.assertEqual((2016, 11, 30, 0, 47, 17), parse_date(u'2016-11-30 00:47:17'))
  455. with self.assertRaises(InvalidDate):
  456. parse_date('2013-06-XY')
  457. def test_format_size(self):
  458. """Test :func:`humanfriendly.format_size()`."""
  459. self.assertEqual('0 bytes', format_size(0))
  460. self.assertEqual('1 byte', format_size(1))
  461. self.assertEqual('42 bytes', format_size(42))
  462. self.assertEqual('1 KB', format_size(1000 ** 1))
  463. self.assertEqual('1 MB', format_size(1000 ** 2))
  464. self.assertEqual('1 GB', format_size(1000 ** 3))
  465. self.assertEqual('1 TB', format_size(1000 ** 4))
  466. self.assertEqual('1 PB', format_size(1000 ** 5))
  467. self.assertEqual('1 EB', format_size(1000 ** 6))
  468. self.assertEqual('1 ZB', format_size(1000 ** 7))
  469. self.assertEqual('1 YB', format_size(1000 ** 8))
  470. self.assertEqual('1 KiB', format_size(1024 ** 1, binary=True))
  471. self.assertEqual('1 MiB', format_size(1024 ** 2, binary=True))
  472. self.assertEqual('1 GiB', format_size(1024 ** 3, binary=True))
  473. self.assertEqual('1 TiB', format_size(1024 ** 4, binary=True))
  474. self.assertEqual('1 PiB', format_size(1024 ** 5, binary=True))
  475. self.assertEqual('1 EiB', format_size(1024 ** 6, binary=True))
  476. self.assertEqual('1 ZiB', format_size(1024 ** 7, binary=True))
  477. self.assertEqual('1 YiB', format_size(1024 ** 8, binary=True))
  478. self.assertEqual('45 KB', format_size(1000 * 45))
  479. self.assertEqual('2.9 TB', format_size(1000 ** 4 * 2.9))
  480. def test_parse_size(self):
  481. """Test :func:`humanfriendly.parse_size()`."""
  482. self.assertEqual(0, parse_size('0B'))
  483. self.assertEqual(42, parse_size('42'))
  484. self.assertEqual(42, parse_size('42B'))
  485. self.assertEqual(1000, parse_size('1k'))
  486. self.assertEqual(1024, parse_size('1k', binary=True))
  487. self.assertEqual(1000, parse_size('1 KB'))
  488. self.assertEqual(1000, parse_size('1 kilobyte'))
  489. self.assertEqual(1024, parse_size('1 kilobyte', binary=True))
  490. self.assertEqual(1000 ** 2 * 69, parse_size('69 MB'))
  491. self.assertEqual(1000 ** 3, parse_size('1 GB'))
  492. self.assertEqual(1000 ** 4, parse_size('1 TB'))
  493. self.assertEqual(1000 ** 5, parse_size('1 PB'))
  494. self.assertEqual(1000 ** 6, parse_size('1 EB'))
  495. self.assertEqual(1000 ** 7, parse_size('1 ZB'))
  496. self.assertEqual(1000 ** 8, parse_size('1 YB'))
  497. self.assertEqual(1000 ** 3 * 1.5, parse_size('1.5 GB'))
  498. self.assertEqual(1024 ** 8 * 1.5, parse_size('1.5 YiB'))
  499. with self.assertRaises(InvalidSize):
  500. parse_size('1q')
  501. with self.assertRaises(InvalidSize):
  502. parse_size('a')
  503. def test_format_length(self):
  504. """Test :func:`humanfriendly.format_length()`."""
  505. self.assertEqual('0 metres', format_length(0))
  506. self.assertEqual('1 metre', format_length(1))
  507. self.assertEqual('42 metres', format_length(42))
  508. self.assertEqual('1 km', format_length(1 * 1000))
  509. self.assertEqual('15.3 cm', format_length(0.153))
  510. self.assertEqual('1 cm', format_length(1e-02))
  511. self.assertEqual('1 mm', format_length(1e-03))
  512. self.assertEqual('1 nm', format_length(1e-09))
  513. def test_parse_length(self):
  514. """Test :func:`humanfriendly.parse_length()`."""
  515. self.assertEqual(0, parse_length('0m'))
  516. self.assertEqual(42, parse_length('42'))
  517. self.assertEqual(1.5, parse_length('1.5'))
  518. self.assertEqual(42, parse_length('42m'))
  519. self.assertEqual(1000, parse_length('1km'))
  520. self.assertEqual(0.153, parse_length('15.3 cm'))
  521. self.assertEqual(1e-02, parse_length('1cm'))
  522. self.assertEqual(1e-03, parse_length('1mm'))
  523. self.assertEqual(1e-09, parse_length('1nm'))
  524. with self.assertRaises(InvalidLength):
  525. parse_length('1z')
  526. with self.assertRaises(InvalidLength):
  527. parse_length('a')
  528. def test_format_number(self):
  529. """Test :func:`humanfriendly.format_number()`."""
  530. self.assertEqual('1', format_number(1))
  531. self.assertEqual('1.5', format_number(1.5))
  532. self.assertEqual('1.56', format_number(1.56789))
  533. self.assertEqual('1.567', format_number(1.56789, 3))
  534. self.assertEqual('1,000', format_number(1000))
  535. self.assertEqual('1,000', format_number(1000.12, 0))
  536. self.assertEqual('1,000,000', format_number(1000000))
  537. self.assertEqual('1,000,000.42', format_number(1000000.42))
  538. # Regression test for https://github.com/xolox/python-humanfriendly/issues/40.
  539. self.assertEqual('-285.67', format_number(-285.67))
  540. def test_round_number(self):
  541. """Test :func:`humanfriendly.round_number()`."""
  542. self.assertEqual('1', round_number(1))
  543. self.assertEqual('1', round_number(1.0))
  544. self.assertEqual('1.00', round_number(1, keep_width=True))
  545. self.assertEqual('3.14', round_number(3.141592653589793))
  546. def test_format_path(self):
  547. """Test :func:`humanfriendly.format_path()`."""
  548. friendly_path = os.path.join('~', '.vimrc')
  549. absolute_path = os.path.join(os.environ['HOME'], '.vimrc')
  550. self.assertEqual(friendly_path, format_path(absolute_path))
  551. def test_parse_path(self):
  552. """Test :func:`humanfriendly.parse_path()`."""
  553. friendly_path = os.path.join('~', '.vimrc')
  554. absolute_path = os.path.join(os.environ['HOME'], '.vimrc')
  555. self.assertEqual(absolute_path, parse_path(friendly_path))
  556. def test_pretty_tables(self):
  557. """Test :func:`humanfriendly.tables.format_pretty_table()`."""
  558. # The simplest case possible :-).
  559. data = [['Just one column']]
  560. assert format_pretty_table(data) == dedent("""
  561. -------------------
  562. | Just one column |
  563. -------------------
  564. """).strip()
  565. # A bit more complex: two rows, three columns, varying widths.
  566. data = [['One', 'Two', 'Three'], ['1', '2', '3']]
  567. assert format_pretty_table(data) == dedent("""
  568. ---------------------
  569. | One | Two | Three |
  570. | 1 | 2 | 3 |
  571. ---------------------
  572. """).strip()
  573. # A table including column names.
  574. column_names = ['One', 'Two', 'Three']
  575. data = [['1', '2', '3'], ['a', 'b', 'c']]
  576. assert ansi_strip(format_pretty_table(data, column_names)) == dedent("""
  577. ---------------------
  578. | One | Two | Three |
  579. ---------------------
  580. | 1 | 2 | 3 |
  581. | a | b | c |
  582. ---------------------
  583. """).strip()
  584. # A table that contains a column with only numeric data (will be right aligned).
  585. column_names = ['Just a label', 'Important numbers']
  586. data = [['Row one', '15'], ['Row two', '300']]
  587. assert ansi_strip(format_pretty_table(data, column_names)) == dedent("""
  588. ------------------------------------
  589. | Just a label | Important numbers |
  590. ------------------------------------
  591. | Row one | 15 |
  592. | Row two | 300 |
  593. ------------------------------------
  594. """).strip()
  595. def test_robust_tables(self):
  596. """Test :func:`humanfriendly.tables.format_robust_table()`."""
  597. column_names = ['One', 'Two', 'Three']
  598. data = [['1', '2', '3'], ['a', 'b', 'c']]
  599. assert ansi_strip(format_robust_table(data, column_names)) == dedent("""
  600. --------
  601. One: 1
  602. Two: 2
  603. Three: 3
  604. --------
  605. One: a
  606. Two: b
  607. Three: c
  608. --------
  609. """).strip()
  610. column_names = ['One', 'Two', 'Three']
  611. data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']]
  612. assert ansi_strip(format_robust_table(data, column_names)) == dedent("""
  613. ------------------
  614. One: 1
  615. Two: 2
  616. Three: 3
  617. ------------------
  618. One: a
  619. Two: b
  620. Three:
  621. Here comes a
  622. multi line column!
  623. ------------------
  624. """).strip()
  625. def test_smart_tables(self):
  626. """Test :func:`humanfriendly.tables.format_smart_table()`."""
  627. column_names = ['One', 'Two', 'Three']
  628. data = [['1', '2', '3'], ['a', 'b', 'c']]
  629. assert ansi_strip(format_smart_table(data, column_names)) == dedent("""
  630. ---------------------
  631. | One | Two | Three |
  632. ---------------------
  633. | 1 | 2 | 3 |
  634. | a | b | c |
  635. ---------------------
  636. """).strip()
  637. column_names = ['One', 'Two', 'Three']
  638. data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']]
  639. assert ansi_strip(format_smart_table(data, column_names)) == dedent("""
  640. ------------------
  641. One: 1
  642. Two: 2
  643. Three: 3
  644. ------------------
  645. One: a
  646. Two: b
  647. Three:
  648. Here comes a
  649. multi line column!
  650. ------------------
  651. """).strip()
  652. def test_rst_tables(self):
  653. """Test :func:`humanfriendly.tables.format_rst_table()`."""
  654. # Generate a table with column names.
  655. column_names = ['One', 'Two', 'Three']
  656. data = [['1', '2', '3'], ['a', 'b', 'c']]
  657. self.assertEqual(
  658. format_rst_table(data, column_names),
  659. dedent("""
  660. === === =====
  661. One Two Three
  662. === === =====
  663. 1 2 3
  664. a b c
  665. === === =====
  666. """).rstrip(),
  667. )
  668. # Generate a table without column names.
  669. data = [['1', '2', '3'], ['a', 'b', 'c']]
  670. self.assertEqual(
  671. format_rst_table(data),
  672. dedent("""
  673. = = =
  674. 1 2 3
  675. a b c
  676. = = =
  677. """).rstrip(),
  678. )
  679. def test_concatenate(self):
  680. """Test :func:`humanfriendly.text.concatenate()`."""
  681. assert concatenate([]) == ''
  682. assert concatenate(['one']) == 'one'
  683. assert concatenate(['one', 'two']) == 'one and two'
  684. assert concatenate(['one', 'two', 'three']) == 'one, two and three'
  685. # Test the 'conjunction' option.
  686. assert concatenate(['one', 'two', 'three'], conjunction='or') == 'one, two or three'
  687. # Test the 'serial_comma' option.
  688. assert concatenate(['one', 'two', 'three'], serial_comma=True) == 'one, two, and three'
  689. def test_split(self):
  690. """Test :func:`humanfriendly.text.split()`."""
  691. from humanfriendly.text import split
  692. self.assertEqual(split(''), [])
  693. self.assertEqual(split('foo'), ['foo'])
  694. self.assertEqual(split('foo, bar'), ['foo', 'bar'])
  695. self.assertEqual(split('foo, bar, baz'), ['foo', 'bar', 'baz'])
  696. self.assertEqual(split('foo,bar,baz'), ['foo', 'bar', 'baz'])
  697. def test_timer(self):
  698. """Test :func:`humanfriendly.Timer`."""
  699. for seconds, text in ((1, '1 second'),
  700. (2, '2 seconds'),
  701. (60, '1 minute'),
  702. (60 * 2, '2 minutes'),
  703. (60 * 60, '1 hour'),
  704. (60 * 60 * 2, '2 hours'),
  705. (60 * 60 * 24, '1 day'),
  706. (60 * 60 * 24 * 2, '2 days'),
  707. (60 * 60 * 24 * 7, '1 week'),
  708. (60 * 60 * 24 * 7 * 2, '2 weeks')):
  709. t = Timer(time.time() - seconds)
  710. self.assertEqual(round_number(t.elapsed_time, keep_width=True), '%i.00' % seconds)
  711. self.assertEqual(str(t), text)
  712. # Test rounding to seconds.
  713. t = Timer(time.time() - 2.2)
  714. self.assertEqual(t.rounded, '2 seconds')
  715. # Test automatic timer.
  716. automatic_timer = Timer()
  717. time.sleep(1)
  718. # XXX The following normalize_timestamp(ndigits=0) calls are intended
  719. # to compensate for unreliable clock sources in virtual machines
  720. # like those encountered on Travis CI, see also:
  721. # https://travis-ci.org/xolox/python-humanfriendly/jobs/323944263
  722. self.assertEqual(normalize_timestamp(automatic_timer.elapsed_time, 0), '1.00')
  723. # Test resumable timer.
  724. resumable_timer = Timer(resumable=True)
  725. for i in range(2):
  726. with resumable_timer:
  727. time.sleep(1)
  728. self.assertEqual(normalize_timestamp(resumable_timer.elapsed_time, 0), '2.00')
  729. # Make sure Timer.__enter__() returns the timer object.
  730. with Timer(resumable=True) as timer:
  731. assert timer is not None
  732. def test_spinner(self):
  733. """Test :func:`humanfriendly.Spinner`."""
  734. stream = StringIO()
  735. spinner = Spinner(label='test spinner', total=4, stream=stream, interactive=True)
  736. for progress in [1, 2, 3, 4]:
  737. spinner.step(progress=progress)
  738. time.sleep(0.2)
  739. spinner.clear()
  740. output = stream.getvalue()
  741. output = (output.replace(ANSI_SHOW_CURSOR, '')
  742. .replace(ANSI_HIDE_CURSOR, ''))
  743. lines = [line for line in output.split(ANSI_ERASE_LINE) if line]
  744. self.assertTrue(len(lines) > 0)
  745. self.assertTrue(all('test spinner' in line for line in lines))
  746. self.assertTrue(all('%' in line for line in lines))
  747. self.assertEqual(sorted(set(lines)), sorted(lines))
  748. def test_automatic_spinner(self):
  749. """
  750. Test :func:`humanfriendly.AutomaticSpinner`.
  751. There's not a lot to test about the :class:`.AutomaticSpinner` class,
  752. but by at least running it here we are assured that the code functions
  753. on all supported Python versions. :class:`.AutomaticSpinner` is built
  754. on top of the :class:`.Spinner` class so at least we also have the
  755. tests for the :class:`.Spinner` class to back us up.
  756. """
  757. with AutomaticSpinner(label='test spinner'):
  758. time.sleep(1)
  759. def test_prompt_for_choice(self):
  760. """Test :func:`humanfriendly.prompts.prompt_for_choice()`."""
  761. # Choice selection without any options should raise an exception.
  762. with self.assertRaises(ValueError):
  763. prompt_for_choice([])
  764. # If there's only one option no prompt should be rendered so we expect
  765. # the following code to not raise an EOFError exception (despite
  766. # connecting standard input to /dev/null).
  767. with open(os.devnull) as handle:
  768. with PatchedAttribute(sys, 'stdin', handle):
  769. only_option = 'only one option (shortcut)'
  770. assert prompt_for_choice([only_option]) == only_option
  771. # Choice selection by full string match.
  772. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'foo'):
  773. assert prompt_for_choice(['foo', 'bar']) == 'foo'
  774. # Choice selection by substring input.
  775. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'f'):
  776. assert prompt_for_choice(['foo', 'bar']) == 'foo'
  777. # Choice selection by number.
  778. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: '2'):
  779. assert prompt_for_choice(['foo', 'bar']) == 'bar'
  780. # Choice selection by going with the default.
  781. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
  782. assert prompt_for_choice(['foo', 'bar'], default='bar') == 'bar'
  783. # Invalid substrings are refused.
  784. replies = ['', 'q', 'z']
  785. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
  786. assert prompt_for_choice(['foo', 'bar', 'baz']) == 'baz'
  787. # Choice selection by substring input requires an unambiguous substring match.
  788. replies = ['a', 'q']
  789. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
  790. assert prompt_for_choice(['foo', 'bar', 'baz', 'qux']) == 'qux'
  791. # Invalid numbers are refused.
  792. replies = ['42', '2']
  793. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
  794. assert prompt_for_choice(['foo', 'bar', 'baz']) == 'bar'
  795. # Test that interactive prompts eventually give up on invalid replies.
  796. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
  797. with self.assertRaises(TooManyInvalidReplies):
  798. prompt_for_choice(['a', 'b', 'c'])
  799. def test_prompt_for_confirmation(self):
  800. """Test :func:`humanfriendly.prompts.prompt_for_confirmation()`."""
  801. # Test some (more or less) reasonable replies that indicate agreement.
  802. for reply in 'yes', 'Yes', 'YES', 'y', 'Y':
  803. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply):
  804. assert prompt_for_confirmation("Are you sure?") is True
  805. # Test some (more or less) reasonable replies that indicate disagreement.
  806. for reply in 'no', 'No', 'NO', 'n', 'N':
  807. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply):
  808. assert prompt_for_confirmation("Are you sure?") is False
  809. # Test that empty replies select the default choice.
  810. for default_choice in True, False:
  811. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
  812. assert prompt_for_confirmation("Are you sure?", default=default_choice) is default_choice
  813. # Test that a warning is shown when no input nor a default is given.
  814. replies = ['', 'y']
  815. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
  816. with CaptureOutput(merged=True) as capturer:
  817. assert prompt_for_confirmation("Are you sure?") is True
  818. assert "there's no default choice" in capturer.get_text()
  819. # Test that the default reply is shown in uppercase.
  820. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'y'):
  821. for default_value, expected_text in (True, 'Y/n'), (False, 'y/N'), (None, 'y/n'):
  822. with CaptureOutput(merged=True) as capturer:
  823. assert prompt_for_confirmation("Are you sure?", default=default_value) is True
  824. assert expected_text in capturer.get_text()
  825. # Test that interactive prompts eventually give up on invalid replies.
  826. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
  827. with self.assertRaises(TooManyInvalidReplies):
  828. prompt_for_confirmation("Are you sure?")
  829. def test_prompt_for_input(self):
  830. """Test :func:`humanfriendly.prompts.prompt_for_input()`."""
  831. with open(os.devnull) as handle:
  832. with PatchedAttribute(sys, 'stdin', handle):
  833. # If standard input isn't connected to a terminal the default value should be returned.
  834. default_value = "To seek the holy grail!"
  835. assert prompt_for_input("What is your quest?", default=default_value) == default_value
  836. # If standard input isn't connected to a terminal and no default value
  837. # is given the EOFError exception should be propagated to the caller.
  838. with self.assertRaises(EOFError):
  839. prompt_for_input("What is your favorite color?")
  840. def test_cli(self):
  841. """Test the command line interface."""
  842. # Test that the usage message is printed by default.
  843. returncode, output = run_cli(main)
  844. assert 'Usage:' in output
  845. # Test that the usage message can be requested explicitly.
  846. returncode, output = run_cli(main, '--help')
  847. assert 'Usage:' in output
  848. # Test handling of invalid command line options.
  849. returncode, output = run_cli(main, '--unsupported-option')
  850. assert returncode != 0
  851. # Test `humanfriendly --format-number'.
  852. returncode, output = run_cli(main, '--format-number=1234567')
  853. assert output.strip() == '1,234,567'
  854. # Test `humanfriendly --format-size'.
  855. random_byte_count = random.randint(1024, 1024 * 1024)
  856. returncode, output = run_cli(main, '--format-size=%i' % random_byte_count)
  857. assert output.strip() == format_size(random_byte_count)
  858. # Test `humanfriendly --format-size --binary'.
  859. random_byte_count = random.randint(1024, 1024 * 1024)
  860. returncode, output = run_cli(main, '--format-size=%i' % random_byte_count, '--binary')
  861. assert output.strip() == format_size(random_byte_count, binary=True)
  862. # Test `humanfriendly --format-length'.
  863. random_len = random.randint(1024, 1024 * 1024)
  864. returncode, output = run_cli(main, '--format-length=%i' % random_len)
  865. assert output.strip() == format_length(random_len)
  866. random_len = float(random_len) / 12345.6
  867. returncode, output = run_cli(main, '--format-length=%f' % random_len)
  868. assert output.strip() == format_length(random_len)
  869. # Test `humanfriendly --format-table'.
  870. returncode, output = run_cli(main, '--format-table', '--delimiter=\t', input='1\t2\t3\n4\t5\t6\n7\t8\t9')
  871. assert output.strip() == dedent('''
  872. -------------
  873. | 1 | 2 | 3 |
  874. | 4 | 5 | 6 |
  875. | 7 | 8 | 9 |
  876. -------------
  877. ''').strip()
  878. # Test `humanfriendly --format-timespan'.
  879. random_timespan = random.randint(5, 600)
  880. returncode, output = run_cli(main, '--format-timespan=%i' % random_timespan)
  881. assert output.strip() == format_timespan(random_timespan)
  882. # Test `humanfriendly --parse-size'.
  883. returncode, output = run_cli(main, '--parse-size=5 KB')
  884. assert int(output) == parse_size('5 KB')
  885. # Test `humanfriendly --parse-size'.
  886. returncode, output = run_cli(main, '--parse-size=5 YiB')
  887. assert int(output) == parse_size('5 YB', binary=True)
  888. # Test `humanfriendly --parse-length'.
  889. returncode, output = run_cli(main, '--parse-length=5 km')
  890. assert int(output) == parse_length('5 km')
  891. returncode, output = run_cli(main, '--parse-length=1.05 km')
  892. assert float(output) == parse_length('1.05 km')
  893. # Test `humanfriendly --run-command'.
  894. returncode, output = run_cli(main, '--run-command', 'bash', '-c', 'sleep 2 && exit 42')
  895. assert returncode == 42
  896. # Test `humanfriendly --demo'. The purpose of this test is
  897. # to ensure that the demo runs successfully on all versions
  898. # of Python and outputs the expected sections (recognized by
  899. # their headings) without triggering exceptions. This was
  900. # written as a regression test after issue #28 was reported:
  901. # https://github.com/xolox/python-humanfriendly/issues/28
  902. returncode, output = run_cli(main, '--demo')
  903. assert returncode == 0
  904. lines = [ansi_strip(line) for line in output.splitlines()]
  905. assert "Text styles:" in lines
  906. assert "Foreground colors:" in lines
  907. assert "Background colors:" in lines
  908. assert "256 color mode (standard colors):" in lines
  909. assert "256 color mode (high-intensity colors):" in lines
  910. assert "256 color mode (216 colors):" in lines
  911. assert "256 color mode (gray scale colors):" in lines
  912. def test_ansi_style(self):
  913. """Test :func:`humanfriendly.terminal.ansi_style()`."""
  914. assert ansi_style(bold=True) == '%s1%s' % (ANSI_CSI, ANSI_SGR)
  915. assert ansi_style(faint=True) == '%s2%s' % (ANSI_CSI, ANSI_SGR)
  916. assert ansi_style(italic=True) == '%s3%s' % (ANSI_CSI, ANSI_SGR)
  917. assert ansi_style(underline=True) == '%s4%s' % (ANSI_CSI, ANSI_SGR)
  918. assert ansi_style(inverse=True) == '%s7%s' % (ANSI_CSI, ANSI_SGR)
  919. assert ansi_style(strike_through=True) == '%s9%s' % (ANSI_CSI, ANSI_SGR)
  920. assert ansi_style(color='blue') == '%s34%s' % (ANSI_CSI, ANSI_SGR)
  921. assert ansi_style(background='blue') == '%s44%s' % (ANSI_CSI, ANSI_SGR)
  922. assert ansi_style(color='blue', bright=True) == '%s94%s' % (ANSI_CSI, ANSI_SGR)
  923. assert ansi_style(color=214) == '%s38;5;214%s' % (ANSI_CSI, ANSI_SGR)
  924. assert ansi_style(background=214) == '%s39;5;214%s' % (ANSI_CSI, ANSI_SGR)
  925. assert ansi_style(color=(0, 0, 0)) == '%s38;2;0;0;0%s' % (ANSI_CSI, ANSI_SGR)
  926. assert ansi_style(color=(255, 255, 255)) == '%s38;2;255;255;255%s' % (ANSI_CSI, ANSI_SGR)
  927. assert ansi_style(background=(50, 100, 150)) == '%s48;2;50;100;150%s' % (ANSI_CSI, ANSI_SGR)
  928. with self.assertRaises(ValueError):
  929. ansi_style(color='unknown')
  930. def test_ansi_width(self):
  931. """Test :func:`humanfriendly.terminal.ansi_width()`."""
  932. text = "Whatever"
  933. # Make sure ansi_width() works as expected on strings without ANSI escape sequences.
  934. assert len(text) == ansi_width(text)
  935. # Wrap a text in ANSI escape sequences and make sure ansi_width() treats it as expected.
  936. wrapped = ansi_wrap(text, bold=True)
  937. # Make sure ansi_wrap() changed the text.
  938. assert wrapped != text
  939. # Make sure ansi_wrap() added additional bytes.
  940. assert len(wrapped) > len(text)
  941. # Make sure the result of ansi_width() stays the same.
  942. assert len(text) == ansi_width(wrapped)
  943. def test_ansi_wrap(self):
  944. """Test :func:`humanfriendly.terminal.ansi_wrap()`."""
  945. text = "Whatever"
  946. # Make sure ansi_wrap() does nothing when no keyword arguments are given.
  947. assert text == ansi_wrap(text)
  948. # Make sure ansi_wrap() starts the text with the CSI sequence.
  949. assert ansi_wrap(text, bold=True).startswith(ANSI_CSI)
  950. # Make sure ansi_wrap() ends the text by resetting the ANSI styles.
  951. assert ansi_wrap(text, bold=True).endswith(ANSI_RESET)
  952. def test_html_to_ansi(self):
  953. """Test the :func:`humanfriendly.terminal.html_to_ansi()` function."""
  954. assert html_to_ansi("Just some plain text") == "Just some plain text"
  955. # Hyperlinks.
  956. assert html_to_ansi('<a href="https://python.org">python.org</a>') == \
  957. '\x1b[0m\x1b[4;94mpython.org\x1b[0m (\x1b[0m\x1b[4;94mhttps://python.org\x1b[0m)'
  958. # Make sure `mailto:' prefixes are stripped (they're not at all useful in a terminal).
  959. assert html_to_ansi('<a href="mailto:peter@peterodding.com">peter@peterodding.com</a>') == \
  960. '\x1b[0m\x1b[4;94mpeter@peterodding.com\x1b[0m'
  961. # Bold text.
  962. assert html_to_ansi("Let's try <b>bold</b>") == "Let's try \x1b[0m\x1b[1mbold\x1b[0m"
  963. assert html_to_ansi("Let's try <span style=\"font-weight: bold\">bold</span>") == \
  964. "Let's try \x1b[0m\x1b[1mbold\x1b[0m"
  965. # Italic text.
  966. assert html_to_ansi("Let's try <i>italic</i>") == \
  967. "Let's try \x1b[0m\x1b[3mitalic\x1b[0m"
  968. assert html_to_ansi("Let's try <span style=\"font-style: italic\">italic</span>") == \
  969. "Let's try \x1b[0m\x1b[3mitalic\x1b[0m"
  970. # Underlined text.
  971. assert html_to_ansi("Let's try <ins>underline</ins>") == \
  972. "Let's try \x1b[0m\x1b[4munderline\x1b[0m"
  973. assert html_to_ansi("Let's try <span style=\"text-decoration: underline\">underline</span>") == \
  974. "Let's try \x1b[0m\x1b[4munderline\x1b[0m"
  975. # Strike-through text.
  976. assert html_to_ansi("Let's try <s>strike-through</s>") == \
  977. "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m"
  978. assert html_to_ansi("Let's try <span style=\"text-decoration: line-through\">strike-through</span>") == \
  979. "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m"
  980. # Pre-formatted text.
  981. assert html_to_ansi("Let's try <code>pre-formatted</code>") == \
  982. "Let's try \x1b[0m\x1b[33mpre-formatted\x1b[0m"
  983. # Text colors (with a 6 digit hexadecimal color value).
  984. assert html_to_ansi("Let's try <span style=\"color: #AABBCC\">text colors</s>") == \
  985. "Let's try \x1b[0m\x1b[38;2;170;187;204mtext colors\x1b[0m"
  986. # Background colors (with an rgb(N, N, N) expression).
  987. assert html_to_ansi("Let's try <span style=\"background-color: rgb(50, 50, 50)\">background colors</s>") == \
  988. "Let's try \x1b[0m\x1b[48;2;50;50;50mbackground colors\x1b[0m"
  989. # Line breaks.
  990. assert html_to_ansi("Let's try some<br>line<br>breaks") == \
  991. "Let's try some\nline\nbreaks"
  992. # Check that decimal entities are decoded.
  993. assert html_to_ansi("&#38;") == "&"
  994. # Check that named entities are decoded.
  995. assert html_to_ansi("&amp;") == "&"
  996. assert html_to_ansi("&gt;") == ">"
  997. assert html_to_ansi("&lt;") == "<"
  998. # Check that hexadecimal entities are decoded.
  999. assert html_to_ansi("&#x26;") == "&"
  1000. # Check that the text callback is actually called.
  1001. def callback(text):
  1002. return text.replace(':wink:', ';-)')
  1003. assert ':wink:' not in html_to_ansi('<b>:wink:</b>', callback=callback)
  1004. # Check that the text callback doesn't process preformatted text.
  1005. assert ':wink:' in html_to_ansi('<code>:wink:</code>', callback=callback)
  1006. # Try a somewhat convoluted but nevertheless real life example from my
  1007. # personal chat archives that causes humanfriendly releases 4.15 and
  1008. # 4.15.1 to raise an exception.
  1009. assert html_to_ansi(u'''
  1010. Tweakers zit er idd nog steeds:<br><br>
  1011. peter@peter-work&gt; curl -s <a href="tweakers.net">tweakers.net</a> | grep -i hosting<br>
  1012. &lt;a href="<a href="http://www.true.nl/webhosting/">http://www.true.nl/webhosting/</a>"
  1013. rel="external" id="true" title="Hosting door True"&gt;&lt;/a&gt;<br>
  1014. Hosting door &lt;a href="<a href="http://www.true.nl/vps/">http://www.true.nl/vps/</a>"
  1015. title="VPS hosting" rel="external"&gt;True</a>
  1016. ''')
  1017. def test_generate_output(self):
  1018. """Test the :func:`humanfriendly.terminal.output()` function."""
  1019. text = "Standard output generated by output()"
  1020. with CaptureOutput(merged=False) as capturer:
  1021. output(text)
  1022. self.assertEqual([text], capturer.stdout.get_lines())
  1023. self.assertEqual([], capturer.stderr.get_lines())
  1024. def test_generate_message(self):
  1025. """Test the :func:`humanfriendly.terminal.message()` function."""
  1026. text = "Standard error generated by message()"
  1027. with CaptureOutput(merged=False) as capturer:
  1028. message(text)
  1029. self.assertEqual([], capturer.stdout.get_lines())
  1030. self.assertEqual([text], capturer.stderr.get_lines())
  1031. def test_generate_warning(self):
  1032. """Test the :func:`humanfriendly.terminal.warning()` function."""
  1033. from capturer import CaptureOutput
  1034. text = "Standard error generated by warning()"
  1035. with CaptureOutput(merged=False) as capturer:
  1036. warning(text)
  1037. self.assertEqual([], capturer.stdout.get_lines())
  1038. self.assertEqual([ansi_wrap(text, color='red')], self.ignore_coverage_warning(capturer.stderr))
  1039. def ignore_coverage_warning(self, stream):
  1040. """
  1041. Filter out coverage.py warning from standard error.
  1042. This is intended to remove the following line from the lines captured
  1043. on the standard error stream:
  1044. Coverage.py warning: No data was collected. (no-data-collected)
  1045. """
  1046. return [line for line in stream.get_lines() if 'no-data-collected' not in line]
  1047. def test_clean_output(self):
  1048. """Test :func:`humanfriendly.terminal.clean_terminal_output()`."""
  1049. # Simple output should pass through unharmed (single line).
  1050. assert clean_terminal_output('foo') == ['foo']
  1051. # Simple output should pass through unharmed (multiple lines).
  1052. assert clean_terminal_output('foo\nbar') == ['foo', 'bar']
  1053. # Carriage returns and preceding substrings are removed.
  1054. assert clean_terminal_output('foo\rbar\nbaz') == ['bar', 'baz']
  1055. # Carriage returns move the cursor to the start of the line without erasing text.
  1056. assert clean_terminal_output('aaa\rab') == ['aba']
  1057. # Backspace moves the cursor one position back without erasing text.
  1058. assert clean_terminal_output('aaa\b\bb') == ['aba']
  1059. # Trailing empty lines should be stripped.
  1060. assert clean_terminal_output('foo\nbar\nbaz\n\n\n') == ['foo', 'bar', 'baz']
  1061. def test_find_terminal_size(self):
  1062. """Test :func:`humanfriendly.terminal.find_terminal_size()`."""
  1063. lines, columns = find_terminal_size()
  1064. # We really can't assert any minimum or maximum values here because it
  1065. # simply doesn't make any sense; it's impossible for me to anticipate
  1066. # on what environments this test suite will run in the future.
  1067. assert lines > 0
  1068. assert columns > 0
  1069. # The find_terminal_size_using_ioctl() function is the default
  1070. # implementation and it will likely work fine. This makes it hard to
  1071. # test the fall back code paths though. However there's an easy way to
  1072. # make find_terminal_size_using_ioctl() fail ...
  1073. saved_stdin = sys.stdin
  1074. saved_stdout = sys.stdout
  1075. saved_stderr = sys.stderr
  1076. try:
  1077. # What do you mean this is brute force?! ;-)
  1078. sys.stdin = StringIO()
  1079. sys.stdout = StringIO()
  1080. sys.stderr = StringIO()
  1081. # Now find_terminal_size_using_ioctl() should fail even though
  1082. # find_terminal_size_using_stty() might work fine.
  1083. lines, columns = find_terminal_size()
  1084. assert lines > 0
  1085. assert columns > 0
  1086. # There's also an ugly way to make `stty size' fail: The
  1087. # subprocess.Popen class uses os.execvp() underneath, so if we
  1088. # clear the $PATH it will break.
  1089. saved_path = os.environ['PATH']
  1090. try:
  1091. os.environ['PATH'] = ''
  1092. # Now find_terminal_size_using_stty() should fail.
  1093. lines, columns = find_terminal_size()
  1094. assert lines > 0
  1095. assert columns > 0
  1096. finally:
  1097. os.environ['PATH'] = saved_path
  1098. finally:
  1099. sys.stdin = saved_stdin
  1100. sys.stdout = saved_stdout
  1101. sys.stderr = saved_stderr
  1102. def test_terminal_capabilities(self):
  1103. """Test the functions that check for terminal capabilities."""
  1104. from capturer import CaptureOutput
  1105. for test_stream in connected_to_terminal, terminal_supports_colors:
  1106. # This test suite should be able to run interactively as well as
  1107. # non-interactively, so we can't expect or demand that standard streams
  1108. # will always be connected to a terminal. Fortunately Capturer enables
  1109. # us to fake it :-).
  1110. for stream in sys.stdout, sys.stderr:
  1111. with CaptureOutput():
  1112. assert test_stream(stream)
  1113. # Test something that we know can never be a terminal.
  1114. with open(os.devnull) as handle:
  1115. assert not test_stream(handle)
  1116. # Verify that objects without isatty() don't raise an exception.
  1117. assert not test_stream(object())
  1118. def test_show_pager(self):
  1119. """Test :func:`humanfriendly.terminal.show_pager()`."""
  1120. original_pager = os.environ.get('PAGER', None)
  1121. try:
  1122. # We specifically avoid `less' because it would become awkward to
  1123. # run the test suite in an interactive terminal :-).
  1124. os.environ['PAGER'] = 'cat'
  1125. # Generate a significant amount of random text spread over multiple
  1126. # lines that we expect to be reported literally on the terminal.
  1127. random_text = "\n".join(random_string(25) for i in range(50))
  1128. # Run the pager command and validate the output.
  1129. with CaptureOutput() as capturer:
  1130. show_pager(random_text)
  1131. assert random_text in capturer.get_text()
  1132. finally:
  1133. if original_pager is not None:
  1134. # Restore the original $PAGER value.
  1135. os.environ['PAGER'] = original_pager
  1136. else:
  1137. # Clear the custom $PAGER value.
  1138. os.environ.pop('PAGER')
  1139. def test_get_pager_command(self):
  1140. """Test :func:`humanfriendly.terminal.get_pager_command()`."""
  1141. # Make sure --RAW-CONTROL-CHARS isn't used when it's not needed.
  1142. assert '--RAW-CONTROL-CHARS' not in get_pager_command("Usage message")
  1143. # Make sure --RAW-CONTROL-CHARS is used when it's needed.
  1144. assert '--RAW-CONTROL-CHARS' in get_pager_command(ansi_wrap("Usage message", bold=True))
  1145. # Make sure that less-specific options are only used when valid.
  1146. options_specific_to_less = ['--no-init', '--quit-if-one-screen']
  1147. for pager in 'cat', 'less':
  1148. original_pager = os.environ.get('PAGER', None)
  1149. try:
  1150. # Set $PAGER to `cat' or `less'.
  1151. os.environ['PAGER'] = pager
  1152. # Get the pager command line.
  1153. command_line = get_pager_command()
  1154. # Check for less-specific options.
  1155. if pager == 'less':
  1156. assert all(opt in command_line for opt in options_specific_to_less)
  1157. else:
  1158. assert not any(opt in command_line for opt in options_specific_to_less)
  1159. finally:
  1160. if original_pager is not None:
  1161. # Restore the original $PAGER value.
  1162. os.environ['PAGER'] = original_pager
  1163. else:
  1164. # Clear the custom $PAGER value.
  1165. os.environ.pop('PAGER')
  1166. def test_find_meta_variables(self):
  1167. """Test :func:`humanfriendly.usage.find_meta_variables()`."""
  1168. assert sorted(find_meta_variables("""
  1169. Here's one example: --format-number=VALUE
  1170. Here's another example: --format-size=BYTES
  1171. A final example: --format-timespan=SECONDS
  1172. This line doesn't contain a META variable.
  1173. """)) == sorted(['VALUE', 'BYTES', 'SECONDS'])
  1174. def test_parse_usage_simple(self):
  1175. """Test :func:`humanfriendly.usage.parse_usage()` (a simple case)."""
  1176. introduction, options = self.preprocess_parse_result("""
  1177. Usage: my-fancy-app [OPTIONS]
  1178. Boring description.
  1179. Supported options:
  1180. -h, --help
  1181. Show this message and exit.
  1182. """)
  1183. # The following fragments are (expected to be) part of the introduction.
  1184. assert "Usage: my-fancy-app [OPTIONS]" in introduction
  1185. assert "Boring description." in introduction
  1186. assert "Supported options:" in introduction
  1187. # The following fragments are (expected to be) part of the documented options.
  1188. assert "-h, --help" in options
  1189. assert "Show this message and exit." in options
  1190. def test_parse_usage_tricky(self):
  1191. """Test :func:`humanfriendly.usage.parse_usage()` (a tricky case)."""
  1192. introduction, options = self.preprocess_parse_result("""
  1193. Usage: my-fancy-app [OPTIONS]
  1194. Here's the introduction to my-fancy-app. Some of the lines in the
  1195. introduction start with a command line option just to confuse the
  1196. parsing algorithm :-)
  1197. For example
  1198. --an-awesome-option
  1199. is still part of the introduction.
  1200. Supported options:
  1201. -a, --an-awesome-option
  1202. Explanation why this is an awesome option.
  1203. -b, --a-boring-option
  1204. Explanation why this is a boring option.
  1205. """)
  1206. # The following fragments are (expected to be) part of the introduction.
  1207. assert "Usage: my-fancy-app [OPTIONS]" in introduction
  1208. assert any('still part of the introduction' in p for p in introduction)
  1209. assert "Supported options:" in introduction
  1210. # The following fragments are (expected to be) part of the documented options.
  1211. assert "-a, --an-awesome-option" in options
  1212. assert "Explanation why this is an awesome option." in options
  1213. assert "-b, --a-boring-option" in options
  1214. assert "Explanation why this is a boring option." in options
  1215. def test_parse_usage_commas(self):
  1216. """Test :func:`humanfriendly.usage.parse_usage()` against option labels containing commas."""
  1217. introduction, options = self.preprocess_parse_result("""
  1218. Usage: my-fancy-app [OPTIONS]
  1219. Some introduction goes here.
  1220. Supported options:
  1221. -f, --first-option
  1222. Explanation of first option.
  1223. -s, --second-option=WITH,COMMA
  1224. This should be a separate option's description.
  1225. """)
  1226. # The following fragments are (expected to be) part of the introduction.
  1227. assert "Usage: my-fancy-app [OPTIONS]" in introduction
  1228. assert "Some introduction goes here." in introduction
  1229. assert "Supported options:" in introduction
  1230. # The following fragments are (expected to be) part of the documented options.
  1231. assert "-f, --first-option" in options
  1232. assert "Explanation of first option." in options
  1233. assert "-s, --second-option=WITH,COMMA" in options
  1234. assert "This should be a separate option's description." in options
  1235. def preprocess_parse_result(self, text):
  1236. """Ignore leading/trailing whitespace in usage parsing tests."""
  1237. return tuple([p.strip() for p in r] for r in parse_usage(dedent(text)))
  1238. def test_format_usage(self):
  1239. """Test :func:`humanfriendly.usage.format_usage()`."""
  1240. # Test that options are highlighted.
  1241. usage_text = "Just one --option"
  1242. formatted_text = format_usage(usage_text)
  1243. assert len(formatted_text) > len(usage_text)
  1244. assert formatted_text.startswith("Just one ")
  1245. # Test that the "Usage: ..." line is highlighted.
  1246. usage_text = "Usage: humanfriendly [OPTIONS]"
  1247. formatted_text = format_usage(usage_text)
  1248. assert len(formatted_text) > len(usage_text)
  1249. assert usage_text in formatted_text
  1250. assert not formatted_text.startswith(usage_text)
  1251. # Test that meta variables aren't erroneously highlighted.
  1252. usage_text = (
  1253. "--valid-option=VALID_METAVAR\n"
  1254. "VALID_METAVAR is bogus\n"
  1255. "INVALID_METAVAR should not be highlighted\n"
  1256. )
  1257. formatted_text = format_usage(usage_text)
  1258. formatted_lines = formatted_text.splitlines()
  1259. # Make sure the meta variable in the second line is highlighted.
  1260. assert ANSI_CSI in formatted_lines[1]
  1261. # Make sure the meta variable in the third line isn't highlighted.
  1262. assert ANSI_CSI not in formatted_lines[2]
  1263. def test_render_usage(self):
  1264. """Test :func:`humanfriendly.usage.render_usage()`."""
  1265. assert render_usage("Usage: some-command WITH ARGS") == "**Usage:** `some-command WITH ARGS`"
  1266. assert render_usage("Supported options:") == "**Supported options:**"
  1267. assert 'code-block' in render_usage(dedent("""
  1268. Here comes a shell command:
  1269. $ echo test
  1270. test
  1271. """))
  1272. assert all(token in render_usage(dedent("""
  1273. Supported options:
  1274. -n, --dry-run
  1275. Don't change anything.
  1276. """)) for token in ('`-n`', '`--dry-run`'))
  1277. def test_deprecated_args(self):
  1278. """Test the deprecated_args() decorator function."""
  1279. @deprecated_args('foo', 'bar')
  1280. def test_function(**options):
  1281. assert options['foo'] == 'foo'
  1282. assert options.get('bar') in (None, 'bar')
  1283. return 42
  1284. fake_fn = MagicMock()
  1285. with PatchedAttribute(warnings, 'warn', fake_fn):
  1286. assert test_function('foo', 'bar') == 42
  1287. with self.assertRaises(TypeError):
  1288. test_function('foo', 'bar', 'baz')
  1289. assert fake_fn.was_called
  1290. def test_alias_proxy_deprecation_warning(self):
  1291. """Test that the DeprecationProxy class emits deprecation warnings."""
  1292. fake_fn = MagicMock()
  1293. with PatchedAttribute(warnings, 'warn', fake_fn):
  1294. module = sys.modules[__name__]
  1295. aliases = dict(concatenate='humanfriendly.text.concatenate')
  1296. proxy = DeprecationProxy(module, aliases)
  1297. assert proxy.concatenate == concatenate
  1298. assert fake_fn.was_called
  1299. def test_alias_proxy_sphinx_compensation(self):
  1300. """Test that the DeprecationProxy class emits deprecation warnings."""
  1301. with PatchedItem(sys.modules, 'sphinx', types.ModuleType('sphinx')):
  1302. define_aliases(__name__, concatenate='humanfriendly.text.concatenate')
  1303. assert "concatenate" in dir(sys.modules[__name__])
  1304. assert "concatenate" in get_aliases(__name__)
  1305. def test_alias_proxy_sphinx_integration(self):
  1306. """Test that aliases can be injected into generated documentation."""
  1307. module = sys.modules[__name__]
  1308. define_aliases(__name__, concatenate='humanfriendly.text.concatenate')
  1309. lines = module.__doc__.splitlines()
  1310. deprecation_note_callback(app=None, what=None, name=None, obj=module, options=None, lines=lines)
  1311. # Check that something was injected.
  1312. assert "\n".join(lines) != module.__doc__
  1313. def test_sphinx_customizations(self):
  1314. """Test the :mod:`humanfriendly.sphinx` module."""
  1315. class FakeApp(object):
  1316. def __init__(self):
  1317. self.callbacks = {}
  1318. self.roles = {}
  1319. def __documented_special_method__(self):
  1320. """Documented unofficial special method."""
  1321. pass
  1322. def __undocumented_special_method__(self):
  1323. # Intentionally not documented :-).
  1324. pass
  1325. def add_role(self, name, callback):
  1326. self.roles[name] = callback
  1327. def connect(self, event, callback):
  1328. self.callbacks.setdefault(event, []).append(callback)
  1329. def bogus_usage(self):
  1330. """Usage: This is not supposed to be reformatted!"""
  1331. pass
  1332. # Test event callback registration.
  1333. fake_app = FakeApp()
  1334. setup(fake_app)
  1335. assert man_role == fake_app.roles['man']
  1336. assert pypi_role == fake_app.roles['pypi']
  1337. assert deprecation_note_callback in fake_app.callbacks['autodoc-process-docstring']
  1338. assert special_methods_callback in fake_app.callbacks['autodoc-skip-member']
  1339. assert usage_message_callback in fake_app.callbacks['autodoc-process-docstring']
  1340. # Test that `special methods' which are documented aren't skipped.
  1341. assert special_methods_callback(
  1342. app=None, what=None, name=None,
  1343. obj=FakeApp.__documented_special_method__,
  1344. skip=True, options=None,
  1345. ) is False
  1346. # Test that `special methods' which are undocumented are skipped.
  1347. assert special_methods_callback(
  1348. app=None, what=None, name=None,
  1349. obj=FakeApp.__undocumented_special_method__,
  1350. skip=True, options=None,
  1351. ) is True
  1352. # Test formatting of usage messages. obj/lines
  1353. from humanfriendly import cli, sphinx
  1354. # We expect the docstring in the `cli' module to be reformatted
  1355. # (because it contains a usage message in the expected format).
  1356. assert self.docstring_is_reformatted(cli)
  1357. # We don't expect the docstring in the `sphinx' module to be
  1358. # reformatted (because it doesn't contain a usage message).
  1359. assert not self.docstring_is_reformatted(sphinx)
  1360. # We don't expect the docstring of the following *method* to be
  1361. # reformatted because only *module* docstrings should be reformatted.
  1362. assert not self.docstring_is_reformatted(fake_app.bogus_usage)
  1363. def docstring_is_reformatted(self, entity):
  1364. """Check whether :func:`.usage_message_callback()` reformats a module's docstring."""
  1365. lines = trim_empty_lines(entity.__doc__).splitlines()
  1366. saved_lines = list(lines)
  1367. usage_message_callback(
  1368. app=None, what=None, name=None,
  1369. obj=entity, options=None, lines=lines,
  1370. )
  1371. return lines != saved_lines
  1372. def normalize_timestamp(value, ndigits=1):
  1373. """
  1374. Round timestamps to the given number of digits.
  1375. This helps to make the test suite less sensitive to timing issues caused by
  1376. multitasking, processor scheduling, etc.
  1377. """
  1378. return '%.2f' % round(float(value), ndigits=ndigits)