Skip to content
Merged
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
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ announce = "announcer.__init__:main"

[dependency-groups]
dev = [
"wheel==0.46.3",
"mypy==2.0.0",
"pytest==9.0.3",
"wheel==0.47.0",
"mypy==2.3.0",
"pytest==9.1.1",
"werkzeug==3.1.8",
"pytest-httpserver==1.1.5",
"tox==4.52.0",
"tox==4.58.0",
"pytest-cov==7.1.0",
"types-requests>=2.32.0,<3",
"ruff>=0.15.8,<0.16.0",
"ruff>=0.16.0,<0.17.0",
]

[build-system]
Expand Down
5 changes: 5 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Copyright (c) Alianza, Inc. All rights reserved.
include = [
"src/**/*.py",
"tests/**/*.py",
]

exclude = [
".venv",
"__pycache__",
Expand Down
52 changes: 25 additions & 27 deletions src/announcer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) Alianza, Inc. All rights reserved.
"""A tool for announcing keepachangelog format logs to Slack and Microsoft Teams channels"""

Expand All @@ -9,7 +7,7 @@
import re
import sys
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any

import requests
from mistletoe.block_token import Document
Expand All @@ -19,16 +17,16 @@

log = logging.getLogger(__name__)

ValidRenderers = Union[ChangeLogRenderer, TeamsChangeLogRenderer]
ValidRenderers = ChangeLogRenderer | TeamsChangeLogRenderer


DIFF_URL_RE = re.compile("^(.*)/compare/[^/]+[.][.][.]([^/]+)$")
TREE_URL_RE = re.compile("^(.*)/tree/([^/]+)$")


def derive_urls(
diff_url: Optional[str],
) -> Tuple[Optional[str], Optional[str]]:
diff_url: str | None,
) -> tuple[str | None, str | None]:
"""Derive base and reference URLs from a GitHub compare or tree URL."""
base_url = None
reference = None
Expand Down Expand Up @@ -78,17 +76,17 @@ def announce(args: argparse.Namespace) -> None:
args.compatibility_teams_sections,
)
else:
raise ValueError("Unknown target! {}".format(args.target))
raise ValueError(f"Unknown target! {args.target}")


