Source code for semantic_release.history.parser_scipy

"""
Parses commit messages using `scipy tags <scipy-style>`_ of the form::

    <tag>(<scope>): <subject>

    <body>


The elements <tag>, <scope> and <body> are optional. If no tag is present, the
commit will be added to the changelog section "None" and no version increment
will be performed.

While <scope> is supported here it isn't actually part of the scipy style.
If it is missing, parentheses around it are too. The commit should then be
of the form::

    <tag>: <subject>

    <body>

To communicate a breaking change add "BREAKING CHANGE" into the body at the
beginning of a paragraph. Fill this paragraph with information how to migrate
from the broken behavior to the new behavior. It will be added to the
"Breaking" section of the changelog.

Supported Tags::

    API, DEP, ENH, REV, BUG, MAINT, BENCH, BLD,
    DEV, DOC, STY, TST, REL, FEAT, TEST

Supported Changelog Sections::

    breaking, feature, fix, Other, None

.. _`scipy-style`: https://docs.scipy.org/doc/scipy/reference/dev/contributor/development_workflow.html#writing-the-commit-message
"""

import logging
import re

from ..errors import UnknownCommitMessageStyleError
from ..helpers import LoggedFunction
from .parser_helpers import ParsedCommit

logger = logging.getLogger(__name__)


[docs]class ChangeType: def __init__(self, tag, section) -> None: self.tag: str = tag self.section: str = section self.bump_level: int = 0
[docs] def make_breaking(self): self.bump_level = 3
[docs]class Breaking(ChangeType): def __init__(self, tag, section) -> None: super().__init__(tag, section) self.bump_level: int = 3
[docs]class Compatible(ChangeType): def __init__(self, tag, section) -> None: super().__init__(tag, section) self.bump_level: int = 2
[docs]class Patch(ChangeType): def __init__(self, tag, section) -> None: super().__init__(tag, section) self.bump_level: int = 1
[docs]class Ignore(ChangeType): def __init__(self, tag, section) -> None: super().__init__(tag, section) self.bump_level: int = 0
COMMIT_TYPES = [ Breaking("API", "breaking"), Ignore("BENCH", "None"), Patch("BLD", "fix"), Patch("BUG", "fix"), Compatible("DEP", "breaking"), Compatible("DEV", "None"), Ignore("DOC", "documentation"), Compatible("ENH", "feature"), Patch("MAINT", "fix"), Compatible("REV", "Other"), Ignore("STY", "None"), Ignore("TST", "None"), Ignore("REL", "None"), # strictly speaking not part of the standard Compatible("FEAT", "feature"), Ignore("TEST", "None"), ] _commit_filter = "|".join(c.tag for c in COMMIT_TYPES) re_parser = re.compile( rf"(?P<tag>{_commit_filter})?" r"(?:\((?P<scope>[^\n]+)\))?" r":? " r"(?P<subject>[^\n]+):?" r"(\n\n(?P<text>.*))?", re.DOTALL, )
[docs]@LoggedFunction(logger) def parse_commit_message(message: str) -> ParsedCommit: """ Parse a scipy-style commit message :param message: A string of a commit message. :return: A tuple of (level to bump, type of change, scope of change, a tuple with descriptions) :raises UnknownCommitMessageStyleError: if regular expression matching fails """ parsed = re_parser.match(message) if not parsed: raise UnknownCommitMessageStyleError( f"Unable to parse the given commit message: {message}" ) if parsed.group("subject"): subject = parsed.group("subject") else: raise UnknownCommitMessageStyleError(f"The commit has no subject {message}") if parsed.group("text"): blocks = parsed.group("text").split("\n\n") blocks = [x for x in blocks if not x == ""] blocks.insert(0, subject) else: blocks = [subject] msg_type: ChangeType for msg_type in COMMIT_TYPES: if msg_type.tag == parsed.group("tag"): break else: # some commits may not have a tag, e.g. if they belong to a PR that # wasn't squashed (for maintainability) ignore them msg_type = Ignore("", "None") # Look for descriptions of breaking changes migration_instructions = [ block for block in blocks if block.startswith("BREAKING CHANGE") ] if migration_instructions: msg_type.make_breaking() return ParsedCommit( msg_type.bump_level, msg_type.section, parsed.group("scope"), blocks, migration_instructions, )