Skip to content
Open
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
18 changes: 11 additions & 7 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
# Changelog

## 4.0.1 (unreleased)
## 4.1.0 (unreleased)

- Add a switch for multiline class attribute values.
Per default class attributes are not split into multiple lines. That's what
most tools do and most people would expect. To split class attributes into
multiple lines use the `--split-class` option on the CLI.
[thet]

- Nothing changed yet.

- Multiline values for class attributes.
Split multiple class attribute values into multiple lines. Keep it
single-lined, if there is only one value. And don't split Chameleon
expressions.
[thet]

## 4.0.0 (2026-04-10)


- Declare support for Python 3.10 - 3.14
[ale-rt]
- Do not break `title` and `textarea` in page templates.
(Fixes #198)
[ale-rt]


## 3.1.1 (2025-06-23)

- When parsing XML files, preserve space in all the tags
Expand All @@ -24,10 +30,8 @@
- Declare support for Python 3.13
[ale-rt]


## 3.1.0 (2023-06-30)


- No changes were made from the latest alpha version
[ale-rt]

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ Basic usage:

```console
$ zpretty -h
usage: zpretty [-h] [--encoding ENCODING] [-i] [-v] [-x] [-z] [--check]
[--include INCLUDE] [--exclude EXCLUDE]
usage: zpretty [-h] [--encoding ENCODING] [-i] [-v] [-x] [-z] [--split-class]
[--check] [--include INCLUDE] [--exclude EXCLUDE]
[--extend-exclude EXTEND_EXCLUDE]
[paths ...]

Expand All @@ -88,6 +88,7 @@ options:
-x, --xml Treat the input file(s) as XML
-z, --zcml Treat the input file(s) as XML. Follow the ZCML
styleguide
--split-class Split CSS class attribute values into multiple lines.
--check Return code 0 if nothing would be changed, 1 if some
files would be reformatted
--include INCLUDE A regular expression that matches files and directories
Expand Down
63 changes: 58 additions & 5 deletions zpretty/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,15 @@ class PrettyAttributes:
"i18n:ignore-attributes",
)

def __init__(self, attributes, element=None):
def __init__(self, config, attributes, element=None):
"""attributes is a dict like object"""
self.config = config
self.attributes = attributes
self.element = element

if self.config.split_class:
self._multiline_attributes += ("class",)

def __len__(self):
return len(self.attributes)

Expand Down Expand Up @@ -129,11 +133,60 @@ def sort_attributes(self, name):
return (900, name)
return (200, name)

def split_outside_braces(self, value: str) -> list[str]:
"""Empty space splitter that respects Chameleon expressions.

It splits on ` ` except for expressions within `{}`
"""
parts = []
current = []
depth = 0

for char in value:
if char == "{":
depth += 1
current.append(char)
elif char == "}":
depth -= 1
current.append(char)
elif char == " " and depth == 0:
if current:
parts.append("".join(current))
current = []
else:
current.append(char)

if current:
parts.append("".join(current))

return parts

def format_multiline(self, name, value):
""""""
value_lines = filter(None, value.split())
line_joiner = "\n" + (" " * (len(name) + 2))
return line_joiner.join(value_lines)
"""Format attributes values to multiline, split on an empty space.` `
except for expressions within `{}`
"""

lines = self.split_outside_braces(value)

# Don't split single-values
if len(lines) < 2:
return value

try:
line_prefix = self.element.prefix + self.prefix + self._multiline_prefix
except AttributeError:
line_prefix = self._multiline_prefix

# unindent at the end
line_prefix_end = line_prefix[:-2]

# Add line indents
new_value = line_prefix + f"\n{line_prefix}".join(lines)

# Line break at start and end and add end indent.
new_value = f"\n{new_value}\n{line_prefix_end}"

return new_value

def format_tal_multiline(self, value):
"""There are some tal specific attributes that contain ; separated
Expand Down
9 changes: 8 additions & 1 deletion zpretty/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ def parser(self):
dest="zcml",
default=False,
)
parser.add_argument(
"--split-class",
help="Split CSS class attribute values into multiple lines.",
action="store_true",
dest="split_class",
default=False,
)
parser.add_argument(
"--check",
help=(
Expand Down Expand Up @@ -212,7 +219,7 @@ def run(self):
for path in self.good_paths:
# use Pathlib to check if the file exists and it is a file
Prettifier = self.choose_prettifier(path)
prettifier = Prettifier(path, encoding=encoding)
prettifier = Prettifier(self.config, path, encoding=encoding)
if self.config.check:
if not prettifier.check():
self.errors.append(f"This file would be rewritten: {path}")
Expand Down
9 changes: 5 additions & 4 deletions zpretty/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ class PrettyElement:
"textarea",
]

def __init__(self, context, level=0):
def __init__(self, config, context, level=0):
"""Take something a (bs4) element and an indentation level"""
self.config = config
self.context = context
self.level = level

Expand Down Expand Up @@ -180,15 +181,15 @@ def getparent(self):
parent = self.context.parent
if not parent or parent.name == BeautifulSoup.ROOT_TAG_NAME:
return None
return self.__class__(parent)
return self.__class__(self.config, parent)

@memo
def getchildren(self):
"""Return this element children as instances of this class"""
children = []
next_level = self.level + 1
for child in getattr(self.context, "children", []):
child = self.__class__(child, next_level)
child = self.__class__(self.config, child, next_level)
try:
child.is_tag() and child.is_self_closing()
except OpenTagException:
Expand Down Expand Up @@ -227,7 +228,7 @@ def text(self):
def attributes(self):
"""Return the wrapped attributes"""
attributes = getattr(self.context, "attrs", {})
return self.attribute_klass(attributes, self)
return self.attribute_klass(self.config, attributes, self)

@property
@memo
Expand Down
5 changes: 3 additions & 2 deletions zpretty/prettifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ class ZPrettifier:
_cdatas = []
_doctype = None

def __init__(self, filename="", text="", encoding="utf8"):
def __init__(self, config, filename="", text="", encoding="utf8"):
"""Create a prettifier instance taking the contents
from a text or a filename
"""
self.config = config
self._entity_mapping = {}
self.encoding = encoding
self.filename = filename
Expand Down Expand Up @@ -67,7 +68,7 @@ def __init__(self, filename="", text="", encoding="utf8"):
for el in self.soup.find_all(attrs={key: ""}):
el.attrs.pop(key, None)

self.root = self.pretty_element(self.soup, -1)
self.root = self.pretty_element(config, self.soup, -1)

def fix_rcdata_markup(self, soup):
"""Parse markup-like text inside RCDATA tags as child nodes.
Expand Down
54 changes: 51 additions & 3 deletions zpretty/tests/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from zpretty.elements import PrettyElement


class FakeConfig:
split_class = False


class TestZPrettyAttributess(TestCase):
"""Test zpretty"""

Expand All @@ -12,15 +16,18 @@ def get_element(self, text, level=0):
soup = BeautifulSoup(
"<soup><fake_root>%s</fake_root></soup>" % text, "html.parser"
)
return PrettyElement(soup.fake_root.next_element, level)
return PrettyElement(FakeConfig(), soup.fake_root.next_element, level)

def assertPrettifiedAttributes(self, attributes, expected, level=0):
def assertPrettifiedAttributes(self, attributes, expected, level=0, config=None):
"""Check if the attributes are properly sorted and formatted"""
if level == 0:
el = None
else:
el = self.get_element("a", level)
pretty_attribute = PrettyAttributes(attributes, el)

config = config if config else FakeConfig()

pretty_attribute = PrettyAttributes(config, attributes, el)
observed = pretty_attribute()
self.assertEqual(observed, expected)

Expand Down Expand Up @@ -77,3 +84,44 @@ def test_format_attributes_many_attribute(self):
)
),
)

def test_class_multiline_no_split(self):
"""Class attributes with multiple values should be split."""
self.assertPrettifiedAttributes(
{"class": "class1 class2 ${python: 'class3' if True else ''} class4"},
'''class="class1 class2 ${python: 'class3' if True else ''} class4"''',
)

def test_class_multiline(self):
"""Class attributes with multiple values should be split."""
config = FakeConfig()
config.split_class = True
self.assertPrettifiedAttributes(
{"class": "class1 class2 ${python: 'class3' if True else ''} class4"},
"\n".join(
(
'class="',
" class1",
" class2",
" ${python: 'class3' if True else ''}",
" class4",
'"',
)
),
0,
config,
)

def test_data_pat_singleline(self):
"""Class with single values should not be split."""
config = FakeConfig()
config.split_class = True
self.assertPrettifiedAttributes(
{"class": "class1"}, 'class="class1"', 0, config
)

def test_data_pat_empty(self):
"""Empty class attributes should not be split."""
config = FakeConfig()
config.split_class = True
self.assertPrettifiedAttributes({"class": ""}, 'class=""', 0, config)
6 changes: 5 additions & 1 deletion zpretty/tests/test_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from zpretty.elements import PrettyElement


class FakeConfig:
split_class = False


class TestPrettyElements(TestCase):
"""Test basic funtionalities of the PrettyElement class"""

Expand All @@ -11,7 +15,7 @@ def get_element(self, text, level=0):
soup = BeautifulSoup(
"<soup><fake_root>%s</fake_root></soup>" % text, "html.parser"
)
return PrettyElement(soup.fake_root.next_element, level)
return PrettyElement(FakeConfig(), soup.fake_root.next_element, level)

def test_comment(self):
el = self.get_element("<!--a-->")
Expand Down
8 changes: 6 additions & 2 deletions zpretty/tests/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from zpretty.xml import XMLPrettifier


class FakeConfig:
split_class = False


class TestZpretty(TestCase):
"""Test zpretty"""

Expand All @@ -17,14 +21,14 @@ def get_element(self, text, level=0):
soup = BeautifulSoup(
"<soup><fake_root>%s</fake_root></soup>" % text, "html.parser"
)
return XMLElement(soup.fake_root.next_element, level)
return XMLElement(FakeConfig(), soup.fake_root.next_element, level)

def prettify(self, filename):
"""Run prettify on filename and check that the output is equal to
the file content itself
"""
filename_path = self.sample_folder_path / filename
prettifier = XMLPrettifier(filename_path)
prettifier = XMLPrettifier(FakeConfig(), filename_path)
observed = prettifier()
expected = filename_path.read_text()
self.assertListEqual(observed.splitlines(), expected.splitlines())
Expand Down
12 changes: 8 additions & 4 deletions zpretty/tests/test_zcml.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from zpretty.zcml import ZCMLPrettifier


class FakeConfig:
split_class = False


class TestZpretty(TestCase):
"""Test zpretty"""

Expand All @@ -17,20 +21,20 @@ def get_element(self, text, level=0):
soup = BeautifulSoup(
"<soup><fake_root>%s</fake_root></soup>" % text, "html.parser"
)
return ZCMLElement(soup.fake_root.next_element, level)
return ZCMLElement(FakeConfig(), soup.fake_root.next_element, level)

def assertPrettifiedAttributes(self, attributes, expected, level=0):
"""Check if the attributes are properly sorted and formatted"""
if level == 0:
el = None
else:
el = self.get_element("foo", level)
pretty_attribute = ZCMLAttributes(attributes, el)
pretty_attribute = ZCMLAttributes(FakeConfig(), attributes, el)
observed = pretty_attribute()
self.assertEqual(observed, expected)

def test_zcml_attributes_no_attributes(self):
self.assertPrettifiedAttributes(ZCMLAttributes({})(), "")
self.assertPrettifiedAttributes(ZCMLAttributes(FakeConfig(), {})(), "")
self.assertPrettifiedAttributes({}, "", level=2)

def test_zcml_attributes_one_attributes(self):
Expand Down Expand Up @@ -225,7 +229,7 @@ def prettify(self, filename):
the file content itself
"""
filename_path = self.sample_folder_path / filename
prettifier = ZCMLPrettifier(filename_path)
prettifier = ZCMLPrettifier(FakeConfig(), filename_path)
observed = prettifier()
expected = filename_path.read_text()
self.assertListEqual(observed.splitlines(), expected.splitlines())
Expand Down
Loading
Loading