comparison env/lib/python3.9/site-packages/humanfriendly/tests.py @ 0:4f3585e2f14b draft default tip

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