1import datetime as dt
2import io
3import random
4import sqlite3
5from math import e, pi, sqrt
6from typing import Any, List
7
8import pytest
9
10import prettytable
11from prettytable import (
12    ALL,
13    DEFAULT,
14    DOUBLE_BORDER,
15    FRAME,
16    HEADER,
17    MARKDOWN,
18    MSWORD_FRIENDLY,
19    NONE,
20    ORGMODE,
21    PLAIN_COLUMNS,
22    RANDOM,
23    PrettyTable,
24    from_csv,
25    from_db_cursor,
26    from_html,
27    from_html_one,
28    from_json,
29)
30
31
32def helper_table(rows=3):
33    t = PrettyTable(["Field 1", "Field 2", "Field 3"])
34    v = 1
35    for row in range(rows):
36        # Some have spaces, some not, to help test padding columns of different widths
37        t.add_row([f"value {v}", f"value{v+1}", f"value{v+2}"])
38        v += 3
39    return t
40
41
42@pytest.fixture
43def row_prettytable():
44    # Row by row...
45    row = PrettyTable()
46    row.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
47    row.add_row(["Adelaide", 1295, 1158259, 600.5])
48    row.add_row(["Brisbane", 5905, 1857594, 1146.4])
49    row.add_row(["Darwin", 112, 120900, 1714.7])
50    row.add_row(["Hobart", 1357, 205556, 619.5])
51    row.add_row(["Sydney", 2058, 4336374, 1214.8])
52    row.add_row(["Melbourne", 1566, 3806092, 646.9])
53    row.add_row(["Perth", 5386, 1554769, 869.4])
54    return row
55
56
57@pytest.fixture
58def col_prettytable():
59    # Column by column...
60    col = PrettyTable()
61    col.add_column(
62        "City name",
63        ["Adelaide", "Brisbane", "Darwin", "Hobart", "Sydney", "Melbourne", "Perth"],
64    )
65    col.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
66    col.add_column(
67        "Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769]
68    )
69    col.add_column(
70        "Annual Rainfall", [600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4]
71    )
72    return col
73
74
75@pytest.fixture
76def mix_prettytable():
77    # A mix of both!
78    mix = PrettyTable()
79    mix.field_names = ["City name", "Area"]
80    mix.add_row(["Adelaide", 1295])
81    mix.add_row(["Brisbane", 5905])
82    mix.add_row(["Darwin", 112])
83    mix.add_row(["Hobart", 1357])
84    mix.add_row(["Sydney", 2058])
85    mix.add_row(["Melbourne", 1566])
86    mix.add_row(["Perth", 5386])
87    mix.add_column(
88        "Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769]
89    )
90    mix.add_column(
91        "Annual Rainfall", [600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4]
92    )
93    return mix
94
95
96class TestBuildEquivalence:
97    """Make sure that building a table row-by-row and column-by-column yield the same
98    results"""
99
100    @pytest.mark.parametrize(
101        ["left_hand", "right_hand"],
102        [
103            (
104                pytest.lazy_fixture("row_prettytable"),
105                pytest.lazy_fixture("col_prettytable"),
106            ),
107            (
108                pytest.lazy_fixture("row_prettytable"),
109                pytest.lazy_fixture("mix_prettytable"),
110            ),
111        ],
112    )
113    def test_equivalence_ASCII(self, left_hand: PrettyTable, right_hand: PrettyTable):
114        assert left_hand.get_string() == right_hand.get_string()
115
116    @pytest.mark.parametrize(
117        ["left_hand", "right_hand"],
118        [
119            (
120                pytest.lazy_fixture("row_prettytable"),
121                pytest.lazy_fixture("col_prettytable"),
122            ),
123            (
124                pytest.lazy_fixture("row_prettytable"),
125                pytest.lazy_fixture("mix_prettytable"),
126            ),
127        ],
128    )
129    def test_equivalence_HTML(self, left_hand: PrettyTable, right_hand: PrettyTable):
130        assert left_hand.get_html_string() == right_hand.get_html_string()
131
132    @pytest.mark.parametrize(
133        ["left_hand", "right_hand"],
134        [
135            (
136                pytest.lazy_fixture("row_prettytable"),
137                pytest.lazy_fixture("col_prettytable"),
138            ),
139            (
140                pytest.lazy_fixture("row_prettytable"),
141                pytest.lazy_fixture("mix_prettytable"),
142            ),
143        ],
144    )
145    def test_equivalence_latex(self, left_hand: PrettyTable, right_hand: PrettyTable):
146        assert left_hand.get_latex_string() == right_hand.get_latex_string()
147
148
149class TestDeleteColumn:
150    def test_delete_column(self):
151        with_del = PrettyTable()
152        with_del.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
153        with_del.add_column("Area", [1295, 5905, 112])
154        with_del.add_column("Population", [1158259, 1857594, 120900])
155        with_del.del_column("Area")
156
157        without_row = PrettyTable()
158        without_row.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
159        without_row.add_column("Population", [1158259, 1857594, 120900])
160
161        assert with_del.get_string() == without_row.get_string()
162
163    def test_delete_illegal_column_raises_exception(self):
164        table = PrettyTable()
165        table.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
166
167        with pytest.raises(Exception):
168            table.del_column("City not-a-name")
169
170
171@pytest.fixture(scope="function")
172def field_name_less_table():
173    x = PrettyTable()
174    x.add_row(["Adelaide", 1295, 1158259, 600.5])
175    x.add_row(["Brisbane", 5905, 1857594, 1146.4])
176    x.add_row(["Darwin", 112, 120900, 1714.7])
177    x.add_row(["Hobart", 1357, 205556, 619.5])
178    x.add_row(["Sydney", 2058, 4336374, 1214.8])
179    x.add_row(["Melbourne", 1566, 3806092, 646.9])
180    x.add_row(["Perth", 5386, 1554769, 869.4])
181    return x
182
183
184class TestFieldNameLessTable:
185
186    """Make sure that building and stringing a table with no fieldnames works fine"""
187
188    def test_can_string_ASCII(self, field_name_less_table: prettytable):
189        output = field_name_less_table.get_string()
190        assert "|  Field 1  | Field 2 | Field 3 | Field 4 |" in output
191        assert "|  Adelaide |   1295  | 1158259 |  600.5  |" in output
192
193    def test_can_string_HTML(self, field_name_less_table: prettytable):
194        output = field_name_less_table.get_html_string()
195        assert "<th>Field 1</th>" in output
196        assert "<td>Adelaide</td>" in output
197
198    def test_can_string_latex(self, field_name_less_table: prettytable):
199        output = field_name_less_table.get_latex_string()
200        assert "Field 1 & Field 2 & Field 3 & Field 4 \\\\" in output
201        assert "Adelaide & 1295 & 1158259 & 600.5 \\\\" in output
202
203    def test_add_field_names_later(self, field_name_less_table: prettytable):
204        field_name_less_table.field_names = [
205            "City name",
206            "Area",
207            "Population",
208            "Annual Rainfall",
209        ]
210        assert (
211            "City name | Area | Population | Annual Rainfall"
212            in field_name_less_table.get_string()
213        )
214
215
216@pytest.fixture(scope="function")
217def city_data_prettytable():
218    """Just build the Australian capital city data example table."""
219    pt = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"])
220    pt.add_row(["Adelaide", 1295, 1158259, 600.5])
221    pt.add_row(["Brisbane", 5905, 1857594, 1146.4])
222    pt.add_row(["Darwin", 112, 120900, 1714.7])
223    pt.add_row(["Hobart", 1357, 205556, 619.5])
224    pt.add_row(["Sydney", 2058, 4336374, 1214.8])
225    pt.add_row(["Melbourne", 1566, 3806092, 646.9])
226    pt.add_row(["Perth", 5386, 1554769, 869.4])
227    return pt
228
229
230@pytest.fixture(scope="function")
231def city_data_from_csv():
232    csv_string = """City name, Area, Population, Annual Rainfall
233    Sydney, 2058, 4336374, 1214.8
234    Melbourne, 1566, 3806092, 646.9
235    Brisbane, 5905, 1857594, 1146.4
236    Perth, 5386, 1554769, 869.4
237    Adelaide, 1295, 1158259, 600.5
238    Hobart, 1357, 205556, 619.5
239    Darwin, 0112, 120900, 1714.7"""
240    csv_fp = io.StringIO(csv_string)
241    return from_csv(csv_fp)
242
243
244class TestOptionOverride:
245
246    """Make sure all options are properly overwritten by get_string."""
247
248    def test_border(self, city_data_prettytable: prettytable):
249        default = city_data_prettytable.get_string()
250        override = city_data_prettytable.get_string(border=False)
251        assert default != override
252
253    def test_header(self, city_data_prettytable):
254        default = city_data_prettytable.get_string()
255        override = city_data_prettytable.get_string(header=False)
256        assert default != override
257
258    def test_hrules_all(self, city_data_prettytable):
259        default = city_data_prettytable.get_string()
260        override = city_data_prettytable.get_string(hrules=ALL)
261        assert default != override
262
263    def test_hrules_none(self, city_data_prettytable):
264        default = city_data_prettytable.get_string()
265        override = city_data_prettytable.get_string(hrules=NONE)
266        assert default != override
267
268
269class TestOptionAttribute:
270
271    """Make sure all options which have an attribute interface work as they should.
272    Also make sure option settings are copied correctly when a table is cloned by
273    slicing."""
274
275    def test_set_for_all_columns(self, city_data_prettytable):
276        city_data_prettytable.field_names = sorted(city_data_prettytable.field_names)
277        city_data_prettytable.align = "l"
278        city_data_prettytable.max_width = 10
279        city_data_prettytable.start = 2
280        city_data_prettytable.end = 4
281        city_data_prettytable.sortby = "Area"
282        city_data_prettytable.reversesort = True
283        city_data_prettytable.header = True
284        city_data_prettytable.border = False
285        city_data_prettytable.hrules = True
286        city_data_prettytable.int_format = "4"
287        city_data_prettytable.float_format = "2.2"
288        city_data_prettytable.padding_width = 2
289        city_data_prettytable.left_padding_width = 2
290        city_data_prettytable.right_padding_width = 2
291        city_data_prettytable.vertical_char = "!"
292        city_data_prettytable.horizontal_char = "~"
293        city_data_prettytable.junction_char = "*"
294        city_data_prettytable.top_junction_char = "@"
295        city_data_prettytable.bottom_junction_char = "#"
296        city_data_prettytable.right_junction_char = "$"
297        city_data_prettytable.left_junction_char = "%"
298        city_data_prettytable.top_right_junction_char = "^"
299        city_data_prettytable.top_left_junction_char = "&"
300        city_data_prettytable.bottom_right_junction_char = "("
301        city_data_prettytable.bottom_left_junction_char = ")"
302        city_data_prettytable.format = True
303        city_data_prettytable.attributes = {"class": "prettytable"}
304        assert (
305            city_data_prettytable.get_string() == city_data_prettytable[:].get_string()
306        )
307
308    def test_set_for_one_column(self, city_data_prettytable):
309        city_data_prettytable.align["Rainfall"] = "l"
310        city_data_prettytable.max_width["Name"] = 10
311        city_data_prettytable.int_format["Population"] = "4"
312        city_data_prettytable.float_format["Area"] = "2.2"
313        assert (
314            city_data_prettytable.get_string() == city_data_prettytable[:].get_string()
315        )
316
317
318@pytest.fixture(scope="module")
319def db_cursor():
320    conn = sqlite3.connect(":memory:")
321    cur = conn.cursor()
322    yield cur
323    cur.close()
324    conn.close()
325
326
327@pytest.fixture(scope="module")
328def init_db(db_cursor):
329    db_cursor.execute(
330        "CREATE TABLE cities "
331        "(name TEXT, area INTEGER, population INTEGER, rainfall REAL)"
332    )
333    db_cursor.execute('INSERT INTO cities VALUES ("Adelaide", 1295, 1158259, 600.5)')
334    db_cursor.execute('INSERT INTO cities VALUES ("Brisbane", 5905, 1857594, 1146.4)')
335    db_cursor.execute('INSERT INTO cities VALUES ("Darwin", 112, 120900, 1714.7)')
336    db_cursor.execute('INSERT INTO cities VALUES ("Hobart", 1357, 205556, 619.5)')
337    db_cursor.execute('INSERT INTO cities VALUES ("Sydney", 2058, 4336374, 1214.8)')
338    db_cursor.execute('INSERT INTO cities VALUES ("Melbourne", 1566, 3806092, 646.9)')
339    db_cursor.execute('INSERT INTO cities VALUES ("Perth", 5386, 1554769, 869.4)')
340    yield
341    db_cursor.execute("DROP TABLE cities")
342
343
344class TestBasic:
345    """Some very basic tests."""
346
347    def test_table_rows(self, city_data_prettytable: PrettyTable) -> None:
348        rows = city_data_prettytable.rows
349        assert len(rows) == 7
350        assert rows[0] == ["Adelaide", 1295, 1158259, 600.5]
351
352    def _test_no_blank_lines(self, table: prettytable):
353        string = table.get_string()
354        lines = string.split("\n")
355        assert "" not in lines
356
357    def _test_all_length_equal(self, table: prettytable):
358        string = table.get_string()
359        lines = string.split("\n")
360        lengths = [len(line) for line in lines]
361        lengths = set(lengths)
362        assert len(lengths) == 1
363
364    def test_no_blank_lines(self, city_data_prettytable):
365        """No table should ever have blank lines in it."""
366        self._test_no_blank_lines(city_data_prettytable)
367
368    def test_all_lengths_equal(self, city_data_prettytable):
369        """All lines in a table should be of the same length."""
370        self._test_all_length_equal(city_data_prettytable)
371
372    def test_no_blank_lines_with_title(self, city_data_prettytable: PrettyTable):
373        """No table should ever have blank lines in it."""
374        city_data_prettytable.title = "My table"
375        self._test_no_blank_lines(city_data_prettytable)
376
377    def test_all_lengths_equal_with_title(self, city_data_prettytable: PrettyTable):
378        """All lines in a table should be of the same length."""
379        city_data_prettytable.title = "My table"
380        self._test_all_length_equal(city_data_prettytable)
381
382    def test_no_blank_lines_without_border(self, city_data_prettytable: PrettyTable):
383        """No table should ever have blank lines in it."""
384        city_data_prettytable.border = False
385        self._test_no_blank_lines(city_data_prettytable)
386
387    def test_all_lengths_equal_without_border(self, city_data_prettytable: PrettyTable):
388        """All lines in a table should be of the same length."""
389        city_data_prettytable.border = False
390        self._test_all_length_equal(city_data_prettytable)
391
392    def test_no_blank_lines_without_header(self, city_data_prettytable: PrettyTable):
393        """No table should ever have blank lines in it."""
394        city_data_prettytable.header = False
395        self._test_no_blank_lines(city_data_prettytable)
396
397    def test_all_lengths_equal_without_header(self, city_data_prettytable: PrettyTable):
398        """All lines in a table should be of the same length."""
399        city_data_prettytable.header = False
400        self._test_all_length_equal(city_data_prettytable)
401
402    def test_no_blank_lines_with_hrules_none(self, city_data_prettytable: PrettyTable):
403        """No table should ever have blank lines in it."""
404        city_data_prettytable.hrules = NONE
405        self._test_no_blank_lines(city_data_prettytable)
406
407    def test_all_lengths_equal_with_hrules_none(
408        self, city_data_prettytable: PrettyTable
409    ):
410        """All lines in a table should be of the same length."""
411        city_data_prettytable.hrules = NONE
412        self._test_all_length_equal(city_data_prettytable)
413
414    def test_no_blank_lines_with_hrules_all(self, city_data_prettytable: PrettyTable):
415        """No table should ever have blank lines in it."""
416        city_data_prettytable.hrules = ALL
417        self._test_no_blank_lines(city_data_prettytable)
418
419    def test_all_lengths_equal_with_hrules_all(
420        self, city_data_prettytable: PrettyTable
421    ):
422        """All lines in a table should be of the same length."""
423        city_data_prettytable.hrules = ALL
424        self._test_all_length_equal(city_data_prettytable)
425
426    def test_no_blank_lines_with_style_msword(self, city_data_prettytable: PrettyTable):
427        """No table should ever have blank lines in it."""
428        city_data_prettytable.set_style(MSWORD_FRIENDLY)
429        self._test_no_blank_lines(city_data_prettytable)
430
431    def test_all_lengths_equal_with_style_msword(
432        self, city_data_prettytable: PrettyTable
433    ):
434        """All lines in a table should be of the same length."""
435        city_data_prettytable.set_style(MSWORD_FRIENDLY)
436        self._test_all_length_equal(city_data_prettytable)
437
438    def test_no_blank_lines_with_int_format(self, city_data_prettytable: PrettyTable):
439        """No table should ever have blank lines in it."""
440        city_data_prettytable.int_format = "04"
441        self._test_no_blank_lines(city_data_prettytable)
442
443    def test_all_lengths_equal_with_int_format(
444        self, city_data_prettytable: PrettyTable
445    ):
446        """All lines in a table should be of the same length."""
447        city_data_prettytable.int_format = "04"
448        self._test_all_length_equal(city_data_prettytable)
449
450    def test_no_blank_lines_with_float_format(self, city_data_prettytable: PrettyTable):
451        """No table should ever have blank lines in it."""
452        city_data_prettytable.float_format = "6.2f"
453        self._test_no_blank_lines(city_data_prettytable)
454
455    def test_all_lengths_equal_with_float_format(
456        self, city_data_prettytable: PrettyTable
457    ):
458        """All lines in a table should be of the same length."""
459        city_data_prettytable.float_format = "6.2f"
460        self._test_all_length_equal(city_data_prettytable)
461
462    def test_no_blank_lines_from_csv(self, city_data_from_csv: PrettyTable):
463        """No table should ever have blank lines in it."""
464        self._test_no_blank_lines(city_data_from_csv)
465
466    def test_all_lengths_equal_from_csv(self, city_data_from_csv: PrettyTable):
467        """All lines in a table should be of the same length."""
468        self._test_all_length_equal(city_data_from_csv)
469
470    @pytest.mark.usefixtures("init_db")
471    def test_no_blank_lines_from_db(self, db_cursor):
472        """No table should ever have blank lines in it."""
473        db_cursor.execute("SELECT * FROM cities")
474        pt = from_db_cursor(db_cursor)
475        self._test_no_blank_lines(pt)
476
477    @pytest.mark.usefixtures("init_db")
478    def test_all_lengths_equal_from_db(self, db_cursor):
479        """No table should ever have blank lines in it."""
480        db_cursor.execute("SELECT * FROM cities")
481        pt = from_db_cursor(db_cursor)
482        self._test_all_length_equal(pt)
483
484
485class TestEmptyTable:
486    """Make sure the print_empty option works"""
487
488    def test_print_empty_true(self, city_data_prettytable: PrettyTable):
489        empty_pt = PrettyTable()
490        empty_pt.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
491
492        assert empty_pt.get_string(print_empty=True) != ""
493        assert empty_pt.get_string(
494            print_empty=True
495        ) != city_data_prettytable.get_string(print_empty=True)
496
497    def test_print_empty_false(self, city_data_prettytable: PrettyTable):
498        empty_pt = PrettyTable()
499        empty_pt.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
500
501        assert empty_pt.get_string(print_empty=False) == ""
502        assert empty_pt.get_string(
503            print_empty=False
504        ) != city_data_prettytable.get_string(print_empty=False)
505
506    def test_interaction_with_border(self):
507        empty_pt = PrettyTable()
508        empty_pt.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
509
510        assert empty_pt.get_string(border=False, print_empty=True) == ""
511
512
513class TestSlicing:
514    def test_slice_all(self, city_data_prettytable: PrettyTable):
515        sliced_pt = city_data_prettytable[:]
516        assert city_data_prettytable.get_string() == sliced_pt.get_string()
517
518    def test_slice_first_two_rows(self, city_data_prettytable: PrettyTable):
519        sliced_pt = city_data_prettytable[0:2]
520        string = sliced_pt.get_string()
521        assert len(string.split("\n")) == 6
522        assert "Adelaide" in string
523        assert "Brisbane" in string
524        assert "Melbourne" not in string
525        assert "Perth" not in string
526
527    def test_slice_last_two_rows(self, city_data_prettytable: PrettyTable):
528        sliced_pt = city_data_prettytable[-2:]
529        string = sliced_pt.get_string()
530        assert len(string.split("\n")) == 6
531        assert "Adelaide" not in string
532        assert "Brisbane" not in string
533        assert "Melbourne" in string
534        assert "Perth" in string
535
536
537class TestSorting:
538    def test_sort_by_different_per_columns(self, city_data_prettytable: PrettyTable):
539        city_data_prettytable.sortby = city_data_prettytable.field_names[0]
540        old = city_data_prettytable.get_string()
541        for field in city_data_prettytable.field_names[1:]:
542            city_data_prettytable.sortby = field
543            new = city_data_prettytable.get_string()
544            assert new != old
545
546    def test_reverse_sort(self, city_data_prettytable: PrettyTable):
547        for field in city_data_prettytable.field_names:
548            city_data_prettytable.sortby = field
549            city_data_prettytable.reversesort = False
550            forward = city_data_prettytable.get_string()
551            city_data_prettytable.reversesort = True
552            backward = city_data_prettytable.get_string()
553            forward_lines = forward.split("\n")[2:]  # Discard header lines
554            backward_lines = backward.split("\n")[2:]
555            backward_lines.reverse()
556            assert forward_lines == backward_lines
557
558    def test_sort_key(self, city_data_prettytable: PrettyTable):
559        # Test sorting by length of city name
560        def key(vals):
561            vals[0] = len(vals[0])
562            return vals
563
564        city_data_prettytable.sortby = "City name"
565        city_data_prettytable.sort_key = key
566        assert (
567            city_data_prettytable.get_string().strip()
568            == """
569+-----------+------+------------+-----------------+
570| City name | Area | Population | Annual Rainfall |
571+-----------+------+------------+-----------------+
572|   Perth   | 5386 |  1554769   |      869.4      |
573|   Darwin  | 112  |   120900   |      1714.7     |
574|   Hobart  | 1357 |   205556   |      619.5      |
575|   Sydney  | 2058 |  4336374   |      1214.8     |
576|  Adelaide | 1295 |  1158259   |      600.5      |
577|  Brisbane | 5905 |  1857594   |      1146.4     |
578| Melbourne | 1566 |  3806092   |      646.9      |
579+-----------+------+------------+-----------------+
580""".strip()
581        )
582
583    def test_sort_slice(self):
584        """Make sure sorting and slicing interact in the expected way"""
585        x = PrettyTable(["Foo"])
586        for i in range(20, 0, -1):
587            x.add_row([i])
588        new_style = x.get_string(sortby="Foo", end=10)
589        assert "10" in new_style
590        assert "20" not in new_style
591        oldstyle = x.get_string(sortby="Foo", end=10, oldsortslice=True)
592        assert "10" not in oldstyle
593        assert "20" in oldstyle
594
595
596@pytest.fixture(scope="function")
597def float_pt():
598    pt = PrettyTable(["Constant", "Value"])
599    pt.add_row(["Pi", pi])
600    pt.add_row(["e", e])
601    pt.add_row(["sqrt(2)", sqrt(2)])
602    return pt
603
604
605class TestFloatFormat:
606    def test_no_decimals(self, float_pt: PrettyTable):
607        float_pt.float_format = ".0f"
608        float_pt.caching = False
609        assert "." not in float_pt.get_string()
610
611    def test_round_to_5DP(self, float_pt: PrettyTable):
612        float_pt.float_format = ".5f"
613        string = float_pt.get_string()
614        assert "3.14159" in string
615        assert "3.141592" not in string
616        assert "2.71828" in string
617        assert "2.718281" not in string
618        assert "2.718282" not in string
619        assert "1.41421" in string
620        assert "1.414213" not in string
621
622    def test_pad_with_2Zeroes(self, float_pt: PrettyTable):
623        float_pt.float_format = "06.2f"
624        string = float_pt.get_string()
625        assert "003.14" in string
626        assert "002.72" in string
627        assert "001.41" in string
628
629
630class TestBreakLine:
631    @pytest.mark.parametrize(
632        ["rows", "hrule", "expected_result"],
633        [
634            (
635                [["value 1", "value2\nsecond line"], ["value 3", "value4"]],
636                ALL,
637                """
638+---------+-------------+
639| Field 1 |   Field 2   |
640+---------+-------------+
641| value 1 |    value2   |
642|         | second line |
643+---------+-------------+
644| value 3 |    value4   |
645+---------+-------------+
646""",
647            ),
648            (
649                [
650                    ["value 1", "value2\nsecond line"],
651                    ["value 3\n\nother line", "value4\n\n\nvalue5"],
652                ],
653                ALL,
654                """
655+------------+-------------+
656|  Field 1   |   Field 2   |
657+------------+-------------+
658|  value 1   |    value2   |
659|            | second line |
660+------------+-------------+
661|  value 3   |    value4   |
662|            |             |
663| other line |             |
664|            |    value5   |
665+------------+-------------+
666""",
667            ),
668            (
669                [
670                    ["value 1", "value2\nsecond line"],
671                    ["value 3\n\nother line", "value4\n\n\nvalue5"],
672                ],
673                FRAME,
674                """
675+------------+-------------+
676|  Field 1   |   Field 2   |
677+------------+-------------+
678|  value 1   |    value2   |
679|            | second line |
680|  value 3   |    value4   |
681|            |             |
682| other line |             |
683|            |    value5   |
684+------------+-------------+
685""",
686            ),
687        ],
688    )
689    def test_break_line_ASCII(
690        self, rows: List[List[Any]], hrule: int, expected_result: str
691    ):
692        t = PrettyTable(["Field 1", "Field 2"])
693        for row in rows:
694            t.add_row(row)
695        result = t.get_string(hrules=hrule)
696        assert result.strip() == expected_result.strip()
697
698    def test_break_line_HTML(self):
699        t = PrettyTable(["Field 1", "Field 2"])
700        t.add_row(["value 1", "value2\nsecond line"])
701        t.add_row(["value 3", "value4"])
702        result = t.get_html_string(hrules=ALL)
703        assert (
704            result.strip()
705            == """
706<table>
707    <thead>
708        <tr>
709            <th>Field 1</th>
710            <th>Field 2</th>
711        </tr>
712    </thead>
713    <tbody>
714        <tr>
715            <td>value 1</td>
716            <td>value2<br>second line</td>
717        </tr>
718        <tr>
719            <td>value 3</td>
720            <td>value4</td>
721        </tr>
722    </tbody>
723</table>
724""".strip()
725        )
726
727
728class TestAnsiWidth:
729    colored = "\033[31mC\033[32mO\033[31mL\033[32mO\033[31mR\033[32mE\033[31mD\033[0m"
730
731    def test_color(self):
732        t = PrettyTable(["Field 1", "Field 2"])
733        t.add_row([self.colored, self.colored])
734        t.add_row(["nothing", "neither"])
735        result = t.get_string()
736        assert (
737            result.strip()
738            == f"""
739+---------+---------+
740| Field 1 | Field 2 |
741+---------+---------+
742| {self.colored} | {self.colored} |
743| nothing | neither |
744+---------+---------+
745""".strip()
746        )
747
748    def test_reset(self):
749        t = PrettyTable(["Field 1", "Field 2"])
750        t.add_row(["abc def\033(B", "\033[31mabc def\033[m"])
751        t.add_row(["nothing", "neither"])
752        result = t.get_string()
753        assert (
754            result.strip()
755            == """
756+---------+---------+
757| Field 1 | Field 2 |
758+---------+---------+
759| abc def\033(B | \033[31mabc def\033[m |
760| nothing | neither |
761+---------+---------+
762""".strip()
763        )
764
765
766class TestFromDB:
767    @pytest.mark.usefixtures("init_db")
768    def test_non_select_cursor(self, db_cursor):
769        db_cursor.execute(
770            'INSERT INTO cities VALUES ("Adelaide", 1295, 1158259, 600.5)'
771        )
772        assert from_db_cursor(db_cursor) is None
773
774
775class TestJSONOutput:
776    def test_JSON_output(self):
777        t = helper_table()
778        result = t.get_json_string()
779        assert (
780            result.strip()
781            == """
782[
783    [
784        "Field 1",
785        "Field 2",
786        "Field 3"
787    ],
788    {
789        "Field 1": "value 1",
790        "Field 2": "value2",
791        "Field 3": "value3"
792    },
793    {
794        "Field 1": "value 4",
795        "Field 2": "value5",
796        "Field 3": "value6"
797    },
798    {
799        "Field 1": "value 7",
800        "Field 2": "value8",
801        "Field 3": "value9"
802    }
803]""".strip()
804        )
805
806    def test_JSON_output_options(self):
807        t = helper_table()
808        result = t.get_json_string(header=False, indent=None, separators=(",", ":"))
809        assert (
810            result
811            == """[{"Field 1":"value 1","Field 2":"value2","Field 3":"value3"},"""
812            """{"Field 1":"value 4","Field 2":"value5","Field 3":"value6"},"""
813            """{"Field 1":"value 7","Field 2":"value8","Field 3":"value9"}]"""
814        )
815
816
817class TestHtmlOutput:
818    def test_HtmlOutput(self):
819        t = helper_table()
820        result = t.get_html_string()
821        assert (
822            result.strip()
823            == """
824<table>
825    <thead>
826        <tr>
827            <th>Field 1</th>
828            <th>Field 2</th>
829            <th>Field 3</th>
830        </tr>
831    </thead>
832    <tbody>
833        <tr>
834            <td>value 1</td>
835            <td>value2</td>
836            <td>value3</td>
837        </tr>
838        <tr>
839            <td>value 4</td>
840            <td>value5</td>
841            <td>value6</td>
842        </tr>
843        <tr>
844            <td>value 7</td>
845            <td>value8</td>
846            <td>value9</td>
847        </tr>
848    </tbody>
849</table>
850""".strip()
851        )
852
853    def test_HtmlOutputFormatted(self):
854        t = helper_table()
855        result = t.get_html_string(format=True)
856        assert (
857            result.strip()
858            == """
859<table frame="box" rules="cols">
860    <thead>
861        <tr>
862            <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 1</th>
863            <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 2</th>
864            <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 3</th>
865        </tr>
866    </thead>
867    <tbody>
868        <tr>
869            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 1</td>
870            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value2</td>
871            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value3</td>
872        </tr>
873        <tr>
874            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 4</td>
875            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value5</td>
876            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value6</td>
877        </tr>
878        <tr>
879            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 7</td>
880            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value8</td>
881            <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value9</td>
882        </tr>
883    </tbody>
884</table>
885""".strip()  # noqa: E501
886        )
887
888
889class TestPositionalJunctions:
890    """Verify different cases for positional-junction characters"""
891
892    def test_Default(self, city_data_prettytable: PrettyTable):
893        city_data_prettytable.set_style(DOUBLE_BORDER)
894
895        assert (
896            city_data_prettytable.get_string().strip()
897            == """
898╔═══════════╦══════╦════════════╦═════════════════╗
899║ City name ║ Area ║ Population ║ Annual Rainfall ║
900╠═══════════╬══════╬════════════╬═════════════════╣
901║  Adelaide ║ 1295 ║  1158259   ║      600.5      ║
902║  Brisbane ║ 5905 ║  1857594   ║      1146.4     ║
903║   Darwin  ║ 112  ║   120900   ║      1714.7     ║
904║   Hobart  ║ 1357 ║   205556   ║      619.5      ║
905║   Sydney  ║ 2058 ║  4336374   ║      1214.8     ║
906║ Melbourne ║ 1566 ║  3806092   ║      646.9      ║
907║   Perth   ║ 5386 ║  1554769   ║      869.4      ║
908╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
909        )
910
911    def test_NoHeader(self, city_data_prettytable: PrettyTable):
912        city_data_prettytable.set_style(DOUBLE_BORDER)
913        city_data_prettytable.header = False
914
915        assert (
916            city_data_prettytable.get_string().strip()
917            == """
918╔═══════════╦══════╦═════════╦════════╗
919║  Adelaide ║ 1295 ║ 1158259 ║ 600.5  ║
920║  Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
921║   Darwin  ║ 112  ║  120900 ║ 1714.7 ║
922║   Hobart  ║ 1357 ║  205556 ║ 619.5  ║
923║   Sydney  ║ 2058 ║ 4336374 ║ 1214.8 ║
924║ Melbourne ║ 1566 ║ 3806092 ║ 646.9  ║
925║   Perth   ║ 5386 ║ 1554769 ║ 869.4  ║
926╚═══════════╩══════╩═════════╩════════╝""".strip()
927        )
928
929    def test_WithTitle(self, city_data_prettytable: PrettyTable):
930        city_data_prettytable.set_style(DOUBLE_BORDER)
931        city_data_prettytable.title = "Title"
932
933        assert (
934            city_data_prettytable.get_string().strip()
935            == """
936╔═════════════════════════════════════════════════╗
937║                      Title                      ║
938╠═══════════╦══════╦════════════╦═════════════════╣
939║ City name ║ Area ║ Population ║ Annual Rainfall ║
940╠═══════════╬══════╬════════════╬═════════════════╣
941║  Adelaide ║ 1295 ║  1158259   ║      600.5      ║
942║  Brisbane ║ 5905 ║  1857594   ║      1146.4     ║
943║   Darwin  ║ 112  ║   120900   ║      1714.7     ║
944║   Hobart  ║ 1357 ║   205556   ║      619.5      ║
945║   Sydney  ║ 2058 ║  4336374   ║      1214.8     ║
946║ Melbourne ║ 1566 ║  3806092   ║      646.9      ║
947║   Perth   ║ 5386 ║  1554769   ║      869.4      ║
948╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
949        )
950
951    def test_WithTitleNoHeader(self, city_data_prettytable: PrettyTable):
952        city_data_prettytable.set_style(DOUBLE_BORDER)
953        city_data_prettytable.title = "Title"
954        city_data_prettytable.header = False
955        assert (
956            city_data_prettytable.get_string().strip()
957            == """
958╔═════════════════════════════════════╗
959║                Title                ║
960╠═══════════╦══════╦═════════╦════════╣
961║  Adelaide ║ 1295 ║ 1158259 ║ 600.5  ║
962║  Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
963║   Darwin  ║ 112  ║  120900 ║ 1714.7 ║
964║   Hobart  ║ 1357 ║  205556 ║ 619.5  ║
965║   Sydney  ║ 2058 ║ 4336374 ║ 1214.8 ║
966║ Melbourne ║ 1566 ║ 3806092 ║ 646.9  ║
967║   Perth   ║ 5386 ║ 1554769 ║ 869.4  ║
968╚═══════════╩══════╩═════════╩════════╝""".strip()
969        )
970
971    def test_HruleAll(self, city_data_prettytable: PrettyTable):
972        city_data_prettytable.set_style(DOUBLE_BORDER)
973        city_data_prettytable.title = "Title"
974        city_data_prettytable.hrules = ALL
975        assert (
976            city_data_prettytable.get_string().strip()
977            == """
978╔═════════════════════════════════════════════════╗
979║                      Title                      ║
980╠═══════════╦══════╦════════════╦═════════════════╣
981║ City name ║ Area ║ Population ║ Annual Rainfall ║
982╠═══════════╬══════╬════════════╬═════════════════╣
983║  Adelaide ║ 1295 ║  1158259   ║      600.5      ║
984╠═══════════╬══════╬════════════╬═════════════════╣
985║  Brisbane ║ 5905 ║  1857594   ║      1146.4     ║
986╠═══════════╬══════╬════════════╬═════════════════╣
987║   Darwin  ║ 112  ║   120900   ║      1714.7     ║
988╠═══════════╬══════╬════════════╬═════════════════╣
989║   Hobart  ║ 1357 ║   205556   ║      619.5      ║
990╠═══════════╬══════╬════════════╬═════════════════╣
991║   Sydney  ║ 2058 ║  4336374   ║      1214.8     ║
992╠═══════════╬══════╬════════════╬═════════════════╣
993║ Melbourne ║ 1566 ║  3806092   ║      646.9      ║
994╠═══════════╬══════╬════════════╬═════════════════╣
995║   Perth   ║ 5386 ║  1554769   ║      869.4      ║
996╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
997        )
998
999    def test_VrulesNone(self, city_data_prettytable: PrettyTable):
1000        city_data_prettytable.set_style(DOUBLE_BORDER)
1001        city_data_prettytable.vrules = NONE
1002        assert (
1003            city_data_prettytable.get_string().strip()
1004            == "═══════════════════════════════════════════════════\n"
1005            "  City name   Area   Population   Annual Rainfall  \n"
1006            "═══════════════════════════════════════════════════\n"
1007            "   Adelaide   1295    1158259          600.5       \n"
1008            "   Brisbane   5905    1857594          1146.4      \n"
1009            "    Darwin    112      120900          1714.7      \n"
1010            "    Hobart    1357     205556          619.5       \n"
1011            "    Sydney    2058    4336374          1214.8      \n"
1012            "  Melbourne   1566    3806092          646.9       \n"
1013            "    Perth     5386    1554769          869.4       \n"
1014            "═══════════════════════════════════════════════════".strip()
1015        )
1016
1017    def test_VrulesFrameWithTitle(self, city_data_prettytable: PrettyTable):
1018        city_data_prettytable.set_style(DOUBLE_BORDER)
1019        city_data_prettytable.vrules = FRAME
1020        city_data_prettytable.title = "Title"
1021        assert (
1022            city_data_prettytable.get_string().strip()
1023            == """
1024╔═════════════════════════════════════════════════╗
1025║                      Title                      ║
1026╠═════════════════════════════════════════════════╣
1027║ City name   Area   Population   Annual Rainfall ║
1028╠═════════════════════════════════════════════════╣
1029║  Adelaide   1295    1158259          600.5      ║
1030║  Brisbane   5905    1857594          1146.4     ║
1031║   Darwin    112      120900          1714.7     ║
1032║   Hobart    1357     205556          619.5      ║
1033║   Sydney    2058    4336374          1214.8     ║
1034║ Melbourne   1566    3806092          646.9      ║
1035║   Perth     5386    1554769          869.4      ║
1036╚═════════════════════════════════════════════════╝""".strip()
1037        )
1038
1039
1040class TestStyle:
1041    @pytest.mark.parametrize(
1042        "style, expected",
1043        [
1044            pytest.param(
1045                DEFAULT,
1046                """
1047+---------+---------+---------+
1048| Field 1 | Field 2 | Field 3 |
1049+---------+---------+---------+
1050| value 1 |  value2 |  value3 |
1051| value 4 |  value5 |  value6 |
1052| value 7 |  value8 |  value9 |
1053+---------+---------+---------+
1054""",
1055                id="DEFAULT",
1056            ),
1057            pytest.param(
1058                MARKDOWN,
1059                """
1060| Field 1 | Field 2 | Field 3 |
1061|:-------:|:-------:|:-------:|
1062| value 1 |  value2 |  value3 |
1063| value 4 |  value5 |  value6 |
1064| value 7 |  value8 |  value9 |
1065""",
1066                id="MARKDOWN",
1067            ),
1068            pytest.param(
1069                MSWORD_FRIENDLY,
1070                """
1071| Field 1 | Field 2 | Field 3 |
1072| value 1 |  value2 |  value3 |
1073| value 4 |  value5 |  value6 |
1074| value 7 |  value8 |  value9 |
1075""",
1076                id="MSWORD_FRIENDLY",
1077            ),
1078            pytest.param(
1079                ORGMODE,
1080                """
1081|---------+---------+---------|
1082| Field 1 | Field 2 | Field 3 |
1083|---------+---------+---------|
1084| value 1 |  value2 |  value3 |
1085| value 4 |  value5 |  value6 |
1086| value 7 |  value8 |  value9 |
1087|---------+---------+---------|
1088""",
1089                id="ORGMODE",
1090            ),
1091            pytest.param(
1092                PLAIN_COLUMNS,
1093                """
1094Field 1        Field 2        Field 3
1095value 1         value2         value3
1096value 4         value5         value6
1097value 7         value8         value9
1098""",  # noqa: W291
1099                id="PLAIN_COLUMNS",
1100            ),
1101            pytest.param(
1102                RANDOM,
1103                """
1104'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
1105%    value 1%    value2%    value3%
1106'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
1107%    value 4%    value5%    value6%
1108'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
1109%    value 7%    value8%    value9%
1110'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
1111""",
1112                id="RANDOM",
1113            ),
1114            pytest.param(
1115                DOUBLE_BORDER,
1116                """
1117╔═════════╦═════════╦═════════╗
1118║ Field 1 ║ Field 2 ║ Field 3 ║
1119╠═════════╬═════════╬═════════╣
1120║ value 1 ║  value2 ║  value3 ║
1121║ value 4 ║  value5 ║  value6 ║
1122║ value 7 ║  value8 ║  value9 ║
1123╚═════════╩═════════╩═════════╝
1124""",
1125            ),
1126        ],
1127    )
1128    def test_style(self, style, expected):
1129        # Arrange
1130        t = helper_table()
1131        random.seed(1234)
1132
1133        # Act
1134        t.set_style(style)
1135
1136        # Assert
1137        result = t.get_string()
1138        assert result.strip() == expected.strip()
1139
1140    def test_style_invalid(self):
1141        # Arrange
1142        t = helper_table()
1143
1144        # Act / Assert
1145        # This is an hrule style, not a table style
1146        with pytest.raises(Exception):
1147            t.set_style(ALL)
1148
1149    @pytest.mark.parametrize(
1150        "style, expected",
1151        [
1152            pytest.param(
1153                MARKDOWN,
1154                """
1155| Align left | Align centre | Align right |
1156|:-----------|:------------:|------------:|
1157| value 1    |    value2    |      value3 |
1158| value 4    |    value5    |      value6 |
1159| value 7    |    value8    |      value9 |
1160""",
1161                id="MARKDOWN",
1162            ),
1163        ],
1164    )
1165    def test_style_align(self, style, expected):
1166        # Arrange
1167        t = helper_table()
1168        t.field_names = ["Align left", "Align centre", "Align right"]
1169
1170        # Act
1171        t.set_style(style)
1172        t.align["Align left"] = "l"
1173        t.align["Align centre"] = "c"
1174        t.align["Align right"] = "r"
1175
1176        # Assert
1177        result = t.get_string()
1178        assert result.strip() == expected.strip()
1179
1180
1181class TestCsvOutput:
1182    def test_csv_output(self):
1183        t = helper_table()
1184        assert t.get_csv_string(delimiter="\t", header=False) == (
1185            "value 1\tvalue2\tvalue3\r\n"
1186            "value 4\tvalue5\tvalue6\r\n"
1187            "value 7\tvalue8\tvalue9\r\n"
1188        )
1189        assert t.get_csv_string() == (
1190            "Field 1,Field 2,Field 3\r\n"
1191            "value 1,value2,value3\r\n"
1192            "value 4,value5,value6\r\n"
1193            "value 7,value8,value9\r\n"
1194        )
1195
1196
1197class TestLatexOutput:
1198    def testLatexOutput(self):
1199        t = helper_table()
1200        assert t.get_latex_string() == (
1201            "\\begin{tabular}{ccc}\r\n"
1202            "Field 1 & Field 2 & Field 3 \\\\\r\n"
1203            "value 1 & value2 & value3 \\\\\r\n"
1204            "value 4 & value5 & value6 \\\\\r\n"
1205            "value 7 & value8 & value9 \\\\\r\n"
1206            "\\end{tabular}"
1207        )
1208        options = {"fields": ["Field 1", "Field 3"]}
1209        assert t.get_latex_string(**options) == (
1210            "\\begin{tabular}{cc}\r\n"
1211            "Field 1 & Field 3 \\\\\r\n"
1212            "value 1 & value3 \\\\\r\n"
1213            "value 4 & value6 \\\\\r\n"
1214            "value 7 & value9 \\\\\r\n"
1215            "\\end{tabular}"
1216        )
1217
1218    def testLatexOutputFormatted(self):
1219        t = helper_table()
1220        assert t.get_latex_string(format=True) == (
1221            "\\begin{tabular}{|c|c|c|}\r\n"
1222            "\\hline\r\n"
1223            "Field 1 & Field 2 & Field 3 \\\\\r\n"
1224            "value 1 & value2 & value3 \\\\\r\n"
1225            "value 4 & value5 & value6 \\\\\r\n"
1226            "value 7 & value8 & value9 \\\\\r\n"
1227            "\\hline\r\n"
1228            "\\end{tabular}"
1229        )
1230
1231        options = {"fields": ["Field 1", "Field 3"]}
1232        assert t.get_latex_string(format=True, **options) == (
1233            "\\begin{tabular}{|c|c|}\r\n"
1234            "\\hline\r\n"
1235            "Field 1 & Field 3 \\\\\r\n"
1236            "value 1 & value3 \\\\\r\n"
1237            "value 4 & value6 \\\\\r\n"
1238            "value 7 & value9 \\\\\r\n"
1239            "\\hline\r\n"
1240            "\\end{tabular}"
1241        )
1242
1243        options = {"vrules": FRAME}
1244        assert t.get_latex_string(format=True, **options) == (
1245            "\\begin{tabular}{|ccc|}\r\n"
1246            "\\hline\r\n"
1247            "Field 1 & Field 2 & Field 3 \\\\\r\n"
1248            "value 1 & value2 & value3 \\\\\r\n"
1249            "value 4 & value5 & value6 \\\\\r\n"
1250            "value 7 & value8 & value9 \\\\\r\n"
1251            "\\hline\r\n"
1252            "\\end{tabular}"
1253        )
1254
1255        options = {"hrules": ALL}
1256        assert t.get_latex_string(format=True, **options) == (
1257            "\\begin{tabular}{|c|c|c|}\r\n"
1258            "\\hline\r\n"
1259            "Field 1 & Field 2 & Field 3 \\\\\r\n"
1260            "\\hline\r\n"
1261            "value 1 & value2 & value3 \\\\\r\n"
1262            "\\hline\r\n"
1263            "value 4 & value5 & value6 \\\\\r\n"
1264            "\\hline\r\n"
1265            "value 7 & value8 & value9 \\\\\r\n"
1266            "\\hline\r\n"
1267            "\\end{tabular}"
1268        )
1269
1270    def testLatexOutputHeader(self):
1271        t = helper_table()
1272        assert t.get_latex_string(format=True, hrules=HEADER) == (
1273            "\\begin{tabular}{|c|c|c|}\r\n"
1274            "Field 1 & Field 2 & Field 3 \\\\\r\n"
1275            "\\hline\r\n"
1276            "value 1 & value2 & value3 \\\\\r\n"
1277            "value 4 & value5 & value6 \\\\\r\n"
1278            "value 7 & value8 & value9 \\\\\r\n"
1279            "\\end{tabular}"
1280        )
1281
1282
1283class TestJSONConstructor:
1284    def test_JSONAndBack(self, city_data_prettytable: PrettyTable):
1285        json_string = city_data_prettytable.get_json_string()
1286        new_table = from_json(json_string)
1287        assert new_table.get_string() == city_data_prettytable.get_string()
1288
1289
1290class TestHtmlConstructor:
1291    def test_HtmlAndBack(self, city_data_prettytable: PrettyTable):
1292        html_string = city_data_prettytable.get_html_string()
1293        new_table = from_html(html_string)[0]
1294        assert new_table.get_string() == city_data_prettytable.get_string()
1295
1296    def test_HtmlOneAndBack(self, city_data_prettytable: PrettyTable):
1297        html_string = city_data_prettytable.get_html_string()
1298        new_table = from_html_one(html_string)
1299        assert new_table.get_string() == city_data_prettytable.get_string()
1300
1301    def test_HtmlOneFailOnMany(self, city_data_prettytable: PrettyTable):
1302        html_string = city_data_prettytable.get_html_string()
1303        html_string += city_data_prettytable.get_html_string()
1304        with pytest.raises(Exception):
1305            from_html_one(html_string)
1306
1307
1308@pytest.fixture
1309def japanese_pretty_table():
1310    pt = PrettyTable(["Kanji", "Hiragana", "English"])
1311    pt.add_row(["神戸", "こうべ", "Kobe"])
1312    pt.add_row(["京都", "きょうと", "Kyoto"])
1313    pt.add_row(["長崎", "ながさき", "Nagasaki"])
1314    pt.add_row(["名古屋", "なごや", "Nagoya"])
1315    pt.add_row(["大阪", "おおさか", "Osaka"])
1316    pt.add_row(["札幌", "さっぽろ", "Sapporo"])
1317    pt.add_row(["東京", "とうきょう", "Tokyo"])
1318    pt.add_row(["横浜", "よこはま", "Yokohama"])
1319    return pt
1320
1321
1322@pytest.fixture
1323def emoji_pretty_table():
1324    thunder1 = [
1325        '\033[38;5;226m _`/""\033[38;5;250m.-.    \033[0m',
1326        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
1327        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
1328        "\033[38;5;228;5m    ⚡\033[38;5;111;25mʻ ʻ\033[38;5;228;5m"
1329        "⚡\033[38;5;111;25mʻ ʻ \033[0m",
1330        "\033[38;5;111m    ʻ ʻ ʻ ʻ  \033[0m",
1331    ]
1332    thunder2 = [
1333        "\033[38;5;240;1m     .-.     \033[0m",
1334        "\033[38;5;240;1m    (   ).   \033[0m",
1335        "\033[38;5;240;1m   (___(__)  \033[0m",
1336        "\033[38;5;21;1m  ‚ʻ\033[38;5;228;5m⚡\033[38;5;21;25mʻ‚\033[38;5;228;5m"
1337        "⚡\033[38;5;21;25m‚ʻ   \033[0m",
1338        "\033[38;5;21;1m  ‚ʻ‚ʻ\033[38;5;228;5m⚡\033[38;5;21;25mʻ‚ʻ   \033[0m",
1339    ]
1340    pt = PrettyTable(["Thunderbolt", "Lightning"])
1341    for i in range(len(thunder1)):
1342        pt.add_row([thunder1[i], thunder2[i]])
1343    return pt
1344
1345
1346class TestMultiPattern:
1347    @pytest.mark.parametrize(
1348        ["pt", "expected_output", "test_type"],
1349        [
1350            (
1351                pytest.lazy_fixture("city_data_prettytable"),
1352                """
1353+-----------+------+------------+-----------------+
1354| City name | Area | Population | Annual Rainfall |
1355+-----------+------+------------+-----------------+
1356|  Adelaide | 1295 |  1158259   |      600.5      |
1357|  Brisbane | 5905 |  1857594   |      1146.4     |
1358|   Darwin  | 112  |   120900   |      1714.7     |
1359|   Hobart  | 1357 |   205556   |      619.5      |
1360|   Sydney  | 2058 |  4336374   |      1214.8     |
1361| Melbourne | 1566 |  3806092   |      646.9      |
1362|   Perth   | 5386 |  1554769   |      869.4      |
1363+-----------+------+------------+-----------------+
1364""",
1365                "English Table",
1366            ),
1367            (
1368                pytest.lazy_fixture("japanese_pretty_table"),
1369                """
1370+--------+------------+----------+
1371| Kanji  |  Hiragana  | English  |
1372+--------+------------+----------+
1373|  神戸  |   こうべ   |   Kobe   |
1374|  京都  |  きょうと  |  Kyoto   |
1375|  長崎  |  ながさき  | Nagasaki |
1376| 名古屋 |   なごや   |  Nagoya  |
1377|  大阪  |  おおさか  |  Osaka   |
1378|  札幌  |  さっぽろ  | Sapporo  |
1379|  東京  | とうきょう |  Tokyo   |
1380|  横浜  |  よこはま  | Yokohama |
1381+--------+------------+----------+
1382
1383""",
1384                "Japanese table",
1385            ),
1386            (
1387                pytest.lazy_fixture("emoji_pretty_table"),
1388                """
1389+-----------------+-----------------+
1390|   Thunderbolt   |    Lightning    |
1391+-----------------+-----------------+
1392|  \x1b[38;5;226m _`/""\x1b[38;5;250m.-.    \x1b[0m  |  \x1b[38;5;240;1m     .-.     \x1b[0m  |
1393|  \x1b[38;5;226m  ,\\_\x1b[38;5;250m(   ).  \x1b[0m  |  \x1b[38;5;240;1m    (   ).   \x1b[0m  |
1394|  \x1b[38;5;226m   /\x1b[38;5;250m(___(__) \x1b[0m  |  \x1b[38;5;240;1m   (___(__)  \x1b[0m  |
1395| \x1b[38;5;228;5m    ⚡\x1b[38;5;111;25mʻ ʻ\x1b[38;5;228;5m⚡\x1b[38;5;111;25mʻ ʻ \x1b[0m | \x1b[38;5;21;1m  ‚ʻ\x1b[38;5;228;5m⚡\x1b[38;5;21;25mʻ‚\x1b[38;5;228;5m⚡\x1b[38;5;21;25m‚ʻ   \x1b[0m |
1396|  \x1b[38;5;111m    ʻ ʻ ʻ ʻ  \x1b[0m  |  \x1b[38;5;21;1m  ‚ʻ‚ʻ\x1b[38;5;228;5m⚡\x1b[38;5;21;25mʻ‚ʻ   \x1b[0m |
1397+-----------------+-----------------+
1398            """,  # noqa: E501
1399                "Emoji table",
1400            ),
1401        ],
1402    )
1403    def test_multi_pattern_outputs(
1404        self, pt: PrettyTable, expected_output: str, test_type: str
1405    ):
1406        printed_table = pt.get_string()
1407        assert (
1408            printed_table.strip() == expected_output.strip()
1409        ), f"Error output for test output of type {test_type}"
1410
1411
1412def test_paginate():
1413    # Arrange
1414    t = helper_table(rows=7)
1415    expected_page_1 = """
1416+----------+---------+---------+
1417| Field 1  | Field 2 | Field 3 |
1418+----------+---------+---------+
1419| value 1  |  value2 |  value3 |
1420| value 4  |  value5 |  value6 |
1421| value 7  |  value8 |  value9 |
1422| value 10 | value11 | value12 |
1423+----------+---------+---------+
1424    """.strip()
1425    expected_page_2 = """
1426+----------+---------+---------+
1427| Field 1  | Field 2 | Field 3 |
1428+----------+---------+---------+
1429| value 13 | value14 | value15 |
1430| value 16 | value17 | value18 |
1431| value 19 | value20 | value21 |
1432+----------+---------+---------+
1433""".strip()
1434
1435    # Act
1436    paginated = t.paginate(page_length=4)
1437
1438    # Assert
1439    paginated = paginated.strip()
1440    assert paginated.startswith(expected_page_1)
1441    assert "\f" in paginated
1442    assert paginated.endswith(expected_page_2)
1443
1444
1445def test_add_rows():
1446    """A table created with multiple add_row calls
1447    is the same as one created with a single add_rows
1448    """
1449    # Arrange
1450    t1 = PrettyTable(["A", "B", "C"])
1451    t2 = PrettyTable(["A", "B", "C"])
1452    t1.add_row([1, 2, 3])
1453    t1.add_row([4, 5, 6])
1454    rows = [
1455        [1, 2, 3],
1456        [4, 5, 6],
1457    ]
1458
1459    # Act
1460    t2.add_rows(rows)
1461
1462    # Assert
1463    assert str(t1) == str(t2)
1464
1465
1466def test_autoindex():
1467
1468    """Testing that a table with a custom index row is
1469    equal to the one produced by the function
1470    .add_autoindex()
1471    """
1472    table1 = PrettyTable()
1473    table1.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
1474    table1.add_row(["Adelaide", 1295, 1158259, 600.5])
1475    table1.add_row(["Brisbane", 5905, 1857594, 1146.4])
1476    table1.add_row(["Darwin", 112, 120900, 1714.7])
1477    table1.add_row(["Hobart", 1357, 205556, 619.5])
1478    table1.add_row(["Sydney", 2058, 4336374, 1214.8])
1479    table1.add_row(["Melbourne", 1566, 3806092, 646.9])
1480    table1.add_row(["Perth", 5386, 1554769, 869.4])
1481    table1.add_autoindex(fieldname="Test")
1482
1483    table2 = PrettyTable()
1484    table2.field_names = ["Test", "City name", "Area", "Population", "Annual Rainfall"]
1485    table2.add_row([1, "Adelaide", 1295, 1158259, 600.5])
1486    table2.add_row([2, "Brisbane", 5905, 1857594, 1146.4])
1487    table2.add_row([3, "Darwin", 112, 120900, 1714.7])
1488    table2.add_row([4, "Hobart", 1357, 205556, 619.5])
1489    table2.add_row([5, "Sydney", 2058, 4336374, 1214.8])
1490    table2.add_row([6, "Melbourne", 1566, 3806092, 646.9])
1491    table2.add_row([7, "Perth", 5386, 1554769, 869.4])
1492
1493    assert str(table1) == str(table2)
1494
1495
1496@pytest.fixture(scope="function")
1497def unpadded_pt():
1498    pt = PrettyTable(header=False, padding_width=0)
1499    pt.add_row("abc")
1500    pt.add_row("def")
1501    pt.add_row("g..")
1502    return pt
1503
1504
1505class TestUnpaddedTable:
1506    def test_unbordered(self, unpadded_pt: PrettyTable):
1507        unpadded_pt.border = False
1508        result = unpadded_pt.get_string()
1509        expected = """
1510abc
1511def
1512g..
1513"""
1514        assert result.strip() == expected.strip()
1515
1516    def test_bordered(self, unpadded_pt: PrettyTable):
1517        unpadded_pt.border = True
1518        result = unpadded_pt.get_string()
1519        expected = """
1520+-+-+-+
1521|a|b|c|
1522|d|e|f|
1523|g|.|.|
1524+-+-+-+
1525"""
1526        assert result.strip() == expected.strip()
1527
1528
1529class TestCustomFormatter:
1530    def test_init_custom_format_is_empty(self):
1531        pt = PrettyTable()
1532        assert pt.custom_format == {}
1533
1534    def test_init_custom_format_set_value(self):
1535        pt = PrettyTable(
1536            custom_format={"col1": (lambda col_name, value: f"{value:.2}")}
1537        )
1538        assert len(pt.custom_format) == 1
1539
1540    def test_init_custom_format_throw_error_is_not_callable(self):
1541        with pytest.raises(Exception) as e:
1542            PrettyTable(custom_format={"col1": "{:.2}"})
1543
1544        assert "Invalid value for custom_format.col1. Must be a function." in str(
1545            e.value
1546        )
1547
1548    def test_can_set_custom_format_from_property_setter(self):
1549        pt = PrettyTable()
1550        pt.custom_format = {"col1": (lambda col_name, value: f"{value:.2}")}
1551        assert len(pt.custom_format) == 1
1552
1553    def test_set_custom_format_to_none_set_empty_dict(self):
1554        pt = PrettyTable()
1555        pt.custom_format = None
1556        assert len(pt.custom_format) == 0
1557        assert isinstance(pt.custom_format, dict)
1558
1559    def test_set_custom_format_invalid_type_throw_error(self):
1560        pt = PrettyTable()
1561        with pytest.raises(Exception) as e:
1562            pt.custom_format = "Some String"
1563        assert "The custom_format property need to be a dictionary or callable" in str(
1564            e.value
1565        )
1566
1567    def test_use_custom_formatter_for_int(self, city_data_prettytable: PrettyTable):
1568        city_data_prettytable.custom_format["Annual Rainfall"] = lambda n, v: f"{v:.2f}"
1569        assert (
1570            city_data_prettytable.get_string().strip()
1571            == """
1572+-----------+------+------------+-----------------+
1573| City name | Area | Population | Annual Rainfall |
1574+-----------+------+------------+-----------------+
1575|  Adelaide | 1295 |  1158259   |      600.50     |
1576|  Brisbane | 5905 |  1857594   |     1146.40     |
1577|   Darwin  | 112  |   120900   |     1714.70     |
1578|   Hobart  | 1357 |   205556   |      619.50     |
1579|   Sydney  | 2058 |  4336374   |     1214.80     |
1580| Melbourne | 1566 |  3806092   |      646.90     |
1581|   Perth   | 5386 |  1554769   |      869.40     |
1582+-----------+------+------------+-----------------+
1583""".strip()
1584        )
1585
1586    def test_custom_format_multi_type(self):
1587        pt = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
1588        pt.add_row([dt.date(2021, 1, 1), "January", 12345.12345, 12345678])
1589        pt.add_row([dt.date(2021, 2, 1), "February", 54321.12345, 87654321])
1590        pt.custom_format["col_date"] = lambda f, v: v.strftime("%d %b %Y")
1591        pt.custom_format["col_float"] = lambda f, v: f"{v:.3f}"
1592        pt.custom_format["col_int"] = lambda f, v: f"{v:,}"
1593        assert (
1594            pt.get_string().strip()
1595            == """
1596+-------------+----------+-----------+------------+
1597|   col_date  | col_str  | col_float |  col_int   |
1598+-------------+----------+-----------+------------+
1599| 01 Jan 2021 | January  | 12345.123 | 12,345,678 |
1600| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
1601+-------------+----------+-----------+------------+
1602""".strip()
1603        )
1604
1605    def test_custom_format_multi_type_using_on_function(self):
1606        pt = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
1607        pt.add_row([dt.date(2021, 1, 1), "January", 12345.12345, 12345678])
1608        pt.add_row([dt.date(2021, 2, 1), "February", 54321.12345, 87654321])
1609
1610        def my_format(col: str, value: Any) -> str:
1611            if col == "col_date":
1612                return value.strftime("%d %b %Y")
1613            if col == "col_float":
1614                return f"{value:.3f}"
1615            if col == "col_int":
1616                return f"{value:,}"
1617            return str(value)
1618
1619        pt.custom_format = my_format
1620        assert (
1621            pt.get_string().strip()
1622            == """
1623+-------------+----------+-----------+------------+
1624|   col_date  | col_str  | col_float |  col_int   |
1625+-------------+----------+-----------+------------+
1626| 01 Jan 2021 | January  | 12345.123 | 12,345,678 |
1627| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
1628+-------------+----------+-----------+------------+
1629""".strip()
1630        )
1631
1632
1633class TestRepr:
1634    def test_default_repr(self, row_prettytable: PrettyTable):
1635        assert row_prettytable.__str__() == row_prettytable.__repr__()
1636
1637    def test_jupyter_repr(self, row_prettytable: PrettyTable):
1638        assert row_prettytable._repr_html_() == row_prettytable.get_html_string()
1639