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}" - 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("

a

", "

a

\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}".format( null=self.pretty_element.null_tag_name, text=text