File: test_formatters.py

package info (click to toggle)
python-blessed 1.21.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,296 kB
  • sloc: python: 7,215; makefile: 13; sh: 7
file content (509 lines) | stat: -rw-r--r-- 16,290 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
# -*- coding: utf-8 -*-
"""Tests string formatting functions."""
# std imports
import pickle
import platform
import multiprocessing

# 3rd party
import pytest

# local
from .accessories import TestTerminal, as_subprocess

try:
    from unittest import mock
except ImportError:
    import mock

if platform.system() != 'Windows':
    import curses
else:
    import jinxed as curses


def fn_tparm(*args):
    """Mock tparm function"""
    return u'~'.join(
        str(arg) if num else arg.decode('latin1') for num, arg in enumerate(args)
    ).encode('latin1')


def test_parameterizing_string_args_unspecified(monkeypatch):
    """Test default args of formatters.ParameterizingString."""
    from blessed.formatters import ParameterizingString, FormattingString
    # first argument to tparm() is the sequence name, returned as-is;
    # subsequent arguments are usually Integers.
    monkeypatch.setattr(curses, 'tparm', fn_tparm)

    # given,
    pstr = ParameterizingString(u'')

    # exercise __new__
    assert str(pstr) == u''
    assert pstr._normal == u''
    assert pstr._name == u'<not specified>'

    # exercise __call__
    zero = pstr(0)
    assert isinstance(zero, FormattingString)
    assert zero == u'~0'
    assert zero('text') == u'~0text'

    # exercise __call__ with multiple args
    onetwo = pstr(1, 2)
    assert isinstance(onetwo, FormattingString)
    assert onetwo == u'~1~2'
    assert onetwo('text') == u'~1~2text'


def test_parameterizing_string_args(monkeypatch):
    """Test basic formatters.ParameterizingString."""
    from blessed.formatters import ParameterizingString, FormattingString

    # first argument to tparm() is the sequence name, returned as-is;
    # subsequent arguments are usually Integers.
    monkeypatch.setattr(curses, 'tparm', fn_tparm)

    # given,
    pstr = ParameterizingString(u'cap', u'norm', u'seq-name')

    # exercise __new__
    assert str(pstr) == u'cap'
    assert pstr._normal == u'norm'
    assert pstr._name == u'seq-name'

    # exercise __call__
    zero = pstr(0)
    assert isinstance(zero, FormattingString)
    assert zero == u'cap~0'
    assert zero('text') == u'cap~0textnorm'

    # exercise __call__ with multiple args
    onetwo = pstr(1, 2)
    assert isinstance(onetwo, FormattingString)
    assert onetwo == u'cap~1~2'
    assert onetwo('text') == u'cap~1~2textnorm'


def test_parameterizing_string_type_error(monkeypatch):
    """Test formatters.ParameterizingString raising TypeError."""
    from blessed.formatters import ParameterizingString

    def tparm_raises_TypeError(*args):
        raise TypeError('custom_err')

    monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError)

    # given,
    pstr = ParameterizingString(u'cap', u'norm', u'cap-name')

    # ensure TypeError when given a string raises custom exception
    try:
        pstr('XYZ')
        assert False, "previous call should have raised TypeError"
    except TypeError as err:
        assert err.args[0] in ((
            "Unknown terminal capability, 'cap-name', or, TypeError "
            "for arguments ('XYZ',): custom_err"  # py3x
        ), (
            "Unknown terminal capability, u'cap-name', or, TypeError "
            "for arguments ('XYZ',): custom_err"))  # py2

    # ensure TypeError when given an integer raises its natural exception
    try:
        pstr(0)
        assert False, "previous call should have raised TypeError"
    except TypeError as err:
        assert err.args[0] == "custom_err"


def test_formattingstring(monkeypatch):
    """Test simple __call__ behavior of formatters.FormattingString."""
    from blessed.formatters import FormattingString

    # given, with arg
    pstr = FormattingString(u'attr', u'norm')

    # exercise __call__,
    assert pstr._normal == u'norm'
    assert str(pstr) == u'attr'
    assert pstr('text') == u'attrtextnorm'

    # given, with empty attribute
    pstr = FormattingString(u'', u'norm')
    assert pstr('text') == u'text'


