From db0ab9a6f16777e1ce9eedea2b63adf3b7fc75da Mon Sep 17 00:00:00 2001 From: Alvaro Tinoco Date: Wed, 31 Jul 2024 13:00:54 +0200 Subject: [PATCH 1/7] feat(html): add escaping options --- README.md | 10 ++++ src/prettytable/prettytable.py | 104 ++++++++++++++++++++++++++------- tests/test_prettytable.py | 93 +++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index aa442709..023d847d 100644 --- a/README.md +++ b/README.md @@ -675,6 +675,16 @@ will print: ``` +#### Setting HTML Escaping + +By default, PrettyTable will escape the data contained in the header and data fields +when sending output to html. This can be disabled by setting the 'escape_header' and +'escape_data' to false. For example: + +```python +print(x.get_html_string(escape_header=False, escape_data=False)) +``` + ### Miscellaneous things #### Copying a table diff --git a/src/prettytable/prettytable.py b/src/prettytable/prettytable.py index 62a1df1d..af09ddbd 100644 --- a/src/prettytable/prettytable.py +++ b/src/prettytable/prettytable.py @@ -188,6 +188,8 @@ def __init__(self, field_names=None, **kwargs) -> None: "max_width", "min_width", "none_format", + "escape_header", + "escape_data", ] for option in self._options: if option in kwargs: @@ -224,6 +226,14 @@ def __init__(self, field_names=None, **kwargs) -> None: self._reversesort = False self._sort_key = kwargs["sort_key"] or (lambda x: x) + if kwargs["escape_data"] in (True, False): + self._escape_data = kwargs["escape_data"] + else: + self._escape_data = True + if kwargs["escape_header"] in (True, False): + self._escape_header = kwargs["escape_header"] + else: + self._escape_header = True # Column specific arguments, use property.setters self.align = kwargs["align"] or {} self.valign = kwargs["valign"] or {} @@ -380,6 +390,8 @@ def _validate_option(self, option, val) -> None: "xhtml", "print_empty", "oldsortslice", + "escape_header", + "escape_data", ): self._validate_true_or_false(option, val) elif option == "header_style": @@ -1252,6 +1264,26 @@ def oldsortslice(self, val) -> None: self._validate_option("oldsortslice", val) self._oldsortslice = val + @property + def escape_header(self): + """Escapes the text within a header (True or False)""" + return self._escape_header + + @escape_header.setter + def escape_header(self, val): + self._validate_option("escape_header", val) + self._escape_header = val + + @property + def escape_data(self): + """Escapes the text within a data field (True or False)""" + return self._escape_data + + @escape_data.setter + def escape_data(self, val): + self._validate_option("escape_data", val) + self._escape_data = val + ############################## # OPTION MIXER # ############################## @@ -2136,6 +2168,7 @@ def get_html_string(self, **kwargs) -> str: end - index of last data row to include in output PLUS ONE (list slice style) fields - names of fields (columns) to include header - print a header showing field names (True or False) + escape_header - escapes the text within a header (True or False) border - print a border around the table (True or False) preserve_internal_border - print a border inside the table even if border is disabled (True or False) @@ -2156,6 +2189,7 @@ def get_html_string(self, **kwargs) -> str: tag format - Controls whether or not HTML tables are formatted to match styling options (True or False) + escape_data - escapes the text within a data field (True or False) xhtml - print
tags if True,
tags if False""" options = self._get_options(kwargs) @@ -2197,11 +2231,16 @@ def _get_simple_html_string(self, options): for field in self._field_names: if options["fields"] and field not in options["fields"]: continue - lines.append( - " ".format( - escape(field).replace("\n", linebreak) + if options["escape_header"]: + lines.append( + " ".format( + escape(field).replace("\n", linebreak) + ) + ) + else: + lines.append( + " ".format(field.replace("\n", linebreak)) ) - ) lines.append(" ") lines.append(" ") @@ -2214,11 +2253,16 @@ def _get_simple_html_string(self, options): for field, datum in zip(self._field_names, row): if options["fields"] and field not in options["fields"]: continue - lines.append( - " ".format( - escape(datum).replace("\n", linebreak) + if options["escape_data"]: + lines.append( + " ".format( + escape(datum).replace("\n", linebreak) + ) + ) + else: + lines.append( + " ".format(datum.replace("\n", linebreak)) ) - ) lines.append(" ") lines.append(" ") lines.append("
{}{}{}
{}{}{}
") @@ -2273,10 +2317,16 @@ def _get_formatted_html_string(self, options): for field in self._field_names: if options["fields"] and field not in options["fields"]: continue - lines.append( - ' %s' # noqa: E501 - % (lpad, rpad, escape(field).replace("\n", linebreak)) - ) + if options["escape_header"]: + lines.append( + ' %s' # noqa: E501 + % (lpad, rpad, escape(field).replace("\n", linebreak)) + ) + else: + lines.append( + ' %s' # noqa: E501 + % (lpad, rpad, field.replace("\n", linebreak)) + ) lines.append(" ") lines.append(" ") @@ -2300,16 +2350,28 @@ def _get_formatted_html_string(self, options): ): if options["fields"] and field not in options["fields"]: continue - lines.append( - ' %s' # noqa: E501 - % ( - lpad, - rpad, - align, - valign, - escape(datum).replace("\n", linebreak), + if options["escape_data"]: + lines.append( + ' %s' # noqa: E501 + % ( + lpad, + rpad, + align, + valign, + escape(datum).replace("\n", linebreak), + ) + ) + else: + lines.append( + ' %s' # noqa: E501 + % ( + lpad, + rpad, + align, + valign, + datum.replace("\n", linebreak), + ) ) - ) lines.append(" ") lines.append(" ") lines.append("") diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py index 9b51e4a2..56b46daa 100644 --- a/tests/test_prettytable.py +++ b/tests/test_prettytable.py @@ -1198,6 +1198,99 @@ def test_html_output_formatted_with_title(self) -> None: """.strip() # noqa: E501 ) + def test_html_output_without_escaped_header(self) -> None: + t = helper_table() + t.field_names = ["", "Field 1", "Field 2", "Field 3"] + result = t.get_html_string(escape_header=False) + assert ( + result.strip() + == """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field 1Field 2Field 3
1value 1value2value3
4value 4value5value6
7value 7value8value9
+""".strip() + ) + + def test_html_output_without_escaped_data(self) -> None: + t = helper_table() + t.add_row( + [ + 1, + "value 1", + "value2", + "value3", + ] + ) + result = t.get_html_string(escape_data=False) + assert ( + result.strip() + == """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field 1Field 2Field 3
1value 1value2value3
4value 4value5value6
7value 7value8value9
1value 1value2value3
+""".strip() + ) + class TestPositionalJunctions: """Verify different cases for positional-junction characters""" From 293937af4ffdb3fd14be5ad4482342a608a34eb4 Mon Sep 17 00:00:00 2001 From: Alvaro Tinoco Date: Fri, 2 Aug 2024 16:22:11 +0200 Subject: [PATCH 2/7] docs(readme): improve format --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 023d847d..74121072 100644 --- a/README.md +++ b/README.md @@ -678,11 +678,11 @@ will print: #### Setting HTML Escaping By default, PrettyTable will escape the data contained in the header and data fields -when sending output to html. This can be disabled by setting the 'escape_header' and -'escape_data' to false. For example: +when sending output to HTML. This can be disabled by setting the `escape_header` and +`escape_data` to false. For example: ```python -print(x.get_html_string(escape_header=False, escape_data=False)) +print(table.get_html_string(escape_header=False, escape_data=False)) ``` ### Miscellaneous things From 68b4ab9a72d318dd8a6dbe132ce648c36820656c Mon Sep 17 00:00:00 2001 From: Alvaro Tinoco Date: Fri, 2 Aug 2024 16:30:42 +0200 Subject: [PATCH 3/7] refactor(html): simplify escaping logic --- src/prettytable/prettytable.py | 75 ++++++++++----------------- tests/test_prettytable.py | 94 ++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 47 deletions(-) diff --git a/src/prettytable/prettytable.py b/src/prettytable/prettytable.py index af09ddbd..ca8ae512 100644 --- a/src/prettytable/prettytable.py +++ b/src/prettytable/prettytable.py @@ -2232,15 +2232,12 @@ def _get_simple_html_string(self, options): if options["fields"] and field not in options["fields"]: continue if options["escape_header"]: - lines.append( - " {}".format( - escape(field).replace("\n", linebreak) - ) - ) - else: - lines.append( - " {}".format(field.replace("\n", linebreak)) - ) + field = escape(field) + + lines.append( + " {}".format(field.replace("\n", linebreak)) + ) + lines.append(" ") lines.append(" ") @@ -2254,15 +2251,11 @@ def _get_simple_html_string(self, options): if options["fields"] and field not in options["fields"]: continue if options["escape_data"]: - lines.append( - " {}".format( - escape(datum).replace("\n", linebreak) - ) - ) - else: - lines.append( - " {}".format(datum.replace("\n", linebreak)) - ) + datum = escape(datum) + + lines.append( + " {}".format(datum.replace("\n", linebreak)) + ) lines.append(" ") lines.append(" ") lines.append("") @@ -2318,15 +2311,12 @@ def _get_formatted_html_string(self, options): if options["fields"] and field not in options["fields"]: continue if options["escape_header"]: - lines.append( - ' %s' # noqa: E501 - % (lpad, rpad, escape(field).replace("\n", linebreak)) - ) - else: - lines.append( - ' %s' # noqa: E501 - % (lpad, rpad, field.replace("\n", linebreak)) - ) + field = escape(field) + + lines.append( + ' %s' # noqa: E501 + % (lpad, rpad, field.replace("\n", linebreak)) + ) lines.append(" ") lines.append(" ") @@ -2351,27 +2341,18 @@ def _get_formatted_html_string(self, options): if options["fields"] and field not in options["fields"]: continue if options["escape_data"]: - lines.append( - ' %s' # noqa: E501 - % ( - lpad, - rpad, - align, - valign, - escape(datum).replace("\n", linebreak), - ) - ) - else: - lines.append( - ' %s' # noqa: E501 - % ( - lpad, - rpad, - align, - valign, - datum.replace("\n", linebreak), - ) + datum = escape(datum) + + lines.append( + ' %s' # noqa: E501 + % ( + lpad, + rpad, + align, + valign, + datum.replace("\n", linebreak), ) + ) lines.append(" ") lines.append(" ") lines.append("") diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py index 56b46daa..13125073 100644 --- a/tests/test_prettytable.py +++ b/tests/test_prettytable.py @@ -1291,6 +1291,100 @@ def test_html_output_without_escaped_data(self) -> None: """.strip() ) + def test_html_output_with_escaped_header(self) -> None: + t = helper_table() + t.field_names = ["", "Field 1", "Field 2", "Field 3"] + result = t.get_html_string(escape_header=True) + assert ( + result.strip() + == """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field 1<em>Field 2</em><a href='#'>Field 3</a>
1value 1value2value3
4value 4value5value6
7value 7value8value9
+""".strip() + ) + + def test_html_output_with_escaped_data(self) -> None: + t = helper_table() + t.add_row( + [ + 1, + "value 1", + "value2", + "value3", + ] + ) + result = t.get_html_string(escape_data=True) + assert ( + result.strip() + == """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field 1Field 2Field 3
1value 1value2value3
4value 4value5value6
7value 7value8value9
1<b>value 1</b><span style='text-decoration: underline;'>value2</span><a href='#'>value3</a>
+""".strip() + ) + + class TestPositionalJunctions: """Verify different cases for positional-junction characters""" From 7ade37048217a3e7392f77ef1d2252a0f2ea08e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:31:07 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_prettytable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py index 13125073..bdce605e 100644 --- a/tests/test_prettytable.py +++ b/tests/test_prettytable.py @@ -1385,7 +1385,6 @@ def test_html_output_with_escaped_data(self) -> None: ) - class TestPositionalJunctions: """Verify different cases for positional-junction characters""" From 01c7fa20b5faa0f92c3bffba78f4cb19eaa55364 Mon Sep 17 00:00:00 2001 From: Alvaro Tinoco Date: Fri, 2 Aug 2024 16:32:38 +0200 Subject: [PATCH 5/7] style: ignore line too long --- tests/test_prettytable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py index bdce605e..71375a5e 100644 --- a/tests/test_prettytable.py +++ b/tests/test_prettytable.py @@ -1381,7 +1381,7 @@ def test_html_output_with_escaped_data(self) -> None: -""".strip() +""".strip() # noqa: E501 ) From 5b70e3752b9561575fe1055f70c968d8d3d8470b Mon Sep 17 00:00:00 2001 From: Alvaro Tinoco Date: Fri, 2 Aug 2024 17:33:36 +0200 Subject: [PATCH 6/7] test(html): improve coverage --- tests/test_prettytable.py | 188 +++++++++++++++++++++++--------------- 1 file changed, 115 insertions(+), 73 deletions(-) diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py index 71375a5e..750b84ce 100644 --- a/tests/test_prettytable.py +++ b/tests/test_prettytable.py @@ -1199,7 +1199,7 @@ def test_html_output_formatted_with_title(self) -> None: ) def test_html_output_without_escaped_header(self) -> None: - t = helper_table() + t = helper_table(rows=0) t.field_names = ["", "Field 1", "Field 2", "Field 3"] result = t.get_html_string(escape_header=False) assert ( @@ -1215,31 +1215,13 @@ def test_html_output_without_escaped_header(self) -> None: - - 1 - value 1 - value2 - value3 - - - 4 - value 4 - value5 - value6 - - - 7 - value 7 - value8 - value9 - """.strip() ) def test_html_output_without_escaped_data(self) -> None: - t = helper_table() + t = helper_table(rows=0) t.add_row( [ 1, @@ -1262,24 +1244,6 @@ def test_html_output_without_escaped_data(self) -> None: - - 1 - value 1 - value2 - value3 - - - 4 - value 4 - value5 - value6 - - - 7 - value 7 - value8 - value9 - 1 value 1 @@ -1292,7 +1256,7 @@ def test_html_output_without_escaped_data(self) -> None: ) def test_html_output_with_escaped_header(self) -> None: - t = helper_table() + t = helper_table(rows=0) t.field_names = ["", "Field 1", "Field 2", "Field 3"] result = t.get_html_string(escape_header=True) assert ( @@ -1308,31 +1272,13 @@ def test_html_output_with_escaped_header(self) -> None: - - 1 - value 1 - value2 - value3 - - - 4 - value 4 - value5 - value6 - - - 7 - value 7 - value8 - value9 - """.strip() ) def test_html_output_with_escaped_data(self) -> None: - t = helper_table() + t = helper_table(rows=0) t.add_row( [ 1, @@ -1357,27 +1303,123 @@ def test_html_output_with_escaped_data(self) -> None: 1 - value 1 - value2 - value3 + <b>value 1</b> + <span style='text-decoration: underline;'>value2</span> + <a href='#'>value3</a> + + +""".strip() # noqa: E501 + ) + + def test_html_output_formatted_without_escaped_header(self) -> None: + t = helper_table(rows=0) + t.field_names = ["", "Field 1", "Field 2", "Field 3"] + result = t.get_html_string(escape_header=False, format=True) + assert ( + result.strip() + == """ + + - - - - + + + + + + + +
4value 4value5value6Field 1Field 2Field 3
+""".strip() # noqa: E501 + ) + + def test_html_output_formatted_without_escaped_data(self) -> None: + t = helper_table(rows=0) + t.add_row( + [ + 1, + "value 1", + "value2", + "value3", + ] + ) + result = t.get_html_string(escape_data=False, format=True) + assert ( + result.strip() + == """ + + - - - - + + + + + + - - - - + + + + + + +
7value 7value8value9Field 1Field 2Field 3
1<b>value 1</b><span style='text-decoration: underline;'>value2</span><a href='#'>value3</a>1value 1value2value3
+""".strip() # noqa: E501 + ) + + def test_html_output_formatted_with_escaped_header(self) -> None: + t = helper_table(rows=0) + t.field_names = ["", "Field 1", "Field 2", "Field 3"] + result = t.get_html_string(escape_header=True, format=True) + assert ( + result.strip() + == """ + + + + + + + + + + + +
Field 1<em>Field 2</em><a href='#'>Field 3</a>
+""".strip() # noqa: E501 + ) + + def test_html_output_formatted_with_escaped_data(self) -> None: + t = helper_table(rows=0) + t.add_row( + [ + 1, + "value 1", + "value2", + "value3", + ] + ) + result = t.get_html_string(escape_data=True, format=True) + assert ( + result.strip() + == """ + + + + + + + + + + + + + + +
Field 1Field 2Field 3
1<b>value 1</b><span style='text-decoration: underline;'>value2</span><a href='#'>value3</a>
From d9082d539cced8f3da6f220cb03c168cdc09fe21 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 4 Aug 2024 19:13:54 +0300 Subject: [PATCH 7/7] Consistent titles --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74121072..bb365317 100644 --- a/README.md +++ b/README.md @@ -675,7 +675,7 @@ will print: ``` -#### Setting HTML Escaping +#### Setting HTML escaping By default, PrettyTable will escape the data contained in the header and data fields when sending output to HTML. This can be disabled by setting the `escape_header` and