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
118 changes: 57 additions & 61 deletions commitizen/changelog_formats/restructuredtext.py
Original file line number Diff line number Diff line change
@@ -1,92 +1,88 @@
from __future__ import annotations

import sys
from itertools import zip_longest
from typing import IO, TYPE_CHECKING, Any, Union
from typing import IO

from commitizen.changelog import Metadata

from .base import BaseFormat

if TYPE_CHECKING:
# TypeAlias is Python 3.10+ but backported in typing-extensions
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias


# Can't use `|` operator and native type because of https://bugs.python.org/issue42233 only fixed in 3.10
TitleKind: TypeAlias = Union[str, tuple[str, str]]


class RestructuredText(BaseFormat):
extension = "rst"

def get_metadata_from_file(self, file: IO[Any]) -> Metadata:
def get_metadata_from_file(self, file: IO[str]) -> Metadata:
"""
RestructuredText section titles are not one-line-based,
they spread on 2 or 3 lines and levels are not predefined
but determined byt their occurrence order.
but determined by their occurrence order.

It requires its own algorithm.

For a more generic approach, you need to rely on `docutils`.
"""
meta = Metadata()
unreleased_title_kind: TitleKind | None = None
in_overlined_title = False
lines = file.readlines()
out_metadata = Metadata()
unreleased_title_kind: str | tuple[str, str] | None = None
is_overlined_title = False
lines = [line.strip().lower() for line in file.readlines()]

for index, (first, second, third) in enumerate(
zip_longest(lines, lines[1:], lines[2:], fillvalue="")
):
first = first.strip().lower()
second = second.strip().lower()
third = third.strip().lower()
title: str | None = None
kind: TitleKind | None = None
if self.is_overlined_title(first, second, third):
kind: str | tuple[str, str] | None = None
if _is_overlined_title(first, second, third):
title = second
kind = (first[0], third[0])
in_overlined_title = True
elif not in_overlined_title and self.is_underlined_title(first, second):
is_overlined_title = True
elif not is_overlined_title and _is_underlined_title(first, second):
title = first
kind = second[0]
else:
in_overlined_title = False

if title:
if "unreleased" in title:
unreleased_title_kind = kind
meta.unreleased_start = index
continue
elif unreleased_title_kind and unreleased_title_kind == kind:
meta.unreleased_end = index
# Try to find the latest release done
if version := self.tag_rules.search_version(title):
meta.latest_version = version[0]
meta.latest_version_tag = version[1]
meta.latest_version_position = index
break
if meta.unreleased_start is not None and meta.unreleased_end is None:
meta.unreleased_end = (
meta.latest_version_position if meta.latest_version else index + 1
is_overlined_title = False

if not title:
continue

if "unreleased" in title:
unreleased_title_kind = kind
out_metadata.unreleased_start = index
continue

if unreleased_title_kind and unreleased_title_kind == kind:
out_metadata.unreleased_end = index
# Try to find the latest release done
if version := self.tag_rules.search_version(title):
out_metadata.latest_version = version[0]
out_metadata.latest_version_tag = version[1]
out_metadata.latest_version_position = index
break

if (
out_metadata.unreleased_start is not None
and out_metadata.unreleased_end is None
):
out_metadata.unreleased_end = (
out_metadata.latest_version_position
if out_metadata.latest_version
else len(lines)
)

return meta

def is_overlined_title(self, first: str, second: str, third: str) -> bool:
return (
len(first) >= len(second)
and len(first) == len(third)
and all(char == first[0] for char in first[1:])
and first[0] == third[0]
and self.is_underlined_title(second, third)
)

def is_underlined_title(self, first: str, second: str) -> bool:
return (
len(second) >= len(first)
and not second.isalnum()
and all(char == second[0] for char in second[1:])
)
return out_metadata


def _is_overlined_title(first: str, second: str, third: str) -> bool:
return (
len(first) == len(third) >= len(second)
and first[0] == third[0]
and all(char == first[0] for char in first)
and _is_underlined_title(second, third)
)


def _is_underlined_title(first: str, second: str) -> bool:
return (
len(second) >= len(first)
and not second.isalnum()
and all(char == second[0] for char in second)
)
14 changes: 9 additions & 5 deletions tests/test_changelog_format_restructuredtext.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import pytest

from commitizen.changelog import Metadata
from commitizen.changelog_formats.restructuredtext import RestructuredText
from commitizen.changelog_formats.restructuredtext import (
RestructuredText,
_is_overlined_title,
_is_underlined_title,
)
from commitizen.config.base_config import BaseConfig

if TYPE_CHECKING:
Expand Down Expand Up @@ -325,20 +329,20 @@ def test_get_metadata(
[(text, True) for text in UNDERLINED_TITLES]
+ [(text, False) for text in NOT_UNDERLINED_TITLES],
)
def test_is_underlined_title(format: RestructuredText, text: str, expected: bool):
def test_is_underlined_title(text: str, expected: bool):
_, first, second = dedent(text).splitlines()
assert format.is_underlined_title(first, second) is expected
assert _is_underlined_title(first, second) is expected


@pytest.mark.parametrize(
"text, expected",
[(text, True) for text in OVERLINED_TITLES]
+ [(text, False) for text in NOT_OVERLINED_TITLES],
)
def test_is_overlined_title(format: RestructuredText, text: str, expected: bool):
def test_is_overlined_title(text: str, expected: bool):
_, first, second, third = dedent(text).splitlines()

assert format.is_overlined_title(first, second, third) is expected
assert _is_overlined_title(first, second, third) is expected


@pytest.mark.parametrize(
Expand Down
Loading