Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
/dist/
/htmlcov/
/man/
/monkeytype.sqlite3
/.venv/
36 changes: 25 additions & 11 deletions zpretty/attributes.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -192,15 +204,17 @@ 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
return 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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
50 changes: 28 additions & 22 deletions zpretty/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -102,15 +108,15 @@ def __repr__(self):
tag = self.tag
return f"<pretty:{self.level}:{tag} />"

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
Expand All @@ -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")
Expand All @@ -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 <?xml...>"""
return isinstance(self.context, ProcessingInstruction)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}<!--{self.text}-->"

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(
Expand All @@ -270,19 +276,19 @@ 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()
if isinstance(first_child, ProcessingInstruction):
return self.render_content()
return f'<?xml version="1.0" encoding="utf-8"?>\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.
Expand Down Expand Up @@ -330,15 +336,15 @@ 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(),
prefix=self.prefix,
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:
Expand All @@ -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:
Expand Down
18 changes: 12 additions & 6 deletions zpretty/prettifier.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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, "&")
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion zpretty/tests/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading
Loading