def test_nested_formattingstring(monkeypatch):
    """Test nested __call__ behavior of formatters.FormattingString."""
    from blessed.formatters import FormattingString

    # given, with arg
    pstr = FormattingString(u'a1-', u'n-')
    zstr = FormattingString(u'a2-', u'n-')

    # exercise __call__
    assert pstr('x-', zstr('f-'), 'q-') == 'a1-x-a2-f-n-a1-q-n-'


def test_nested_formattingstring_type_error(monkeypatch):
    """Test formatters.FormattingString raising TypeError."""
    from blessed.formatters import FormattingString

    # given,
    pstr = FormattingString(u'a-', u'n-')
    expected_msgs = ((
        "TypeError for FormattingString argument, 291, at position 1: "
        "expected type str, got int"  # py3x
    ), ("TypeError for FormattingString argument, 291, at position 1: "
        "expected type basestring, got int"
        ))  # py2

    # exercise,
    with pytest.raises(TypeError) as err:
        pstr('text', 0x123, '...')

    # verify,
    assert str(err.value) in expected_msgs


def test_nullcallablestring(monkeypatch):
    """Test formatters.NullCallableString."""
    from blessed.formatters import (NullCallableString)

    # given, with arg
    pstr = NullCallableString()

    # exercise __call__,
    assert str(pstr) == u''
    assert pstr('text') == u'text'
    assert pstr('text', 'moretext') == u'textmoretext'
    assert pstr(99, 1) == u''
    assert pstr() == u''
    assert pstr(0) == u''


def test_split_compound():
    """Test formatters.split_compound."""
    from blessed.formatters import split_compound

    assert split_compound(u'') == [u'']
    assert split_compound(u'a_b_c') == [u'a', u'b', u'c']
    assert split_compound(u'a_on_b_c') == [u'a', u'on_b', u'c']
    assert split_compound(u'a_bright_b_c') == [u'a', u'bright_b', u'c']
    assert split_compound(u'a_on_bright_b_c') == [u'a', u'on_bright_b', u'c']


def test_resolve_capability(monkeypatch):
    """Test formatters.resolve_capability and term sugaring."""
    from blessed.formatters import resolve_capability

    # given, always returns a b'seq'
    def tigetstr(attr):
        return ('seq-%s' % (attr,)).encode('latin1')
    monkeypatch.setattr(curses, 'tigetstr', tigetstr)
    term = mock.Mock()
    term._sugar = {'mnemonic': 'xyz'}

    # exercise
    assert resolve_capability(term, 'mnemonic') == u'seq-xyz'
    assert resolve_capability(term, 'natural') == u'seq-natural'

    # given, where tigetstr returns None
    def tigetstr_none(attr):
        return None
    monkeypatch.setattr(curses, 'tigetstr', tigetstr_none)

    # exercise,
    assert resolve_capability(term, 'natural') == u''

    # given, where does_styling is False
    def raises_exception(*args):
        assert False, "Should not be called"
    term.does_styling = False
    monkeypatch.setattr(curses, 'tigetstr', raises_exception)

    # exercise,
    assert resolve_capability(term, 'natural') == u''


def test_resolve_color(monkeypatch):
    """Test formatters.resolve_color."""
    from blessed.formatters import (resolve_color,
                                    FormattingString,
                                    NullCallableString)

    def color_cap(digit):
        return 'seq-%s' % (digit,)
    monkeypatch.setattr(curses, 'COLOR_RED', 1984)

    # given, terminal with color capabilities
    term = mock.Mock()
    term._background_color = color_cap
    term._foreground_color = color_cap
    term.number_of_colors = -1
    term.normal = 'seq-normal'

    # exercise,
    red = resolve_color(term, 'red')
    assert isinstance(red, FormattingString)
    assert red == u'seq-1984'
    assert red('text') == u'seq-1984textseq-normal'

    # exercise bold, +8
    bright_red = resolve_color(term, 'bright_red')
    assert isinstance(bright_red, FormattingString)
    assert bright_red == u'seq-1992'
    assert bright_red('text') == u'seq-1992textseq-normal'

    # given, terminal without color
    term.number_of_colors = 0

    # exercise,
    red = resolve_color(term, 'red')
    assert isinstance(red, NullCallableString)
    assert red == u''
    assert red('text') == u'text'

    # exercise bold,
    bright_red = resolve_color(term, 'bright_red')
    assert isinstance(bright_red, NullCallableString)
    assert bright_red == u''
    assert bright_red('text') == u'text'


