comparison env/lib/python3.7/site-packages/humanfriendly/tests.py @ 5:9b1c78e6ba9c draft default tip

"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author shellac
date Mon, 01 Jun 2020 08:59:25 -0400
parents 79f47841a781
children
comparison
equal deleted inserted replaced
4:79f47841a781 5:9b1c78e6ba9c
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: April 19, 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 second' == 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
579 def test_round_number(self):
580 """Test :func:`humanfriendly.round_number()`."""
581 self.assertEqual('1', round_number(1))
582 self.assertEqual('1', round_number(1.0))
583 self.assertEqual('1.00', round_number(1, keep_width=True))
584 self.assertEqual('3.14', round_number(3.141592653589793))
585
586 def test_format_path(self):
587 """Test :func:`humanfriendly.format_path()`."""
588 friendly_path = os.path.join('~', '.vimrc')
589 absolute_path = os.path.join(os.environ['HOME'], '.vimrc')
590 self.assertEqual(friendly_path, format_path(absolute_path))
591
592 def test_parse_path(self):
593 """Test :func:`humanfriendly.parse_path()`."""
594 friendly_path = os.path.join('~', '.vimrc')
595 absolute_path = os.path.join(os.environ['HOME'], '.vimrc')
596 self.assertEqual(absolute_path, parse_path(friendly_path))
597
598 def test_pretty_tables(self):
599 """Test :func:`humanfriendly.tables.format_pretty_table()`."""
600 # The simplest case possible :-).
601 data = [['Just one column']]
602 assert format_pretty_table(data) == dedent("""
603 -------------------
604 | Just one column |
605 -------------------
606 """).strip()
607 # A bit more complex: two rows, three columns, varying widths.
608 data = [['One', 'Two', 'Three'], ['1', '2', '3']]
609 assert format_pretty_table(data) == dedent("""
610 ---------------------
611 | One | Two | Three |
612 | 1 | 2 | 3 |
613 ---------------------
614 """).strip()
615 # A table including column names.
616 column_names = ['One', 'Two', 'Three']
617 data = [['1', '2', '3'], ['a', 'b', 'c']]
618 assert ansi_strip(format_pretty_table(data, column_names)) == dedent("""
619 ---------------------
620 | One | Two | Three |
621 ---------------------
622 | 1 | 2 | 3 |
623 | a | b | c |
624 ---------------------
625 """).strip()
626 # A table that contains a column with only numeric data (will be right aligned).
627 column_names = ['Just a label', 'Important numbers']
628 data = [['Row one', '15'], ['Row two', '300']]
629 assert ansi_strip(format_pretty_table(data, column_names)) == dedent("""
630 ------------------------------------
631 | Just a label | Important numbers |
632 ------------------------------------
633 | Row one | 15 |
634 | Row two | 300 |
635 ------------------------------------
636 """).strip()
637
638 def test_robust_tables(self):
639 """Test :func:`humanfriendly.tables.format_robust_table()`."""
640 column_names = ['One', 'Two', 'Three']
641 data = [['1', '2', '3'], ['a', 'b', 'c']]
642 assert ansi_strip(format_robust_table(data, column_names)) == dedent("""
643 --------
644 One: 1
645 Two: 2
646 Three: 3
647 --------
648 One: a
649 Two: b
650 Three: c
651 --------
652 """).strip()
653 column_names = ['One', 'Two', 'Three']
654 data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']]
655 assert ansi_strip(format_robust_table(data, column_names)) == dedent("""
656 ------------------
657 One: 1
658 Two: 2
659 Three: 3
660 ------------------
661 One: a
662 Two: b
663 Three:
664 Here comes a
665 multi line column!
666 ------------------
667 """).strip()
668
669 def test_smart_tables(self):
670 """Test :func:`humanfriendly.tables.format_smart_table()`."""
671 column_names = ['One', 'Two', 'Three']
672 data = [['1', '2', '3'], ['a', 'b', 'c']]
673 assert ansi_strip(format_smart_table(data, column_names)) == dedent("""
674 ---------------------
675 | One | Two | Three |
676 ---------------------
677 | 1 | 2 | 3 |
678 | a | b | c |
679 ---------------------
680 """).strip()
681 column_names = ['One', 'Two', 'Three']
682 data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']]
683 assert ansi_strip(format_smart_table(data, column_names)) == dedent("""
684 ------------------
685 One: 1
686 Two: 2
687 Three: 3
688 ------------------
689 One: a
690 Two: b
691 Three:
692 Here comes a
693 multi line column!
694 ------------------
695 """).strip()
696
697 def test_rst_tables(self):
698 """Test :func:`humanfriendly.tables.format_rst_table()`."""
699 # Generate a table with column names.
700 column_names = ['One', 'Two', 'Three']
701 data = [['1', '2', '3'], ['a', 'b', 'c']]
702 self.assertEqual(
703 format_rst_table(data, column_names),
704 dedent("""
705 === === =====
706 One Two Three
707 === === =====
708 1 2 3
709 a b c
710 === === =====
711 """).rstrip(),
712 )
713 # Generate a table without column names.
714 data = [['1', '2', '3'], ['a', 'b', 'c']]
715 self.assertEqual(
716 format_rst_table(data),
717 dedent("""
718 = = =
719 1 2 3
720 a b c
721 = = =
722 """).rstrip(),
723 )
724
725 def test_concatenate(self):
726 """Test :func:`humanfriendly.text.concatenate()`."""
727 assert concatenate([]) == ''
728 assert concatenate(['one']) == 'one'
729 assert concatenate(['one', 'two']) == 'one and two'
730 assert concatenate(['one', 'two', 'three']) == 'one, two and three'
731
732 def test_split(self):
733 """Test :func:`humanfriendly.text.split()`."""
734 from humanfriendly.text import split
735 self.assertEqual(split(''), [])
736 self.assertEqual(split('foo'), ['foo'])
737 self.assertEqual(split('foo, bar'), ['foo', 'bar'])
738 self.assertEqual(split('foo, bar, baz'), ['foo', 'bar', 'baz'])
739 self.assertEqual(split('foo,bar,baz'), ['foo', 'bar', 'baz'])
740
741 def test_timer(self):
742 """Test :func:`humanfriendly.Timer`."""
743 for seconds, text in ((1, '1 second'),
744 (2, '2 seconds'),
745 (60, '1 minute'),
746 (60 * 2, '2 minutes'),
747 (60 * 60, '1 hour'),
748 (60 * 60 * 2, '2 hours'),
749 (60 * 60 * 24, '1 day'),
750 (60 * 60 * 24 * 2, '2 days'),
751 (60 * 60 * 24 * 7, '1 week'),
752 (60 * 60 * 24 * 7 * 2, '2 weeks')):
753 t = Timer(time.time() - seconds)
754 self.assertEqual(round_number(t.elapsed_time, keep_width=True), '%i.00' % seconds)
755 self.assertEqual(str(t), text)
756 # Test rounding to seconds.
757 t = Timer(time.time() - 2.2)
758 self.assertEqual(t.rounded, '2 seconds')
759 # Test automatic timer.
760 automatic_timer = Timer()
761 time.sleep(1)
762 # XXX The following normalize_timestamp(ndigits=0) calls are intended
763 # to compensate for unreliable clock sources in virtual machines
764 # like those encountered on Travis CI, see also:
765 # https://travis-ci.org/xolox/python-humanfriendly/jobs/323944263
766 self.assertEqual(normalize_timestamp(automatic_timer.elapsed_time, 0), '1.00')
767 # Test resumable timer.
768 resumable_timer = Timer(resumable=True)
769 for i in range(2):
770 with resumable_timer:
771 time.sleep(1)
772 self.assertEqual(normalize_timestamp(resumable_timer.elapsed_time, 0), '2.00')
773 # Make sure Timer.__enter__() returns the timer object.
774 with Timer(resumable=True) as timer:
775 assert timer is not None
776
777 def test_spinner(self):
778 """Test :func:`humanfriendly.Spinner`."""
779 stream = StringIO()
780 spinner = Spinner(label='test spinner', total=4, stream=stream, interactive=True)
781 for progress in [1, 2, 3, 4]:
782 spinner.step(progress=progress)
783 time.sleep(0.2)
784 spinner.clear()
785 output = stream.getvalue()
786 output = (output.replace(ANSI_SHOW_CURSOR, '')
787 .replace(ANSI_HIDE_CURSOR, ''))
788 lines = [line for line in output.split(ANSI_ERASE_LINE) if line]
789 self.assertTrue(len(lines) > 0)
790 self.assertTrue(all('test spinner' in l for l in lines))
791 self.assertTrue(all('%' in l for l in lines))
792 self.assertEqual(sorted(set(lines)), sorted(lines))
793
794 def test_automatic_spinner(self):
795 """
796 Test :func:`humanfriendly.AutomaticSpinner`.
797
798 There's not a lot to test about the :class:`.AutomaticSpinner` class,
799 but by at least running it here we are assured that the code functions
800 on all supported Python versions. :class:`.AutomaticSpinner` is built
801 on top of the :class:`.Spinner` class so at least we also have the
802 tests for the :class:`.Spinner` class to back us up.
803 """
804 with AutomaticSpinner(label='test spinner'):
805 time.sleep(1)
806
807 def test_prompt_for_choice(self):
808 """Test :func:`humanfriendly.prompts.prompt_for_choice()`."""
809 # Choice selection without any options should raise an exception.
810 with self.assertRaises(ValueError):
811 prompt_for_choice([])
812 # If there's only one option no prompt should be rendered so we expect
813 # the following code to not raise an EOFError exception (despite
814 # connecting standard input to /dev/null).
815 with open(os.devnull) as handle:
816 with PatchedAttribute(sys, 'stdin', handle):
817 only_option = 'only one option (shortcut)'
818 assert prompt_for_choice([only_option]) == only_option
819 # Choice selection by full string match.
820 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'foo'):
821 assert prompt_for_choice(['foo', 'bar']) == 'foo'
822 # Choice selection by substring input.
823 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'f'):
824 assert prompt_for_choice(['foo', 'bar']) == 'foo'
825 # Choice selection by number.
826 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: '2'):
827 assert prompt_for_choice(['foo', 'bar']) == 'bar'
828 # Choice selection by going with the default.
829 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
830 assert prompt_for_choice(['foo', 'bar'], default='bar') == 'bar'
831 # Invalid substrings are refused.
832 replies = ['', 'q', 'z']
833 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
834 assert prompt_for_choice(['foo', 'bar', 'baz']) == 'baz'
835 # Choice selection by substring input requires an unambiguous substring match.
836 replies = ['a', 'q']
837 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
838 assert prompt_for_choice(['foo', 'bar', 'baz', 'qux']) == 'qux'
839 # Invalid numbers are refused.
840 replies = ['42', '2']
841 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
842 assert prompt_for_choice(['foo', 'bar', 'baz']) == 'bar'
843 # Test that interactive prompts eventually give up on invalid replies.
844 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
845 with self.assertRaises(TooManyInvalidReplies):
846 prompt_for_choice(['a', 'b', 'c'])
847
848 def test_prompt_for_confirmation(self):
849 """Test :func:`humanfriendly.prompts.prompt_for_confirmation()`."""
850 # Test some (more or less) reasonable replies that indicate agreement.
851 for reply in 'yes', 'Yes', 'YES', 'y', 'Y':
852 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply):
853 assert prompt_for_confirmation("Are you sure?") is True
854 # Test some (more or less) reasonable replies that indicate disagreement.
855 for reply in 'no', 'No', 'NO', 'n', 'N':
856 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply):
857 assert prompt_for_confirmation("Are you sure?") is False
858 # Test that empty replies select the default choice.
859 for default_choice in True, False:
860 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
861 assert prompt_for_confirmation("Are you sure?", default=default_choice) is default_choice
862 # Test that a warning is shown when no input nor a default is given.
863 replies = ['', 'y']
864 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)):
865 with CaptureOutput(merged=True) as capturer:
866 assert prompt_for_confirmation("Are you sure?") is True
867 assert "there's no default choice" in capturer.get_text()
868 # Test that the default reply is shown in uppercase.
869 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'y'):
870 for default_value, expected_text in (True, 'Y/n'), (False, 'y/N'), (None, 'y/n'):
871 with CaptureOutput(merged=True) as capturer:
872 assert prompt_for_confirmation("Are you sure?", default=default_value) is True
873 assert expected_text in capturer.get_text()
874 # Test that interactive prompts eventually give up on invalid replies.
875 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''):
876 with self.assertRaises(TooManyInvalidReplies):
877 prompt_for_confirmation("Are you sure?")
878
879 def test_prompt_for_input(self):
880 """Test :func:`humanfriendly.prompts.prompt_for_input()`."""
881 with open(os.devnull) as handle:
882 with PatchedAttribute(sys, 'stdin', handle):
883 # If standard input isn't connected to a terminal the default value should be returned.
884 default_value = "To seek the holy grail!"
885 assert prompt_for_input("What is your quest?", default=default_value) == default_value
886 # If standard input isn't connected to a terminal and no default value
887 # is given the EOFError exception should be propagated to the caller.
888 with self.assertRaises(EOFError):
889 prompt_for_input("What is your favorite color?")
890
891 def test_cli(self):
892 """Test the command line interface."""
893 # Test that the usage message is printed by default.
894 returncode, output = run_cli(main)
895 assert 'Usage:' in output
896 # Test that the usage message can be requested explicitly.
897 returncode, output = run_cli(main, '--help')
898 assert 'Usage:' in output
899 # Test handling of invalid command line options.
900 returncode, output = run_cli(main, '--unsupported-option')
901 assert returncode != 0
902 # Test `humanfriendly --format-number'.
903 returncode, output = run_cli(main, '--format-number=1234567')
904 assert output.strip() == '1,234,567'
905 # Test `humanfriendly --format-size'.
906 random_byte_count = random.randint(1024, 1024 * 1024)
907 returncode, output = run_cli(main, '--format-size=%i' % random_byte_count)
908 assert output.strip() == format_size(random_byte_count)
909 # Test `humanfriendly --format-size --binary'.
910 random_byte_count = random.randint(1024, 1024 * 1024)
911 returncode, output = run_cli(main, '--format-size=%i' % random_byte_count, '--binary')
912 assert output.strip() == format_size(random_byte_count, binary=True)
913 # Test `humanfriendly --format-length'.
914 random_len = random.randint(1024, 1024 * 1024)
915 returncode, output = run_cli(main, '--format-length=%i' % random_len)
916 assert output.strip() == format_length(random_len)
917 random_len = float(random_len) / 12345.6
918 returncode, output = run_cli(main, '--format-length=%f' % random_len)
919 assert output.strip() == format_length(random_len)
920 # Test `humanfriendly --format-table'.
921 returncode, output = run_cli(main, '--format-table', '--delimiter=\t', input='1\t2\t3\n4\t5\t6\n7\t8\t9')
922 assert output.strip() == dedent('''
923 -------------
924 | 1 | 2 | 3 |
925 | 4 | 5 | 6 |
926 | 7 | 8 | 9 |
927 -------------
928 ''').strip()
929 # Test `humanfriendly --format-timespan'.
930 random_timespan = random.randint(5, 600)
931 returncode, output = run_cli(main, '--format-timespan=%i' % random_timespan)
932 assert output.strip() == format_timespan(random_timespan)
933 # Test `humanfriendly --parse-size'.
934 returncode, output = run_cli(main, '--parse-size=5 KB')
935 assert int(output) == parse_size('5 KB')
936 # Test `humanfriendly --parse-size'.
937 returncode, output = run_cli(main, '--parse-size=5 YiB')
938 assert int(output) == parse_size('5 YB', binary=True)
939 # Test `humanfriendly --parse-length'.
940 returncode, output = run_cli(main, '--parse-length=5 km')
941 assert int(output) == parse_length('5 km')
942 returncode, output = run_cli(main, '--parse-length=1.05 km')
943 assert float(output) == parse_length('1.05 km')
944 # Test `humanfriendly --run-command'.
945 returncode, output = run_cli(main, '--run-command', 'bash', '-c', 'sleep 2 && exit 42')
946 assert returncode == 42
947 # Test `humanfriendly --demo'. The purpose of this test is
948 # to ensure that the demo runs successfully on all versions
949 # of Python and outputs the expected sections (recognized by
950 # their headings) without triggering exceptions. This was
951 # written as a regression test after issue #28 was reported:
952 # https://github.com/xolox/python-humanfriendly/issues/28
953 returncode, output = run_cli(main, '--demo')
954 assert returncode == 0
955 lines = [ansi_strip(l) for l in output.splitlines()]
956 assert "Text styles:" in lines
957 assert "Foreground colors:" in lines
958 assert "Background colors:" in lines
959 assert "256 color mode (standard colors):" in lines
960 assert "256 color mode (high-intensity colors):" in lines
961 assert "256 color mode (216 colors):" in lines
962 assert "256 color mode (gray scale colors):" in lines
963
964 def test_ansi_style(self):
965 """Test :func:`humanfriendly.terminal.ansi_style()`."""
966 assert ansi_style(bold=True) == '%s1%s' % (ANSI_CSI, ANSI_SGR)
967 assert ansi_style(faint=True) == '%s2%s' % (ANSI_CSI, ANSI_SGR)
968 assert ansi_style(italic=True) == '%s3%s' % (ANSI_CSI, ANSI_SGR)
969 assert ansi_style(underline=True) == '%s4%s' % (ANSI_CSI, ANSI_SGR)
970 assert ansi_style(inverse=True) == '%s7%s' % (ANSI_CSI, ANSI_SGR)
971 assert ansi_style(strike_through=True) == '%s9%s' % (ANSI_CSI, ANSI_SGR)
972 assert ansi_style(color='blue') == '%s34%s' % (ANSI_CSI, ANSI_SGR)
973 assert ansi_style(background='blue') == '%s44%s' % (ANSI_CSI, ANSI_SGR)
974 assert ansi_style(color='blue', bright=True) == '%s94%s' % (ANSI_CSI, ANSI_SGR)
975 assert ansi_style(color=214) == '%s38;5;214%s' % (ANSI_CSI, ANSI_SGR)
976 assert ansi_style(background=214) == '%s39;5;214%s' % (ANSI_CSI, ANSI_SGR)
977 assert ansi_style(color=(0, 0, 0)) == '%s38;2;0;0;0%s' % (ANSI_CSI, ANSI_SGR)
978 assert ansi_style(color=(255, 255, 255)) == '%s38;2;255;255;255%s' % (ANSI_CSI, ANSI_SGR)
979 assert ansi_style(background=(50, 100, 150)) == '%s48;2;50;100;150%s' % (ANSI_CSI, ANSI_SGR)
980 with self.assertRaises(ValueError):
981 ansi_style(color='unknown')
982
983 def test_ansi_width(self):
984 """Test :func:`humanfriendly.terminal.ansi_width()`."""
985 text = "Whatever"
986 # Make sure ansi_width() works as expected on strings without ANSI escape sequences.
987 assert len(text) == ansi_width(text)
988 # Wrap a text in ANSI escape sequences and make sure ansi_width() treats it as expected.
989 wrapped = ansi_wrap(text, bold=True)
990 # Make sure ansi_wrap() changed the text.
991 assert wrapped != text
992 # Make sure ansi_wrap() added additional bytes.
993 assert len(wrapped) > len(text)
994 # Make sure the result of ansi_width() stays the same.
995 assert len(text) == ansi_width(wrapped)
996
997 def test_ansi_wrap(self):
998 """Test :func:`humanfriendly.terminal.ansi_wrap()`."""
999 text = "Whatever"
1000 # Make sure ansi_wrap() does nothing when no keyword arguments are given.
1001 assert text == ansi_wrap(text)
1002 # Make sure ansi_wrap() starts the text with the CSI sequence.
1003 assert ansi_wrap(text, bold=True).startswith(ANSI_CSI)
1004 # Make sure ansi_wrap() ends the text by resetting the ANSI styles.
1005 assert ansi_wrap(text, bold=True).endswith(ANSI_RESET)
1006
1007 def test_html_to_ansi(self):
1008 """Test the :func:`humanfriendly.terminal.html_to_ansi()` function."""
1009 assert html_to_ansi("Just some plain text") == "Just some plain text"
1010 # Hyperlinks.
1011 assert html_to_ansi('<a href="https://python.org">python.org</a>') == \
1012 '\x1b[0m\x1b[4;94mpython.org\x1b[0m (\x1b[0m\x1b[4;94mhttps://python.org\x1b[0m)'
1013 # Make sure `mailto:' prefixes are stripped (they're not at all useful in a terminal).
1014 assert html_to_ansi('<a href="mailto:peter@peterodding.com">peter@peterodding.com</a>') == \
1015 '\x1b[0m\x1b[4;94mpeter@peterodding.com\x1b[0m'
1016 # Bold text.
1017 assert html_to_ansi("Let's try <b>bold</b>") == "Let's try \x1b[0m\x1b[1mbold\x1b[0m"
1018 assert html_to_ansi("Let's try <span style=\"font-weight: bold\">bold</span>") == \
1019 "Let's try \x1b[0m\x1b[1mbold\x1b[0m"
1020 # Italic text.
1021 assert html_to_ansi("Let's try <i>italic</i>") == \
1022 "Let's try \x1b[0m\x1b[3mitalic\x1b[0m"
1023 assert html_to_ansi("Let's try <span style=\"font-style: italic\">italic</span>") == \
1024 "Let's try \x1b[0m\x1b[3mitalic\x1b[0m"
1025 # Underlined text.
1026 assert html_to_ansi("Let's try <ins>underline</ins>") == \
1027 "Let's try \x1b[0m\x1b[4munderline\x1b[0m"
1028 assert html_to_ansi("Let's try <span style=\"text-decoration: underline\">underline</span>") == \
1029 "Let's try \x1b[0m\x1b[4munderline\x1b[0m"
1030 # Strike-through text.
1031 assert html_to_ansi("Let's try <s>strike-through</s>") == \
1032 "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m"
1033 assert html_to_ansi("Let's try <span style=\"text-decoration: line-through\">strike-through</span>") == \
1034 "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m"
1035 # Pre-formatted text.
1036 assert html_to_ansi("Let's try <code>pre-formatted</code>") == \
1037 "Let's try \x1b[0m\x1b[33mpre-formatted\x1b[0m"
1038 # Text colors (with a 6 digit hexadecimal color value).
1039 assert html_to_ansi("Let's try <span style=\"color: #AABBCC\">text colors</s>") == \
1040 "Let's try \x1b[0m\x1b[38;2;170;187;204mtext colors\x1b[0m"
1041 # Background colors (with an rgb(N, N, N) expression).
1042 assert html_to_ansi("Let's try <span style=\"background-color: rgb(50, 50, 50)\">background colors</s>") == \
1043 "Let's try \x1b[0m\x1b[48;2;50;50;50mbackground colors\x1b[0m"
1044 # Line breaks.
1045 assert html_to_ansi("Let's try some<br>line<br>breaks") == \
1046 "Let's try some\nline\nbreaks"
1047 # Check that decimal entities are decoded.
1048 assert html_to_ansi("&#38;") == "&"
1049 # Check that named entities are decoded.
1050 assert html_to_ansi("&amp;") == "&"
1051 assert html_to_ansi("&gt;") == ">"
1052 assert html_to_ansi("&lt;") == "<"
1053 # Check that hexadecimal entities are decoded.
1054 assert html_to_ansi("&#x26;") == "&"
1055 # Check that the text callback is actually called.
1056
1057 def callback(text):
1058 return text.replace(':wink:', ';-)')
1059
1060 assert ':wink:' not in html_to_ansi('<b>:wink:</b>', callback=callback)
1061 # Check that the text callback doesn't process preformatted text.
1062 assert ':wink:' in html_to_ansi('<code>:wink:</code>', callback=callback)
1063 # Try a somewhat convoluted but nevertheless real life example from my
1064 # personal chat archives that causes humanfriendly releases 4.15 and
1065 # 4.15.1 to raise an exception.
1066 assert html_to_ansi(u'''
1067 Tweakers zit er idd nog steeds:<br><br>
1068 peter@peter-work&gt; curl -s <a href="tweakers.net">tweakers.net</a> | grep -i hosting<br>
1069 &lt;a href="<a href="http://www.true.nl/webhosting/">http://www.true.nl/webhosting/</a>"
1070 rel="external" id="true" title="Hosting door True"&gt;&lt;/a&gt;<br>
1071 Hosting door &lt;a href="<a href="http://www.true.nl/vps/">http://www.true.nl/vps/</a>"
1072 title="VPS hosting" rel="external"&gt;True</a>
1073 ''')
1074
1075 def test_generate_output(self):
1076 """Test the :func:`humanfriendly.terminal.output()` function."""
1077 text = "Standard output generated by output()"
1078 with CaptureOutput(merged=False) as capturer:
1079 output(text)
1080 self.assertEqual([text], capturer.stdout.get_lines())
1081 self.assertEqual([], capturer.stderr.get_lines())
1082
1083 def test_generate_message(self):
1084 """Test the :func:`humanfriendly.terminal.message()` function."""
1085 text = "Standard error generated by message()"
1086 with CaptureOutput(merged=False) as capturer:
1087 message(text)
1088 self.assertEqual([], capturer.stdout.get_lines())
1089 self.assertEqual([text], capturer.stderr.get_lines())
1090
1091 def test_generate_warning(self):
1092 """Test the :func:`humanfriendly.terminal.warning()` function."""
1093 from capturer import CaptureOutput
1094 text = "Standard error generated by warning()"
1095 with CaptureOutput(merged=False) as capturer:
1096 warning(text)
1097 self.assertEqual([], capturer.stdout.get_lines())
1098 self.assertEqual([ansi_wrap(text, color='red')], self.ignore_coverage_warning(capturer.stderr))
1099
1100 def ignore_coverage_warning(self, stream):
1101 """
1102 Filter out coverage.py warning from standard error.
1103
1104 This is intended to remove the following line from the lines captured
1105 on the standard error stream:
1106
1107 Coverage.py warning: No data was collected. (no-data-collected)
1108 """
1109 return [line for line in stream.get_lines() if 'no-data-collected' not in line]
1110
1111 def test_clean_output(self):
1112 """Test :func:`humanfriendly.terminal.clean_terminal_output()`."""
1113 # Simple output should pass through unharmed (single line).
1114 assert clean_terminal_output('foo') == ['foo']
1115 # Simple output should pass through unharmed (multiple lines).
1116 assert clean_terminal_output('foo\nbar') == ['foo', 'bar']
1117 # Carriage returns and preceding substrings are removed.
1118 assert clean_terminal_output('foo\rbar\nbaz') == ['bar', 'baz']
1119 # Carriage returns move the cursor to the start of the line without erasing text.
1120 assert clean_terminal_output('aaa\rab') == ['aba']
1121 # Backspace moves the cursor one position back without erasing text.
1122 assert clean_terminal_output('aaa\b\bb') == ['aba']
1123 # Trailing empty lines should be stripped.
1124 assert clean_terminal_output('foo\nbar\nbaz\n\n\n') == ['foo', 'bar', 'baz']
1125
1126 def test_find_terminal_size(self):
1127 """Test :func:`humanfriendly.terminal.find_terminal_size()`."""
1128 lines, columns = find_terminal_size()
1129 # We really can't assert any minimum or maximum values here because it
1130 # simply doesn't make any sense; it's impossible for me to anticipate
1131 # on what environments this test suite will run in the future.
1132 assert lines > 0
1133 assert columns > 0
1134 # The find_terminal_size_using_ioctl() function is the default
1135 # implementation and it will likely work fine. This makes it hard to
1136 # test the fall back code paths though. However there's an easy way to
1137 # make find_terminal_size_using_ioctl() fail ...
1138 saved_stdin = sys.stdin
1139 saved_stdout = sys.stdout
1140 saved_stderr = sys.stderr
1141 try:
1142 # What do you mean this is brute force?! ;-)
1143 sys.stdin = StringIO()
1144 sys.stdout = StringIO()
1145 sys.stderr = StringIO()
1146 # Now find_terminal_size_using_ioctl() should fail even though
1147 # find_terminal_size_using_stty() might work fine.
1148 lines, columns = find_terminal_size()
1149 assert lines > 0
1150 assert columns > 0
1151 # There's also an ugly way to make `stty size' fail: The
1152 # subprocess.Popen class uses os.execvp() underneath, so if we
1153 # clear the $PATH it will break.
1154 saved_path = os.environ['PATH']
1155 try:
1156 os.environ['PATH'] = ''
1157 # Now find_terminal_size_using_stty() should fail.
1158 lines, columns = find_terminal_size()
1159 assert lines > 0
1160 assert columns > 0
1161 finally:
1162 os.environ['PATH'] = saved_path
1163 finally:
1164 sys.stdin = saved_stdin
1165 sys.stdout = saved_stdout
1166 sys.stderr = saved_stderr
1167
1168 def test_terminal_capabilities(self):
1169 """Test the functions that check for terminal capabilities."""
1170 from capturer import CaptureOutput
1171 for test_stream in connected_to_terminal, terminal_supports_colors:
1172 # This test suite should be able to run interactively as well as
1173 # non-interactively, so we can't expect or demand that standard streams
1174 # will always be connected to a terminal. Fortunately Capturer enables
1175 # us to fake it :-).
1176 for stream in sys.stdout, sys.stderr:
1177 with CaptureOutput():
1178 assert test_stream(stream)
1179 # Test something that we know can never be a terminal.
1180 with open(os.devnull) as handle:
1181 assert not test_stream(handle)
1182 # Verify that objects without isatty() don't raise an exception.
1183 assert not test_stream(object())
1184
1185 def test_show_pager(self):
1186 """Test :func:`humanfriendly.terminal.show_pager()`."""
1187 original_pager = os.environ.get('PAGER', None)
1188 try:
1189 # We specifically avoid `less' because it would become awkward to
1190 # run the test suite in an interactive terminal :-).
1191 os.environ['PAGER'] = 'cat'
1192 # Generate a significant amount of random text spread over multiple
1193 # lines that we expect to be reported literally on the terminal.
1194 random_text = "\n".join(random_string(25) for i in range(50))
1195 # Run the pager command and validate the output.
1196 with CaptureOutput() as capturer:
1197 show_pager(random_text)
1198 assert random_text in capturer.get_text()
1199 finally:
1200 if original_pager is not None:
1201 # Restore the original $PAGER value.
1202 os.environ['PAGER'] = original_pager
1203 else:
1204 # Clear the custom $PAGER value.
1205 os.environ.pop('PAGER')
1206
1207 def test_get_pager_command(self):
1208 """Test :func:`humanfriendly.terminal.get_pager_command()`."""
1209 # Make sure --RAW-CONTROL-CHARS isn't used when it's not needed.
1210 assert '--RAW-CONTROL-CHARS' not in get_pager_command("Usage message")
1211 # Make sure --RAW-CONTROL-CHARS is used when it's needed.
1212 assert '--RAW-CONTROL-CHARS' in get_pager_command(ansi_wrap("Usage message", bold=True))
1213 # Make sure that less-specific options are only used when valid.
1214 options_specific_to_less = ['--no-init', '--quit-if-one-screen']
1215 for pager in 'cat', 'less':
1216 original_pager = os.environ.get('PAGER', None)
1217 try:
1218 # Set $PAGER to `cat' or `less'.
1219 os.environ['PAGER'] = pager
1220 # Get the pager command line.
1221 command_line = get_pager_command()
1222 # Check for less-specific options.
1223 if pager == 'less':
1224 assert all(opt in command_line for opt in options_specific_to_less)
1225 else:
1226 assert not any(opt in command_line for opt in options_specific_to_less)
1227 finally:
1228 if original_pager is not None:
1229 # Restore the original $PAGER value.
1230 os.environ['PAGER'] = original_pager
1231 else:
1232 # Clear the custom $PAGER value.
1233 os.environ.pop('PAGER')
1234
1235 def test_find_meta_variables(self):
1236 """Test :func:`humanfriendly.usage.find_meta_variables()`."""
1237 assert sorted(find_meta_variables("""
1238 Here's one example: --format-number=VALUE
1239 Here's another example: --format-size=BYTES
1240 A final example: --format-timespan=SECONDS
1241 This line doesn't contain a META variable.
1242 """)) == sorted(['VALUE', 'BYTES', 'SECONDS'])
1243
1244 def test_parse_usage_simple(self):
1245 """Test :func:`humanfriendly.usage.parse_usage()` (a simple case)."""
1246 introduction, options = self.preprocess_parse_result("""
1247 Usage: my-fancy-app [OPTIONS]
1248
1249 Boring description.
1250
1251 Supported options:
1252
1253 -h, --help
1254
1255 Show this message and exit.
1256 """)
1257 # The following fragments are (expected to be) part of the introduction.
1258 assert "Usage: my-fancy-app [OPTIONS]" in introduction
1259 assert "Boring description." in introduction
1260 assert "Supported options:" in introduction
1261 # The following fragments are (expected to be) part of the documented options.
1262 assert "-h, --help" in options
1263 assert "Show this message and exit." in options
1264
1265 def test_parse_usage_tricky(self):
1266 """Test :func:`humanfriendly.usage.parse_usage()` (a tricky case)."""
1267 introduction, options = self.preprocess_parse_result("""
1268 Usage: my-fancy-app [OPTIONS]
1269
1270 Here's the introduction to my-fancy-app. Some of the lines in the
1271 introduction start with a command line option just to confuse the
1272 parsing algorithm :-)
1273
1274 For example
1275 --an-awesome-option
1276 is still part of the introduction.
1277
1278 Supported options:
1279
1280 -a, --an-awesome-option
1281
1282 Explanation why this is an awesome option.
1283
1284 -b, --a-boring-option
1285
1286 Explanation why this is a boring option.
1287 """)
1288 # The following fragments are (expected to be) part of the introduction.
1289 assert "Usage: my-fancy-app [OPTIONS]" in introduction
1290 assert any('still part of the introduction' in p for p in introduction)
1291 assert "Supported options:" in introduction
1292 # The following fragments are (expected to be) part of the documented options.
1293 assert "-a, --an-awesome-option" in options
1294 assert "Explanation why this is an awesome option." in options
1295 assert "-b, --a-boring-option" in options
1296 assert "Explanation why this is a boring option." in options
1297
1298 def test_parse_usage_commas(self):
1299 """Test :func:`humanfriendly.usage.parse_usage()` against option labels containing commas."""
1300 introduction, options = self.preprocess_parse_result("""
1301 Usage: my-fancy-app [OPTIONS]
1302
1303 Some introduction goes here.
1304
1305 Supported options:
1306
1307 -f, --first-option
1308
1309 Explanation of first option.
1310
1311 -s, --second-option=WITH,COMMA
1312
1313 This should be a separate option's description.
1314 """)
1315 # The following fragments are (expected to be) part of the introduction.
1316 assert "Usage: my-fancy-app [OPTIONS]" in introduction
1317 assert "Some introduction goes here." in introduction
1318 assert "Supported options:" in introduction
1319 # The following fragments are (expected to be) part of the documented options.
1320 assert "-f, --first-option" in options
1321 assert "Explanation of first option." in options
1322 assert "-s, --second-option=WITH,COMMA" in options
1323 assert "This should be a separate option's description." in options
1324
1325 def preprocess_parse_result(self, text):
1326 """Ignore leading/trailing whitespace in usage parsing tests."""
1327 return tuple([p.strip() for p in r] for r in parse_usage(dedent(text)))
1328
1329 def test_format_usage(self):
1330 """Test :func:`humanfriendly.usage.format_usage()`."""
1331 # Test that options are highlighted.
1332 usage_text = "Just one --option"
1333 formatted_text = format_usage(usage_text)
1334 assert len(formatted_text) > len(usage_text)
1335 assert formatted_text.startswith("Just one ")
1336 # Test that the "Usage: ..." line is highlighted.
1337 usage_text = "Usage: humanfriendly [OPTIONS]"
1338 formatted_text = format_usage(usage_text)
1339 assert len(formatted_text) > len(usage_text)
1340 assert usage_text in formatted_text
1341 assert not formatted_text.startswith(usage_text)
1342 # Test that meta variables aren't erroneously highlighted.
1343 usage_text = (
1344 "--valid-option=VALID_METAVAR\n"
1345 "VALID_METAVAR is bogus\n"
1346 "INVALID_METAVAR should not be highlighted\n"
1347 )
1348 formatted_text = format_usage(usage_text)
1349 formatted_lines = formatted_text.splitlines()
1350 # Make sure the meta variable in the second line is highlighted.
1351 assert ANSI_CSI in formatted_lines[1]
1352 # Make sure the meta variable in the third line isn't highlighted.
1353 assert ANSI_CSI not in formatted_lines[2]
1354
1355 def test_render_usage(self):
1356 """Test :func:`humanfriendly.usage.render_usage()`."""
1357 assert render_usage("Usage: some-command WITH ARGS") == "**Usage:** `some-command WITH ARGS`"
1358 assert render_usage("Supported options:") == "**Supported options:**"
1359 assert 'code-block' in render_usage(dedent("""
1360 Here comes a shell command:
1361
1362 $ echo test
1363 test
1364 """))
1365 assert all(token in render_usage(dedent("""
1366 Supported options:
1367
1368 -n, --dry-run
1369
1370 Don't change anything.
1371 """)) for token in ('`-n`', '`--dry-run`'))
1372
1373 def test_deprecated_args(self):
1374 """Test the deprecated_args() decorator function."""
1375 @deprecated_args('foo', 'bar')
1376 def test_function(**options):
1377 assert options['foo'] == 'foo'
1378 assert options.get('bar') in (None, 'bar')
1379 return 42
1380 fake_fn = MagicMock()
1381 with PatchedAttribute(warnings, 'warn', fake_fn):
1382 assert test_function('foo', 'bar') == 42
1383 with self.assertRaises(TypeError):
1384 test_function('foo', 'bar', 'baz')
1385 assert fake_fn.was_called
1386
1387 def test_alias_proxy_deprecation_warning(self):
1388 """Test that the DeprecationProxy class emits deprecation warnings."""
1389 fake_fn = MagicMock()
1390 with PatchedAttribute(warnings, 'warn', fake_fn):
1391 module = sys.modules[__name__]
1392 aliases = dict(concatenate='humanfriendly.text.concatenate')
1393 proxy = DeprecationProxy(module, aliases)
1394 assert proxy.concatenate == concatenate
1395 assert fake_fn.was_called
1396
1397 def test_alias_proxy_sphinx_compensation(self):
1398 """Test that the DeprecationProxy class emits deprecation warnings."""
1399 with PatchedItem(sys.modules, 'sphinx', types.ModuleType('sphinx')):
1400 define_aliases(__name__, concatenate='humanfriendly.text.concatenate')
1401 assert "concatenate" in dir(sys.modules[__name__])
1402 assert "concatenate" in get_aliases(__name__)
1403
1404 def test_alias_proxy_sphinx_integration(self):
1405 """Test that aliases can be injected into generated documentation."""
1406 module = sys.modules[__name__]
1407 define_aliases(__name__, concatenate='humanfriendly.text.concatenate')
1408 lines = module.__doc__.splitlines()
1409 deprecation_note_callback(app=None, what=None, name=None, obj=module, options=None, lines=lines)
1410 # Check that something was injected.
1411 assert "\n".join(lines) != module.__doc__
1412
1413 def test_sphinx_customizations(self):
1414 """Test the :mod:`humanfriendly.sphinx` module."""
1415 class FakeApp(object):
1416
1417 def __init__(self):
1418 self.callbacks = {}
1419 self.roles = {}
1420
1421 def __documented_special_method__(self):
1422 """Documented unofficial special method."""
1423 pass
1424
1425 def __undocumented_special_method__(self):
1426 # Intentionally not documented :-).
1427 pass
1428
1429 def add_role(self, name, callback):
1430 self.roles[name] = callback
1431
1432 def connect(self, event, callback):
1433 self.callbacks.setdefault(event, []).append(callback)
1434
1435 def bogus_usage(self):
1436 """Usage: This is not supposed to be reformatted!"""
1437 pass
1438
1439 # Test event callback registration.
1440 fake_app = FakeApp()
1441 setup(fake_app)
1442 assert man_role == fake_app.roles['man']
1443 assert pypi_role == fake_app.roles['pypi']
1444 assert deprecation_note_callback in fake_app.callbacks['autodoc-process-docstring']
1445 assert special_methods_callback in fake_app.callbacks['autodoc-skip-member']
1446 assert usage_message_callback in fake_app.callbacks['autodoc-process-docstring']
1447 # Test that `special methods' which are documented aren't skipped.
1448 assert special_methods_callback(
1449 app=None, what=None, name=None,
1450 obj=FakeApp.__documented_special_method__,
1451 skip=True, options=None,
1452 ) is False
1453 # Test that `special methods' which are undocumented are skipped.
1454 assert special_methods_callback(
1455 app=None, what=None, name=None,
1456 obj=FakeApp.__undocumented_special_method__,
1457 skip=True, options=None,
1458 ) is True
1459 # Test formatting of usage messages. obj/lines
1460 from humanfriendly import cli, sphinx
1461 # We expect the docstring in the `cli' module to be reformatted
1462 # (because it contains a usage message in the expected format).
1463 assert self.docstring_is_reformatted(cli)
1464 # We don't expect the docstring in the `sphinx' module to be
1465 # reformatted (because it doesn't contain a usage message).
1466 assert not self.docstring_is_reformatted(sphinx)
1467 # We don't expect the docstring of the following *method* to be
1468 # reformatted because only *module* docstrings should be reformatted.
1469 assert not self.docstring_is_reformatted(fake_app.bogus_usage)
1470
1471 def docstring_is_reformatted(self, entity):
1472 """Check whether :func:`.usage_message_callback()` reformats a module's docstring."""
1473 lines = trim_empty_lines(entity.__doc__).splitlines()
1474 saved_lines = list(lines)
1475 usage_message_callback(
1476 app=None, what=None, name=None,
1477 obj=entity, options=None, lines=lines,
1478 )
1479 return lines != saved_lines
1480
1481
1482 def normalize_timestamp(value, ndigits=1):
1483 """
1484 Round timestamps to the given number of digits.
1485
1486 This helps to make the test suite less sensitive to timing issues caused by
1487 multitasking, processor scheduling, etc.
1488 """
1489 return '%.2f' % round(float(value), ndigits=ndigits)