def announce_slack(
webhook: str,
changelogversion: str,
changelogfile: str,
projectname: str,
username: Optional[str] = None,
icon_url: Optional[str] = None,
icon_emoji: Optional[str] = None,
username: str | None = None,
icon_url: str | None = None,
icon_emoji: str | None = None,
) -> None:
"""Announce changelog changes to Slack"""
# Get the changelog
Expand All @@ -97,7 +95,7 @@ def announce_slack(

# Get the version information
log.info("Getting version %s info from changelog", changelogversion)
(changelog_info, diff_url, sections) = changelog.get_version_details(
(changelog_info, diff_url, _sections) = changelog.get_version_details(
changelogversion
)

Expand All @@ -110,12 +108,12 @@ def announce_slack(
log.debug("Base URL: %s; Reference %s", base_url, reference)

# Make a message attachment.
pretext = ["*{0} {1}*".format(projectname, changelogversion)]
pretext = [f"*{projectname} {changelogversion}*"]

if base_url:
pretext.append("({0})".format(base_url))
pretext.append(f"({base_url})")

attachments: List[Dict[str, Any]] = [
attachments: list[dict[str, Any]] = [
{"color": "good", "pretext": " ".join(pretext), "text": changelog_info}
]

Expand All @@ -124,13 +122,13 @@ def announce_slack(

if diff_url:
# Add a button to view the changes at the diff URL
fallback.append("View changes at {0}".format(diff_url))
fallback.append(f"View changes at {diff_url}")
actions.append({"type": "button", "text": "View Changes", "url": diff_url})

if base_url:
# Add a button to view the CHANGELOG.md.
changelog_url = "{0}/blob/{1}/CHANGELOG.md".format(base_url, reference)
fallback.append("View CHANGELOG.md at {0}".format(changelog_url))
changelog_url = f"{base_url}/blob/{reference}/CHANGELOG.md"
fallback.append(f"View CHANGELOG.md at {changelog_url}")
actions.append(
{"type": "button", "text": "View CHANGELOG.md", "url": changelog_url}
)
Expand All @@ -141,7 +139,7 @@ def announce_slack(
)

# Construct the data to send to the endpoint.
message_data: Dict[str, Any] = {"attachments": attachments}
message_data: dict[str, Any] = {"attachments": attachments}

if username:
message_data["username"] = username
Expand All @@ -150,7 +148,7 @@ def announce_slack(
message_data["icon_url"] = icon_url
elif icon_emoji:
# Wrap the emoji name in colons to use that emoji
message_data["icon_emoji"] = ":{}:".format(icon_emoji)
message_data["icon_emoji"] = f":{icon_emoji}:"

log.debug("Sending info %s", message_data)

Expand Down Expand Up @@ -209,7 +207,7 @@ def announce_teams(

if base_url:
# Add a button to view the CHANGELOG.md.
changelog_url = "{0}/blob/{1}/CHANGELOG.md".format(base_url, reference)
changelog_url = f"{base_url}/blob/{reference}/CHANGELOG.md"
actions.append(
{
"@type": "OpenUri",
Expand All @@ -234,8 +232,8 @@ def announce_teams(
message_data = {
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "{0} {1}".format(projectname, changelogversion),
"title": "{0} {1}".format(projectname, changelogversion),
"summary": f"{projectname} {changelogversion}",
"title": f"{projectname} {changelogversion}",
"sections": sections,
}

Expand All @@ -254,7 +252,7 @@ def announce_teams(
r.raise_for_status()


class Changelog(object):
class Changelog:
"""Helper for loading and rendering changelog sections."""

def __init__(self, filename: str, renderer_class: type[ValidRenderers]) -> None:
Expand All @@ -264,7 +262,7 @@ def __init__(self, filename: str, renderer_class: type[ValidRenderers]) -> None:

def get_version_details(
self, version: str
) -> Tuple[str, Optional[str], List[Dict[str, str]]]:
) -> tuple[str, str | None, list[dict[str, str]]]:
"""Render and return details for a specific changelog version."""
with open(self.filename, "r") as f:
document = Document(f)
Expand Down Expand Up @@ -378,9 +376,9 @@ def main() -> None:

try:
announce(args)
except Exception as e:
log.exception(e)
raise e
except Exception:
log.exception("Announcement failed")
raise


if __name__ == "__main__":
Expand Down
36 changes: 16 additions & 20 deletions src/announcer/changelogrenderer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) Metaswitch Networks.
"""ChangeLogRenderer for mistletoe for rendering changelogs to Slack's markdown format."""

import html
import logging
from typing import Optional, cast
from typing import cast

from mistletoe import block_token, span_token, token
from mistletoe.base_renderer import BaseRenderer
Expand All @@ -27,7 +25,7 @@ def __init__(self, version: str, *extras: object) -> None:
"""Create a ChangeLogRenderer."""
super().__init__(*extras)
self.version = version
self.diff_url: Optional[str] = None
self.diff_url: str | None = None
self.sections: list[dict[str, str]] = []

def __exit__(self, *args: object) -> None:
Expand All @@ -49,31 +47,31 @@ def render(self, token: token.Token) -> str:

def render_strong(self, token: span_token.Strong) -> str:
"""Render strong text as *text*."""
return "*{}*".format(self.render_inner(token))
return f"*{self.render_inner(token)}*"

def render_emphasis(self, token: span_token.Emphasis) -> str:
"""Render emphasis text as _text_."""
return "_{}_".format(self.render_inner(token))
return f"_{self.render_inner(token)}_"

def render_inline_code(self, token: span_token.InlineCode) -> str:
"""Render inline code as `code`."""
if not token.children:
return ""

first_child = list(token.children)[0]
first_child = next(iter(token.children))
if hasattr(first_child, "content"):
content = getattr(first_child, "content")
return "`{}`".format(content)
content = first_child.content
return f"`{content}`"
else:
return ""

def render_strikethrough(self, token: span_token.Strikethrough) -> str:
"""Render strikethrough text as ~text~."""
return "~{}~".format(self.render_inner(token))
return f"~{self.render_inner(token)}~"

def render_image(self, token: span_token.Image) -> str:
"""Render an image as <src|alt>."""
return "<{}|{}>".format(token.src, self.escape_html(token.src))
return f"<{token.src}|{self.escape_html(token.src)}>"

def render_link(self, token: span_token.Link) -> str:
"""Render a link as <target|inner>."""
Expand All @@ -86,7 +84,7 @@ def render_auto_link(self, token: span_token.AutoLink) -> str:
"""Render an auto link as <target|inner>."""
template = "<{target}|{inner}>"
if token.mailto:
target = "mailto:{}".format(token.target)
target = f"mailto:{token.target}"
else:
target = token.target
inner = self.escape_html(render_to_plaintext(token))
Expand All @@ -112,7 +110,7 @@ def render_heading(self, token: block_token.Heading) -> str:
def render_quote(self, token: block_token.Quote) -> str:
"""Render a quote as > text."""
inner = self.render_inner(token)
return "> {}\n".format(inner)
return f"> {inner}\n"

def render_paragraph(self, token: block_token.Paragraph) -> str:
"""Render a paragraph."""
Expand All @@ -123,10 +121,10 @@ def render_block_code(self, token: block_token.BlockCode) -> str:
if token.children is None:
return ""

first_child = list(token.children)[0]
first_child = next(iter(token.children))
if not hasattr(first_child, "content"):
return ""
content = str(getattr(first_child, "content"))
content = str(first_child.content)
return f"```\n{content}```\n"

def render_list(self, token: block_token.List) -> str:
Expand All @@ -138,7 +136,7 @@ def render_list(self, token: block_token.List) -> str:
def render_listentry(self, listentry: ListEntry) -> str:
"""Render a list entry."""
if listentry.number is not None:
bullet = "{}.".format(listentry.number)
bullet = f"{listentry.number}."
else:
if listentry.depth > 0:
# Use TRIANGULAR BULLET for subbullets
Expand All @@ -149,16 +147,14 @@ def render_listentry(self, listentry: ListEntry) -> str:

leading_spaces = " " * (listentry.depth * 4)

return "{spaces}{bullet} {content}\n".format(
spaces=leading_spaces, bullet=bullet, content=listentry.content
)
return f"{leading_spaces}{bullet} {listentry.content}\n"

def analyse_list(self, token: block_token.List, depth: int) -> list[ListEntry]:
"""Analyse a list and return a list of ListEntry objects representing the list entries."""
analysed = []

# token.start is a property, List.start() is a class method.
start = cast(Optional[int], token.start)
start = cast(int | None, token.start)

counter = ListCounter(start)
if token.children:
Expand Down
14 changes: 6 additions & 8 deletions src/announcer/common.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) Metaswitch Networks.
"""Common functionality for all renderers."""

import logging
from collections import namedtuple
from collections.abc import Callable
from dataclasses import dataclass
from typing import Optional, cast
from typing import cast

from mistletoe import block_token, span_token, token

Expand All @@ -20,11 +18,11 @@
class ListCounter:
"""A simple counter for numbered lists."""

def __init__(self, start: Optional[int]) -> None:
def __init__(self, start: int | None) -> None:
"""Initialise the counter with the given start value."""
self.current = start

def __next__(self) -> Optional[int]:
def __next__(self) -> int | None:
"""Return the current value and increment the counter."""
current = self.current
if self.current is not None:
Expand All @@ -38,7 +36,7 @@ def render_to_plaintext(token: token.Token) -> str:
rendered = [render_to_plaintext(child) for child in token.children]
return "".join(rendered)
elif hasattr(token, "content"):
return getattr(token, "content")
return token.content
else:
return ""

Expand All @@ -58,7 +56,7 @@ def render_block_document(
) -> DocumentRender:
"""Render a document token to plain text, only rendering the section for the given version."""
to_render = []
diff_url: Optional[str] = None
diff_url: str | None = None
rendering = False

if token.children:
Expand All @@ -68,7 +66,7 @@ def render_block_document(
if heading.level == 2 and heading.children is not None:
# Get the text of the first child of this heading. This should be the
# version number, or "Unreleased".
first_child = list(heading.children)[0]
first_child = next(iter(heading.children))
heading_text = render_to_plaintext(first_child)

# Only render things under the right level 2 heading.
Expand Down
5 changes: 1 addition & 4 deletions src/announcer/teamschangelogrenderer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) Metaswitch Networks.
"""TeamsChangeLogRenderer for mistletoe."""

import logging
from typing import Optional

from mistletoe import block_token, token
from mistletoe.html_renderer import HtmlRenderer
Expand All @@ -21,7 +18,7 @@ def __init__(self, version: str, *extras: object) -> None:
"""Create a TeamsChangeLogRenderer for the given version."""
super().__init__(*extras)
self.version = version
self.diff_url: Optional[str] = None
self.diff_url: str | None = None
self.sections: list[dict[str, str]] = []

def __exit__(self, *args: object) -> None:
Expand Down
Loading