def test_resolve_attribute_as_color(monkeypatch):
    """Test simple resolve_attribte() given color name."""
    import blessed
    from blessed.formatters import resolve_attribute

    def resolve_color(term, digit):
        return 'seq-%s' % (digit,)
    COLORS = {'COLORX', 'COLORY'}
    COMPOUNDABLES = {'JOINT', 'COMPOUND'}
    monkeypatch.setattr(blessed.formatters, 'resolve_color', resolve_color)
    monkeypatch.setattr(blessed.formatters, 'COLORS', COLORS)
    monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES)
    term = mock.Mock()
    assert resolve_attribute(term, 'COLORX') == u'seq-COLORX'


def test_resolve_attribute_as_compoundable(monkeypatch):
    """Test simple resolve_attribte() given a compoundable."""
    import blessed
    from blessed.formatters import resolve_attribute, FormattingString

    def resolve_cap(term, digit):
        return 'seq-%s' % (digit,)
    COMPOUNDABLES = {'JOINT', 'COMPOUND'}
    monkeypatch.setattr(blessed.formatters,
                        'resolve_capability',
                        resolve_cap)
    monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES)
    term = mock.Mock()
    term.normal = 'seq-normal'

    compound = resolve_attribute(term, 'JOINT')
    assert isinstance(compound, FormattingString)
    assert str(compound) == u'seq-JOINT'
    assert compound('text') == u'seq-JOINTtextseq-normal'


def test_resolve_attribute_non_compoundables(monkeypatch):
    """Test recursive compounding of resolve_attribute()."""
    import blessed
    from blessed.formatters import resolve_attribute, ParameterizingString

    def uncompoundables(attr):
        return ['split', 'compound']

    def resolve_cap(term, digit):
        return 'seq-%s' % (digit,)

    monkeypatch.setattr(blessed.formatters,
                        'split_compound',
                        uncompoundables)
    monkeypatch.setattr(blessed.formatters,
                        'resolve_capability',
                        resolve_cap)
    monkeypatch.setattr(curses, 'tparm', fn_tparm)

    term = mock.Mock()
    term.normal = 'seq-normal'

    # given
    pstr = resolve_attribute(term, 'not-a-compoundable')
    assert isinstance(pstr, ParameterizingString)
    assert str(pstr) == u'seq-not-a-compoundable'
    # this is like calling term.move_x(3)
    assert pstr(3) == u'seq-not-a-compoundable~3'
    # this is like calling term.move_x(3)('text')
    assert pstr(3)('text') == u'seq-not-a-compoundable~3textseq-normal'


def test_resolve_attribute_recursive_compoundables(monkeypatch):
    """Test recursive compounding of resolve_attribute()."""
    import blessed
    from blessed.formatters import resolve_attribute, FormattingString

    # patch,
    def resolve_cap(term, digit):
        return 'seq-%s' % (digit,)
    monkeypatch.setattr(blessed.formatters,
                        'resolve_capability',
                        resolve_cap)
    monkeypatch.setattr(curses, 'tparm', fn_tparm)
    monkeypatch.setattr(curses, 'COLOR_RED', 6502)
    monkeypatch.setattr(curses, 'COLOR_BLUE', 6800)

    def color_cap(digit):
        return 'seq-%s' % (digit,)
    term = mock.Mock()
    term._background_color = color_cap
    term._foreground_color = color_cap
    term.normal = 'seq-normal'

    # given,
    pstr = resolve_attribute(term, 'bright_blue_on_red')

    # exercise,
    assert isinstance(pstr, FormattingString)
    assert str(pstr) == 'seq-6808seq-6502'
    assert pstr('text') == 'seq-6808seq-6502textseq-normal'


