diff --git a/.gitignore b/.gitignore
index d633447..0385fff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,5 @@
/dist/
/htmlcov/
/man/
+/monkeytype.sqlite3
/.venv/
diff --git a/zpretty/attributes.py b/zpretty/attributes.py
index ec17d0d..1d359c9 100644
--- a/zpretty/attributes.py
+++ b/zpretty/attributes.py
@@ -1,4 +1,12 @@
+from bs4.element import AttributeDict
+from bs4.element import CharsetMetaAttributeValue
from logging import getLogger
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import Union
+from zpretty.elements import PrettyElement
try:
@@ -85,16 +93,20 @@ class PrettyAttributes:
"i18n:ignore-attributes",
)
- def __init__(self, attributes, element=None):
+ def __init__(
+ self,
+ attributes: AttributeDict | dict[str, str],
+ element: PrettyElement | None = None,
+ ) -> None:
"""attributes is a dict like object"""
self.attributes = attributes
self.element = element
- def __len__(self):
+ def __len__(self) -> int:
return len(self.attributes)
@property
- def prefix(self):
+ def prefix(self) -> str:
"""Return the prefix for the attributes
The returned value will be a number of spaces equal to the tag name length + 2,
@@ -108,7 +120,7 @@ def prefix(self):
return " "
return " " * (len(self.element.tag or "") + 2)
- def sort_attributes(self, name):
+ def sort_attributes(self, name: str) -> tuple[int, str]:
"""This sorts the attribute trying to group them semantically
Starting from the top:
@@ -142,7 +154,7 @@ def format_multiline(self, name, value):
line_joiner = "\n" + (" " * (len(name) + 2))
return line_joiner.join(value_lines)
- def format_tal_multiline(self, value):
+ def format_tal_multiline(self, value: str) -> str:
"""There are some tal specific attributes that contain ; separated
statements.
They are used to define variables or set other attributes.
@@ -180,7 +192,7 @@ def format_tal_multiline(self, value):
# restore ';;'
return new_value.replace("<>", ";;")
- def is_tal_attribute(self, name):
+ def is_tal_attribute(self, name: str) -> bool | None:
"""Check if the attribute is a tal attribute"""
if name.startswith("tal:"):
return True
@@ -192,7 +204,9 @@ def is_tal_attribute(self, name):
if f"tal:{name}" in self._tal_attribute_order:
return True
- def maybe_escape(self, name, value):
+ def maybe_escape(
+ self, name: str, value: CharsetMetaAttributeValue | str
+ ) -> str:
"""Escape the value if needed"""
if self.is_tal_attribute(name):
# Never escape what we have in tal attributes
@@ -200,7 +214,7 @@ def maybe_escape(self, name, value):
return escape(value, quote=False)
- def can_be_valueless(self, name):
+ def can_be_valueless(self, name: str) -> bool:
"""Check if the attribute name can be without a value"""
if not self._boolean_attributes_are_allowed:
return False
@@ -210,7 +224,7 @@ def can_be_valueless(self, name):
return True
return False
- def lines(self):
+ def lines(self) -> list[str]:
"""Take the attributes, sort them and prettify their values"""
attributes = self.attributes
sorted_names = sorted(attributes, key=self.sort_attributes)
@@ -238,11 +252,11 @@ def lines(self):
lines.append(line)
return lines
- def lstrip(self):
+ def lstrip(self) -> str:
"""This returns the attributes with the left spaces removed"""
return self().lstrip()
- def __call__(self):
+ def __call__(self) -> str:
"""Render the attributes as text
Render and an empty string if no attributes
diff --git a/zpretty/elements.py b/zpretty/elements.py
index e32ba09..644472d 100644
--- a/zpretty/elements.py
+++ b/zpretty/elements.py
@@ -3,8 +3,14 @@
from bs4.element import Comment
from bs4.element import Doctype
from bs4.element import NavigableString
+from bs4.element import PageElement
from bs4.element import ProcessingInstruction
+from bs4.element import Script
+from bs4.element import Stylesheet
from bs4.element import Tag
+from collections.abc import Callable
+from typing import Optional
+from typing import Union
from zpretty.attributes import PrettyAttributes
from zpretty.text import endswith_whitespace
from zpretty.text import lstrip_first_line
@@ -23,7 +29,7 @@ def __str__(self):
return "Known self closing tag %r is not closed" % self.el.context
-def memo(f):
+def memo(f: Callable) -> Callable:
"""Simple memoize"""
key = "__zpretty_memo__" + f.__name__
@@ -79,7 +85,7 @@ class PrettyElement:
preserve_text_whitespace_elements = ["pre"]
skip_text_escaping_elements = ["script", "style"]
- def __init__(self, context, level=0):
+ def __init__(self, context: PageElement, level: int = 0) -> None:
"""Take something a (bs4) element and an indentation level"""
self.context = context
self.level = level
@@ -88,7 +94,7 @@ def __str__(self):
"""Reuse the context method"""
return str(self.context)
- def __repr__(self):
+ def __repr__(self) -> str:
"""Try to make evident:
- the element type
@@ -102,15 +108,15 @@ def __repr__(self):
tag = self.tag
return f""
- def is_comment(self):
+ def is_comment(self) -> bool:
"""Check if this element is a comment"""
return isinstance(self.context, Comment)
- def is_doctype(self):
+ def is_doctype(self) -> bool:
"""Check if this element is a doctype"""
return isinstance(self.context, Doctype)
- def is_text(self):
+ def is_text(self) -> bool:
"""Check if this element is a text
Also comments and processing instructions
@@ -123,11 +129,11 @@ def is_text(self):
return False
return True
- def is_tag(self):
+ def is_tag(self) -> bool:
"""Check if this element is a notmal tag"""
return isinstance(self.context, Tag)
- def is_self_closing(self):
+ def is_self_closing(self) -> bool:
"""Is this element self closing?"""
if not self.is_tag():
raise ValueError("This is not a tag")
@@ -150,15 +156,15 @@ def is_self_closing(self):
# All the other elements will have an open an close tag
return False
- def is_null(self):
+ def is_null(self) -> bool:
"""We define a special tag null_tag_name to wrap text"""
return self.context.name == self.null_tag_name
- def is_soup(self):
+ def is_soup(self) -> bool:
"""Check if this element is a BeautifulSoup instance"""
return isinstance(self.context, BeautifulSoup)
- def is_processing_instruction(self):
+ def is_processing_instruction(self) -> bool:
"""Check if this element is a processing instruction like """
return isinstance(self.context, ProcessingInstruction)
@@ -189,12 +195,12 @@ def getchildren(self):
return children
@property
- def tag(self):
+ def tag(self) -> str | None:
"""Return the tag name"""
return self.context.name
@property
- def text(self):
+ def text(self) -> Comment | Stylesheet | str | Script:
"""Return the text contained in this element (if any)
Convert the text characters to html entities
@@ -254,14 +260,14 @@ def render_content(self):
return content
@property
- def prefix(self):
+ def prefix(self) -> str:
return self.indent * self.level
- def render_comment(self):
+ def render_comment(self) -> str:
"""Render a properly indented comment"""
return f"{self.prefix}"
- def render_doctype(self):
+ def render_doctype(self) -> str:
"""Render a properly indented comment"""
doctype = f"{self.prefix}{self.context.PREFIX}{self.text}{self.context.SUFFIX}"
if isinstance(
@@ -270,11 +276,11 @@ def render_doctype(self):
doctype = doctype.rstrip()
return doctype
- def render_processing_instruction(self):
+ def render_processing_instruction(self) -> str:
"""Render a properly indented processing instruction"""
return f"{self.prefix}{self.text.rstrip('?')}?>"
- def render_soup(self):
+ def render_soup(self) -> str:
first_child = next(self.context.children)
if isinstance(first_child, Doctype) and not self.context.is_xml:
return self.render_content()
@@ -282,7 +288,7 @@ def render_soup(self):
return self.render_content()
return f'\n{self.render_content()}'
- def render_text(self):
+ def render_text(self) -> str:
"""Render a properly indented text
If the text starts with spaces, strip them and add a newline.
@@ -330,7 +336,7 @@ def render_text(self):
text = "".join(rendered_lines)
return text
- def _render_template(self, template):
+ def _render_template(self, template: str) -> str:
return template.format(
before_closing_multiline=self.before_closing_multiline,
attributes=self.attributes.lstrip(),
@@ -338,7 +344,7 @@ def _render_template(self, template):
tag=self.tag,
)
- def render_self_closing(self):
+ def render_self_closing(self) -> str:
"""Render a properly indented a self closing tag"""
attributes_len = len(self.attributes)
if attributes_len == 0:
@@ -349,7 +355,7 @@ def render_self_closing(self):
template = self.self_closing_multiline_template
return self._render_template(template)
- def render_not_self_closing(self):
+ def render_not_self_closing(self) -> str:
"""Render a properly indented not self closing tag"""
attributes_len = len(self.attributes)
if attributes_len == 0:
diff --git a/zpretty/prettifier.py b/zpretty/prettifier.py
index b874f1c..017f67b 100644
--- a/zpretty/prettifier.py
+++ b/zpretty/prettifier.py
@@ -1,9 +1,13 @@
from bs4 import BeautifulSoup
from bs4.element import Doctype
from bs4.element import ProcessingInstruction
+from element import Tag
from logging import getLogger
+from typing import Union
from uuid import uuid4
from zpretty.elements import PrettyElement
+from zpretty.xml import XMLElement
+from zpretty.zcml import ZCMLElement
import fileinput
import re
@@ -30,7 +34,9 @@ class ZPrettifier:
_cdatas = []
_doctype = None
- def __init__(self, filename="", text="", encoding="utf8"):
+ def __init__(
+ self, filename: str = "", text: str = "", encoding: str = "utf8"
+ ) -> None:
"""Create a prettifier instance taking the contents
from a text or a filename
"""
@@ -62,7 +68,7 @@ def __init__(self, filename="", text="", encoding="utf8"):
self.root = self.pretty_element(self.soup, -1)
- def _prepare_text(self):
+ def _prepare_text(self) -> str:
"""This tweaks the text passed to the prettifier
to overcome some limitations of the BeautifulSoup parser
that wants to strip what he does not understand
@@ -96,7 +102,7 @@ def _prepare_text(self):
for line in text.splitlines()
).replace("&", self._ampersand_marker)
- def get_soup(self, text):
+ def get_soup(self, text: str) -> BeautifulSoup | Tag:
"""Tries to get the soup from the given test
If the text is not some xml like think a dummy element will be used to wrap it.
@@ -115,7 +121,7 @@ def get_soup(self, text):
wrapped_soup = BeautifulSoup(markup, self.parser)
return getattr(wrapped_soup, self.pretty_element.null_tag_name)
- def pretty_print(self, el):
+ def pretty_print(self, el: XMLElement | PrettyElement | ZCMLElement) -> str:
"""Pretty print an element indenting it based on level"""
prettified = (
el().replace(self._newlines_marker, "").replace(self._ampersand_marker, "&")
@@ -135,11 +141,11 @@ def pretty_print(self, el):
prettified += "\n"
return prettified
- def check(self):
+ def check(self) -> bool:
"""Checks if the input object should be prettified"""
return self.original_text == self()
- def __call__(self):
+ def __call__(self) -> str:
if not self.root.getchildren():
# The parsed content is not even something that looks like an XML
return self.original_text
diff --git a/zpretty/tests/mock.py b/zpretty/tests/mock.py
index d32b33d..849c445 100644
--- a/zpretty/tests/mock.py
+++ b/zpretty/tests/mock.py
@@ -2,6 +2,6 @@
class MockCLIRunner(CLIRunner):
- def __init__(self, *args):
+ def __init__(self, *args) -> None:
self.errors = []
self.config = self.parser.parse_args(args)
diff --git a/zpretty/tests/test_attributes.py b/zpretty/tests/test_attributes.py
index bcc40a4..1dae50b 100644
--- a/zpretty/tests/test_attributes.py
+++ b/zpretty/tests/test_attributes.py
@@ -1,4 +1,5 @@
from bs4 import BeautifulSoup
+from typing import Dict
from unittest import TestCase
from zpretty.attributes import PrettyAttributes
from zpretty.elements import PrettyElement
@@ -7,14 +8,16 @@
class TestZPrettyAttributess(TestCase):
"""Test zpretty"""
- def get_element(self, text, level=0):
+ def get_element(self, text: str, level: int = 0) -> PrettyElement:
"""Given a text return a PrettyElement"""
soup = BeautifulSoup(
"%s" % text, "html.parser"
)
return PrettyElement(soup.fake_root.next_element, level)
- def assertPrettifiedAttributes(self, attributes, expected, level=0):
+ def assertPrettifiedAttributes(
+ self, attributes: dict[str, str], expected: str, level: int = 0
+ ) -> None:
"""Check if the attributes are properly sorted and formatted"""
if level == 0:
el = None
@@ -24,22 +27,22 @@ def assertPrettifiedAttributes(self, attributes, expected, level=0):
observed = pretty_attribute()
self.assertEqual(observed, expected)
- def test_no_attributes(self):
+ def test_no_attributes(self) -> None:
self.assertPrettifiedAttributes({}, "")
self.assertPrettifiedAttributes({}, "", level=2)
- def test_one_attribute(self):
+ def test_one_attribute(self) -> None:
self.assertPrettifiedAttributes({"a": "1"}, 'a="1"')
self.assertPrettifiedAttributes({"a": "1"}, 'a="1"', level=1)
self.assertPrettifiedAttributes({"a": "1"}, 'a="1"', level=2)
- def test_value_with_double_quoptes(self):
+ def test_value_with_double_quoptes(self) -> None:
self.assertPrettifiedAttributes({"a": '"'}, "a='\"'")
- def test_transform_forbidden_characters(self):
+ def test_transform_forbidden_characters(self) -> None:
self.assertPrettifiedAttributes({"a": "> < &"}, 'a="> < &"')
- def test_many_attributes_attribute(self):
+ def test_many_attributes_attribute(self) -> None:
self.assertPrettifiedAttributes({"a": "1", "b": "2"}, 'a="1"\nb="2"')
self.assertPrettifiedAttributes(
{"a": "1", "b": "2"}, ' a="1"\n b="2"', level=1
@@ -48,13 +51,13 @@ def test_many_attributes_attribute(self):
{"a": "1", "b": "2"}, ' a="1"\n b="2"', level=2
)
- def test_tal_define(self):
+ def test_tal_define(self) -> None:
self.assertPrettifiedAttributes(
{"tal:define": "a 1; b 2"},
"\n".join(('tal:define="', " a 1;", " b 2;", '"')),
)
- def test_format_attributes_many_attribute(self):
+ def test_format_attributes_many_attribute(self) -> None:
self.assertPrettifiedAttributes(
{
"a": "1",
diff --git a/zpretty/tests/test_cli.py b/zpretty/tests/test_cli.py
index a431f08..56cb949 100644
--- a/zpretty/tests/test_cli.py
+++ b/zpretty/tests/test_cli.py
@@ -11,7 +11,7 @@
try:
from importlib.resources import files
- def resource_filename(package, resource):
+ def resource_filename(package: str, resource: str) -> str:
"""Get the resource filename for a package and resource."""
return str(files(package).joinpath(resource))
@@ -23,7 +23,7 @@ def resource_filename(package, resource):
class TestCli(TestCase):
"""Test the cli options"""
- def test_defaults(self):
+ def test_defaults(self) -> None:
config = MockCLIRunner().config
self.assertEqual(config.paths, "-")
self.assertFalse(config.inplace)
@@ -32,25 +32,25 @@ def test_defaults(self):
self.assertEqual(config.encoding, "utf8")
self.assertFalse(config.check)
- def test_short_options(self):
+ def test_short_options(self) -> None:
config = MockCLIRunner("-i", "-x", "-z").config
self.assertTrue(all((config.inplace, config.xml, config.zcml)))
- def test_long_options(self):
+ def test_long_options(self) -> None:
config = MockCLIRunner("--inplace", "--xml", "--zcml").config
self.assertTrue(all((config.inplace, config.xml, config.zcml)))
- def test_file(self):
+ def test_file(self) -> None:
html = resource_filename("zpretty.tests", "original/sample_html.html")
xml = resource_filename("zpretty.tests", "original/sample_xml.xml")
config = MockCLIRunner(html, xml).config
self.assertEqual(config.paths, [html, xml])
- def test_stdin(self):
+ def test_stdin(self) -> None:
clirunner = MockCLIRunner()
self.assertListEqual(clirunner.good_paths, ["-"])
- def test_broken_file_path(self):
+ def test_broken_file_path(self) -> None:
with TemporaryDirectory() as tmpdir:
bad_path = os.path.join(tmpdir, "bad path")
good_path = os.path.join(tmpdir, "good path")
@@ -61,7 +61,7 @@ def test_broken_file_path(self):
clirunner = MockCLIRunner(bad_path, good_path)
self.assertListEqual(clirunner.good_paths, [good_path])
- def test_choose_prettifier(self):
+ def test_choose_prettifier(self) -> None:
"""Check the for the given options and file the best choice is made"""
clirunner = MockCLIRunner("--xml", "--zcml")
self.assertEqual(clirunner.choose_prettifier(""), ZCMLPrettifier)
@@ -76,11 +76,11 @@ def test_choose_prettifier(self):
# The default one is returned if the extension is not recognized
self.assertEqual(clirunner.choose_prettifier("a.txt"), ZPrettifier)
- def test_check(self):
+ def test_check(self) -> None:
config = MockCLIRunner("--check").config
self.assertTrue(config.check)
- def test_run_check(self):
+ def test_run_check(self) -> None:
# XXX increase coverage by improving the mock
from unittest import mock
@@ -95,7 +95,7 @@ def test_run_check(self):
clirunner.run()
mocked.assert_called_once_with(1)
- def test_good_paths(self):
+ def test_good_paths(self) -> None:
"""Test the good_paths property"""
clirunner = MockCLIRunner()
self.assertListEqual(clirunner.good_paths, ["-"])
diff --git a/zpretty/tests/test_elements.py b/zpretty/tests/test_elements.py
index 9df6dbd..9e6ff43 100644
--- a/zpretty/tests/test_elements.py
+++ b/zpretty/tests/test_elements.py
@@ -6,14 +6,14 @@
class TestPrettyElements(TestCase):
"""Test basic funtionalities of the PrettyElement class"""
- def get_element(self, text, level=0):
+ def get_element(self, text: str, level: int = 0) -> PrettyElement:
"""Given a text return a PrettyElement"""
soup = BeautifulSoup(
"%s" % text, "html.parser"
)
return PrettyElement(soup.fake_root.next_element, level)
- def test_comment(self):
+ def test_comment(self) -> None:
el = self.get_element("")
self.assertFalse(el.is_processing_instruction())
self.assertFalse(el.is_doctype())
@@ -26,7 +26,7 @@ def test_comment(self):
self.assertEqual(el.attributes(), "")
self.assertEqual(el(), "")
- def test_text_element(self):
+ def test_text_element(self) -> None:
el = self.get_element("text")
self.assertFalse(el.is_comment())
self.assertFalse(el.is_doctype())
@@ -39,7 +39,7 @@ def test_text_element(self):
self.assertEqual(el.attributes(), "")
self.assertEqual(el(), "text")
- def test_empty_element(self):
+ def test_empty_element(self) -> None:
el = self.get_element("")
self.assertFalse(el.is_comment())
self.assertFalse(el.is_doctype())
@@ -51,7 +51,7 @@ def test_empty_element(self):
self.assertEqual(el.getchildren(), [])
self.assertEqual(el.attributes(), "")
- def test_empty_element_attributes(self):
+ def test_empty_element_attributes(self) -> None:
el = self.get_element('')
self.assertFalse(el.is_comment())
self.assertFalse(el.is_doctype())
@@ -64,7 +64,7 @@ def test_empty_element_attributes(self):
self.assertEqual(el.attributes(), 'class="b"')
self.assertEqual(el(), '')
- def test_processing_instruction(self):
+ def test_processing_instruction(self) -> None:
el = self.get_element('')
self.assertFalse(el.is_tag())
self.assertFalse(el.is_text())
@@ -75,7 +75,7 @@ def test_processing_instruction(self):
self.assertEqual(el.getchildren(), [])
self.assertEqual(el.attributes(), "")
- def test_doctype(self):
+ def test_doctype(self) -> None:
el = self.get_element("")
self.assertFalse(el.is_tag())
self.assertFalse(el.is_text())
@@ -95,7 +95,7 @@ def test_doctype(self):
'"http://www.w3.org/TR/html4/strict.dtd"',
)
- def test_render_text(self):
+ def test_render_text(self) -> None:
el = self.get_element(" a")
self.assertEqual(el.render_text(), "\na")
el = self.get_element(" ")
@@ -103,7 +103,7 @@ def test_render_text(self):
el = self.get_element("\n")
self.assertEqual(el.render_text(), "\n")
- def test_get_parent(self):
+ def test_get_parent(self) -> None:
el = self.get_element(" a")
self.assertEqual(el.getparent().tag, "fake_root")
self.assertEqual(el.getparent().getparent().tag, "soup")
diff --git a/zpretty/tests/test_functions.py b/zpretty/tests/test_functions.py
index a84cb5a..954d801 100644
--- a/zpretty/tests/test_functions.py
+++ b/zpretty/tests/test_functions.py
@@ -8,19 +8,19 @@
class TestFunctions(TestCase):
"""Test functions used by zpretty"""
- def test_lstrip_first_line_oneline(self):
+ def test_lstrip_first_line_oneline(self) -> None:
self.assertEqual(lstrip_first_line(" a"), "a")
- def test_lstrip_first_line_twolines(self):
+ def test_lstrip_first_line_twolines(self) -> None:
self.assertEqual(lstrip_first_line(" a \n b"), ("a \n b"))
- def test_rstrip_larst_line_oneline(self):
+ def test_rstrip_larst_line_oneline(self) -> None:
self.assertEqual(rstrip_last_line("a"), "a")
- def test_rstrip_larst_line_twoline(self):
+ def test_rstrip_larst_line_twoline(self) -> None:
self.assertEqual(rstrip_last_line("a\n b "), ("a\n b"))
- def test_none(self):
+ def test_none(self) -> None:
self.assertEqual(lstrip_first_line(None), None)
self.assertEqual(rstrip_last_line(None), None)
self.assertFalse(endswith_whitespace(None))
diff --git a/zpretty/tests/test_readme.py b/zpretty/tests/test_readme.py
index 10c58f9..c578a42 100644
--- a/zpretty/tests/test_readme.py
+++ b/zpretty/tests/test_readme.py
@@ -1,4 +1,5 @@
from pathlib import Path
+from typing import List
from unittest import TestCase
from zpretty.tests.mock import MockCLIRunner
@@ -9,7 +10,7 @@
try:
from importlib.resources import files
- def resource_filename(package, resource):
+ def resource_filename(package: str, resource: str) -> str:
"""Get the resource filename for a package and resource."""
return str(files(package).joinpath(resource))
@@ -23,7 +24,7 @@ class TestReadme(TestCase):
maxDiff = None
- def extract_usage_from_readme(self):
+ def extract_usage_from_readme(self) -> list[str]:
"""Extract the usage from the documentation"""
resolved_filename = Path(resource_filename("zpretty", ".")) / ".." / "README.md"
@@ -40,7 +41,7 @@ def extract_usage_from_readme(self):
# Take all the lines ignoring whitespaces
return [x.strip() for x in readme[start:end].splitlines()]
- def extract_usage_from_parser(self):
+ def extract_usage_from_parser(self) -> list[str]:
"""Ask the parser for the usage and indent it"""
parser = MockCLIRunner().parser
# This is needed to keep the 100 lines limit
@@ -53,7 +54,7 @@ def extract_usage_from_parser(self):
# Take all the lines ignoring whitespaces
return [x.strip() for x in parser_help.splitlines()]
- def test_readme(self):
+ def test_readme(self) -> None:
observed = self.extract_usage_from_readme()
expected = self.extract_usage_from_parser()
self.assertListEqual(observed, expected)
diff --git a/zpretty/tests/test_xml.py b/zpretty/tests/test_xml.py
index ac3ad0f..0e6d7f9 100644
--- a/zpretty/tests/test_xml.py
+++ b/zpretty/tests/test_xml.py
@@ -7,7 +7,7 @@
try:
from importlib.resources import files
- def resource_filename(package, resource):
+ def resource_filename(package: str, resource: str) -> str:
"""Get the resource filename for a package and resource."""
return str(files(package).joinpath(resource))
@@ -21,14 +21,14 @@ class TestZpretty(TestCase):
maxDiff = None
- def get_element(self, text, level=0):
+ def get_element(self, text: str, level: int = 0) -> XMLElement:
"""Given a text return a XMLElement"""
soup = BeautifulSoup(
"%s" % text, "html.parser"
)
return XMLElement(soup.fake_root.next_element, level)
- def prettify(self, filename):
+ def prettify(self, filename: str) -> None:
"""Run prettify on filename and check that the output is equal to
the file content itself
"""
@@ -38,16 +38,16 @@ def prettify(self, filename):
expected = open(resolved_filename).read()
self.assertListEqual(observed.splitlines(), expected.splitlines())
- def test_newline_between_attributes(self):
+ def test_newline_between_attributes(self) -> None:
"""See #84"""
element = self.get_element('')
self.assertEqual(element(), '')
- def test_zcml(self):
+ def test_zcml(self) -> None:
self.prettify("sample_xml.xml")
- def test_sample_dtml(self):
+ def test_sample_dtml(self) -> None:
self.prettify("sample_dtml.dtml")
- def test_sample_txt(self):
+ def test_sample_txt(self) -> None:
self.prettify("sample.txt")
diff --git a/zpretty/tests/test_zcml.py b/zpretty/tests/test_zcml.py
index 21a44b5..ca3328a 100644
--- a/zpretty/tests/test_zcml.py
+++ b/zpretty/tests/test_zcml.py
@@ -1,4 +1,6 @@
from bs4 import BeautifulSoup
+from typing import Dict
+from typing import Union
from unittest import TestCase
from zpretty.zcml import ZCMLAttributes
from zpretty.zcml import ZCMLElement
@@ -8,7 +10,7 @@
try:
from importlib.resources import files
- def resource_filename(package, resource):
+ def resource_filename(package: str, resource: str) -> str:
"""Get the resource filename for a package and resource."""
return str(files(package).joinpath(resource))
@@ -22,14 +24,16 @@ class TestZpretty(TestCase):
maxDiff = None
- def get_element(self, text, level=0):
+ def get_element(self, text: str, level: int = 0) -> ZCMLElement:
"""Given a text return a PrettyElement"""
soup = BeautifulSoup(
"%s" % text, "html.parser"
)
return ZCMLElement(soup.fake_root.next_element, level)
- def assertPrettifiedAttributes(self, attributes, expected, level=0):
+ def assertPrettifiedAttributes(
+ self, attributes: dict[str, str] | str, expected: str, level: int = 0
+ ) -> None:
"""Check if the attributes are properly sorted and formatted"""
if level == 0:
el = None
@@ -39,16 +43,16 @@ def assertPrettifiedAttributes(self, attributes, expected, level=0):
observed = pretty_attribute()
self.assertEqual(observed, expected)
- def test_zcml_attributes_no_attributes(self):
+ def test_zcml_attributes_no_attributes(self) -> None:
self.assertPrettifiedAttributes(ZCMLAttributes({})(), "")
self.assertPrettifiedAttributes({}, "", level=2)
- def test_zcml_attributes_one_attributes(self):
+ def test_zcml_attributes_one_attributes(self) -> None:
self.assertPrettifiedAttributes({"a": "1"}, 'a="1"')
self.assertPrettifiedAttributes({"a": "1"}, 'a="1"', level=1)
self.assertPrettifiedAttributes({"a": "1"}, 'a="1"', level=2)
- def test_zcml_attributes_many_attributes(self):
+ def test_zcml_attributes_many_attributes(self) -> None:
self.assertPrettifiedAttributes({"a": "1", "b": "2"}, ' a="1"\n b="2"')
self.assertPrettifiedAttributes(
{"a": "1", "b": "2"}, ' a="1"\n b="2"', level=1
@@ -57,7 +61,7 @@ def test_zcml_attributes_many_attributes(self):
{"a": "1", "b": "2"}, ' a="1"\n b="2"', level=2
)
- def test_zcml_self_closing_no_attributes(self):
+ def test_zcml_self_closing_no_attributes(self) -> None:
element = self.get_element("")
self.assertEqual(element(), "")
element = self.get_element("", 1)
@@ -65,7 +69,7 @@ def test_zcml_self_closing_no_attributes(self):
element = self.get_element("", 2)
self.assertEqual(element(), " ")
- def test_zcml_self_closing_one_attributes(self):
+ def test_zcml_self_closing_one_attributes(self) -> None:
element = self.get_element('')
self.assertEqual(element(), '')
element = self.get_element('', 1)
@@ -73,7 +77,7 @@ def test_zcml_self_closing_one_attributes(self):
element = self.get_element('', 2)
self.assertEqual(element(), ' ')
- def test_zcml_self_closing_many_attributes(self):
+ def test_zcml_self_closing_many_attributes(self) -> None:
element = self.get_element('')
self.assertEqual(
element(),
@@ -111,7 +115,7 @@ def test_zcml_self_closing_many_attributes(self):
),
)
- def test_for_attribute_single_attribute(self):
+ def test_for_attribute_single_attribute(self) -> None:
element = self.get_element('')
self.assertEqual(element(), '')
element = self.get_element('', 1)
@@ -152,7 +156,7 @@ def test_for_attribute_single_attribute(self):
),
)
- def test_for_attribute_multiple_attribute(self):
+ def test_for_attribute_multiple_attribute(self) -> None:
element = self.get_element('')
self.assertEqual(
element(),
@@ -230,7 +234,7 @@ def test_for_attribute_multiple_attribute(self):
),
)
- def prettify(self, filename):
+ def prettify(self, filename: str) -> None:
"""Run prettify on filename and check that the output is equal to
the file content itself
"""
@@ -240,5 +244,5 @@ def prettify(self, filename):
expected = open(resolved_filename).read()
self.assertListEqual(observed.splitlines(), expected.splitlines())
- def test_zcml(self):
+ def test_zcml(self) -> None:
self.prettify("sample.zcml")
diff --git a/zpretty/tests/test_zpretty.py b/zpretty/tests/test_zpretty.py
index c5a243d..44c12cd 100644
--- a/zpretty/tests/test_zpretty.py
+++ b/zpretty/tests/test_zpretty.py
@@ -1,3 +1,5 @@
+from typing import Tuple
+from typing import Union
from unittest import TestCase
from zpretty.prettifier import ZPrettifier
@@ -5,7 +7,7 @@
try:
from importlib.resources import files
- def resource_filename(package, resource):
+ def resource_filename(package: str, resource: str) -> str:
"""Get the resource filename for a package and resource."""
return str(files(package).joinpath(resource))
@@ -19,7 +21,12 @@ class TestZpretty(TestCase):
maxDiff = None
- def assertPrettified(self, original, expected, encoding="utf8"):
+ def assertPrettified(
+ self,
+ original: str,
+ expected: tuple[str, str, str, str] | str | tuple[str, str, str],
+ encoding: str = "utf8",
+ ) -> None:
"""Check if the original html has been prettified as expected"""
if isinstance(expected, tuple):
expected = "\n".join(expected)
@@ -28,7 +35,7 @@ def assertPrettified(self, original, expected, encoding="utf8"):
observed = prettifier()
self.assertEqual(observed, expected)
- def prettify(self, filename):
+ def prettify(self, filename: str) -> None:
"""Run prettify on filename and check that the output is equal to
the file content itself
"""
@@ -39,10 +46,10 @@ def prettify(self, filename):
expected = open(resolved_filename).read()
self.assertListEqual(observed.splitlines(), expected.splitlines())
- def test_format_self_closing_tag(self):
+ def test_format_self_closing_tag(self) -> None:
self.assertPrettified("", "\n")
- def test_nesting_no_text(self):
+ def test_nesting_no_text(self) -> None:
# no attributes
self.assertPrettified(
"", "\n"
@@ -64,7 +71,7 @@ def test_nesting_no_text(self):
"\n",
)
- def test_nesting_with_text(self):
+ def test_nesting_with_text(self) -> None:
# no attributes
self.assertPrettified(
" a", "\n a\n"
@@ -101,7 +108,7 @@ def test_nesting_with_text(self):
)
self.assertPrettified("", "\n")
- def test_nesting_with_tail(self):
+ def test_nesting_with_tail(self) -> None:
# no attributes
self.assertPrettified(
"\n\n",
@@ -122,7 +129,7 @@ def test_nesting_with_tail(self):
" a ", "\n a\n\n"
)
- def test_many_children(self):
+ def test_many_children(self) -> None:
""""""
self.assertPrettified(
"",
@@ -133,7 +140,7 @@ def test_many_children(self):
"\n \n",
)
- def test_boolean_attributes(self):
+ def test_boolean_attributes(self) -> None:
"""Test attributes without value
(hidden, required, data-attributes, ...)
Some of them are rendered valueless, some other not.
@@ -144,28 +151,28 @@ def test_boolean_attributes(self):
self.assertPrettified("", "\n")
self.assertPrettified("", '\n')
- def test_fix_self_closing(self):
+ def test_fix_self_closing(self) -> None:
"""Check if open self closing tags are rendered correctly"""
self.assertPrettified("
", "
\n")
self.assertPrettified("", "\n")
self.assertPrettified("", "\n")
- def test_element_repr(self):
+ def test_element_repr(self) -> None:
prettifier = ZPrettifier(text="")
self.assertEqual(repr(prettifier.root), "")
- def test_whitelines_not_stripped(self):
+ def test_whitelines_not_stripped(self) -> None:
self.assertPrettified("\n", "\n\n")
self.assertPrettified(
"\n Hello! \n", "\n Hello!\n\n"
)
- def test_text_close_to_an_element(self):
+ def test_text_close_to_an_element(self) -> None:
self.assertPrettified(
"\n ()\n", "\n ()\n\n"
)
- def test_elements_with_new_lines(self):
+ def test_elements_with_new_lines(self) -> None:
self.assertPrettified("", "\n")
self.assertPrettified(
'',
@@ -176,46 +183,46 @@ def test_elements_with_new_lines(self):
('\n'),
)
- def test_entities(self):
+ def test_entities(self) -> None:
self.assertPrettified(" ", " \n")
- def test_single_quotes_in_attrs(self):
+ def test_single_quotes_in_attrs(self) -> None:
self.assertPrettified('', '\n')
- def test_ampersand_in_attrs(self):
+ def test_ampersand_in_attrs(self) -> None:
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
- def test_escaped_ampersand_in_attrs(self):
+ def test_escaped_ampersand_in_attrs(self) -> None:
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
self.assertPrettified('', '\n')
- def test_ampersand_and_column_in_separate_attrs(self):
+ def test_ampersand_and_column_in_separate_attrs(self) -> None:
self.assertPrettified(
'\n',
'\n\n',
)
- def test_sample_html(self):
+ def test_sample_html(self) -> None:
self.prettify("sample_html.html")
- def test_sample_html4(self):
+ def test_sample_html4(self) -> None:
self.prettify("sample_html4.html")
- def test_sample_html_with_preprocessing_instruction(self):
+ def test_sample_html_with_preprocessing_instruction(self) -> None:
self.prettify("sample_html_with_preprocessing_instruction.html")
- def test_sample_pt(self):
+ def test_sample_pt(self) -> None:
self.prettify("sample_pt.pt")
- def test_text_with_markup(self):
+ def test_text_with_markup(self) -> None:
self.prettify("text_with_markup.md")
- def test_text_file(self):
+ def test_text_file(self) -> None:
self.prettify("sample.txt")
diff --git a/zpretty/text.py b/zpretty/text.py
index 29e3060..48b38cc 100644
--- a/zpretty/text.py
+++ b/zpretty/text.py
@@ -1,4 +1,7 @@
-def startswith_whitespace(text):
+from typing import Optional
+
+
+def startswith_whitespace(text: str | None) -> bool:
"""Check if text starts with a whitespace
If text is not a string return False
@@ -8,7 +11,7 @@ def startswith_whitespace(text):
return text[:1].isspace()
-def endswith_whitespace(text):
+def endswith_whitespace(text: str | None) -> bool:
"""Check if text ends with a whitespace
If text is not a string return False
@@ -18,7 +21,7 @@ def endswith_whitespace(text):
return text[-1:].isspace()
-def lstrip_first_line(text):
+def lstrip_first_line(text: str | None) -> str | None:
"""lstrip only the first line of text"""
if not text:
return text
@@ -29,7 +32,7 @@ def lstrip_first_line(text):
return "\n".join(lines)
-def rstrip_last_line(text):
+def rstrip_last_line(text: str | None) -> str | None:
"""rstrip only the last line of text"""
if not text:
return text
diff --git a/zpretty/xml.py b/zpretty/xml.py
index 5055e8a..32e89ba 100644
--- a/zpretty/xml.py
+++ b/zpretty/xml.py
@@ -1,7 +1,11 @@
from bs4 import BeautifulSoup
from bs4.builder import LXMLTreeBuilderForXML
+from bs4.element import Comment
+from bs4.element import NamespacedAttribute
from bs4.element import NavigableString
from logging import getLogger
+from typing import Tuple
+from typing import Union
from zpretty.attributes import PrettyAttributes
from zpretty.elements import PrettyElement
from zpretty.prettifier import ZPrettifier
@@ -11,7 +15,7 @@
class AnyIn:
- def __contains__(self, item):
+ def __contains__(self, item: str) -> bool:
return True
@@ -25,7 +29,9 @@ class XMLAttributes(PrettyAttributes):
_xml_attribute_order = ()
_tal_attribute_order = ()
- def sort_attributes(self, name):
+ def sort_attributes(
+ self, name: str | NamespacedAttribute
+ ) -> tuple[int, str] | tuple[int, NamespacedAttribute]:
"""Sort ZCML attributes in a consistent way"""
if name in self._xml_attribute_order:
return (100 + self._xml_attribute_order.index(name), name)
@@ -36,7 +42,7 @@ class XMLElement(PrettyElement):
attribute_klass = XMLAttributes
preserve_text_whitespace_elements = AnyIn()
- def is_self_closing(self):
+ def is_self_closing(self) -> bool:
"""Is this element self closing?"""
if not self.is_tag():
raise ValueError("This is not a tag")
@@ -44,7 +50,7 @@ def is_self_closing(self):
return not self.getchildren()
@property
- def tag(self):
+ def tag(self) -> str:
"""Return the tag name"""
prefix = getattr(self.context, "prefix", "")
if not prefix:
@@ -52,7 +58,7 @@ def tag(self):
return f"{prefix}:{self.context.name}"
@property
- def text(self):
+ def text(self) -> str | Comment:
"""Return the text contained in this element (if any)
Convert the text characters to html entities
@@ -70,7 +76,7 @@ class XMLPrettifier(ZPrettifier):
parser = "xml"
pretty_element = XMLElement
- def get_soup(self, text):
+ def get_soup(self, text: str) -> BeautifulSoup:
"""Tries to get the soup from the given test
If the text is not some xml like think a dummy element will be used to wrap it.
diff --git a/zpretty/zcml.py b/zpretty/zcml.py
index e308b80..a9bc8af 100644
--- a/zpretty/zcml.py
+++ b/zpretty/zcml.py
@@ -1,5 +1,8 @@
from bs4 import BeautifulSoup
+from bs4.element import Tag
from logging import getLogger
+from typing import Tuple
+from typing import Union
from zpretty.xml import XMLAttributes
from zpretty.xml import XMLElement
from zpretty.xml import XMLPrettifier
@@ -468,7 +471,17 @@ class ZCMLAttributes(XMLAttributes):
)
@property
- def _xml_attribute_order(self):
+ def _xml_attribute_order(
+ self,
+ ) -> (
+ tuple[str, str, str, str]
+ | tuple[str, str, str, str, str, str, str]
+ | tuple[str, str, str]
+ | tuple[
+ str, str, str, str, str, str, str, str, str, str, str, str, str, str, str
+ ]
+ | tuple[str, str, str, str, str, str, str, str, str, str, str]
+ ):
"""Sort the attributes based on the element
_xml_attribute_order_by_ns_and_tag comments contain references
@@ -493,7 +506,7 @@ def _xml_attribute_order(self):
mapping_by_namespace = self._xml_attribute_order_by_ns_and_tag.get(ns, {})
return mapping_by_namespace.get(name, self._xml_attribute_order_fallback)
- def format_multiline(self, name, value):
+ def format_multiline(self, name: str, value: str) -> str:
"""We have two cases according if we have just one attribute or more
1. single attribute
@@ -528,11 +541,11 @@ def format_multiline(self, name, value):
)
return line_joiner.join(value_lines)
- def lstrip(self):
+ def lstrip(self) -> str:
"""Actually we do not want to remove the spaces"""
return self()
- def __call__(self):
+ def __call__(self) -> str:
"""Render the attributes as text
Render and an empty string if no attributes
@@ -571,7 +584,7 @@ class ZCMLPrettifier(XMLPrettifier):
pretty_element = ZCMLElement
- def get_soup(self, text):
+ def get_soup(self, text: str) -> Tag:
"""Tries to get the soup from the given text"""
markup = "<{null}>{text}{null}>".format(
null=self.pretty_element.null_tag_name, text=text