def test_formattingstring_picklability():
    """Test pickle-ability of a FormattingString."""
    @as_subprocess
    def child():
        t = TestTerminal(force_styling=True)
        # basic pickle
        assert pickle.loads(pickle.dumps(t.red))('orange') == t.red('orange')
        assert pickle.loads(pickle.dumps(t.normal)) == t.normal

        # and, pickle through multiprocessing
        r, w = multiprocessing.Pipe()
        w.send(t.normal)
        assert r.recv() == t.normal

    child()


def test_formattingotherstring_picklability():
    """Test pickle-ability of a FormattingOtherString."""
    @as_subprocess
    def child():
        t = TestTerminal(force_styling=True)
        # basic pickle
        assert pickle.loads(pickle.dumps(t.move_left)) == t.move_left
        assert pickle.loads(pickle.dumps(t.move_left(3))) == t.move_left(3)
        assert pickle.loads(pickle.dumps(t.move_left))(3) == t.move_left(3)

        # and, pickle through multiprocessing
        r, w = multiprocessing.Pipe()
        w.send(t.move_left)
        assert r.recv()(3) == t.move_left(3)
        w.send(t.move_left(3))
        assert r.recv() == t.move_left(3)

    child()


def test_paramterizingstring_picklability():
    """Test pickle-ability of ParameterizingString."""
    @as_subprocess
    def child():
        from blessed.formatters import ParameterizingString
        t = TestTerminal(force_styling=True)

        color = ParameterizingString(t.color, t.normal, 'color')
        assert pickle.loads(pickle.dumps(color)) == color
        assert pickle.loads(pickle.dumps(color(3))) == color(3)
        assert pickle.loads(pickle.dumps(color))(3) == color(3)

        # and, pickle through multiprocessing
        r, w = multiprocessing.Pipe()
        w.send(color)
        assert r.recv() == color
        w.send(color(3))
        assert r.recv() == color(3)
        w.send(t.color)
        assert r.recv()(3) == t.color(3)

    child()


def test_pickled_parameterizing_string(monkeypatch):
    """Test pickle-ability of a formatters.ParameterizingString."""
    from blessed.formatters import ParameterizingString

    # simply send()/recv() over multiprocessing Pipe, a simple
    # pickle.loads(dumps(...)) did not reproduce this issue,

    # first argument to tparm() is the sequence name, returned as-is;
    # subsequent arguments are usually Integers.
    monkeypatch.setattr(curses, 'tparm', fn_tparm)

    # given,
    pstr = ParameterizingString(u'seqname', u'norm', u'cap-name')

    # multiprocessing Pipe implicitly pickles.
    r, w = multiprocessing.Pipe()

    # exercise picklability of ParameterizingString
    for proto_num in range(pickle.HIGHEST_PROTOCOL):
        assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num))
    w.send(pstr)
    assert r.recv() == pstr

    # exercise picklability of FormattingString
    # -- the return value of calling ParameterizingString
    zero = pstr(0)
    for proto_num in range(pickle.HIGHEST_PROTOCOL):
        assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num))
    w.send(zero)
    assert r.recv() == zero


def test_tparm_returns_null(monkeypatch):
    """Test 'tparm() returned NULL' is caught (win32 PDCurses systems)."""
    # on win32, any calls to tparm raises curses.error with message,
    # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c
    from blessed.formatters import ParameterizingString, NullCallableString

    def tparm(*args):
        raise curses.error("tparm() returned NULL")

    monkeypatch.setattr(curses, 'tparm', tparm)

    term = mock.Mock()
    term.normal = 'seq-normal'

    pstr = ParameterizingString(u'cap', u'norm', u'seq-name')

    value = pstr(0)
    assert isinstance(value, NullCallableString)


def test_tparm_other_exception(monkeypatch):
    """Test 'tparm() returned NULL' is caught (win32 PDCurses systems)."""
    # on win32, any calls to tparm raises curses.error with message,
    # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c
    from blessed.formatters import ParameterizingString

    def tparm(*args):
        raise curses.error("unexpected error in tparm()")

    monkeypatch.setattr(curses, 'tparm', tparm)

    term = mock.Mock()
    term.normal = 'seq-normal'

    pstr = ParameterizingString(u'cap', u'norm', u'seq-name')

    try:
        pstr(u'x')
        assert False, "previous call should have raised curses.error"
    except curses.error:
        pass