Building newsfiles for your project.
Core functionality for discovering, parsing, and rendering news fragments into formatted changelog entries.
Find and load news fragment files from the filesystem.
def find_fragments(
base_directory: str,
config: Config,
strict: bool = False
) -> tuple[dict[str, dict[tuple[str, str, int], str]], list[tuple[str, str]]]:
"""
Find and load news fragment files.
Args:
base_directory: Base directory to search from
config: Configuration object with fragment settings
strict: If True, fail on invalid fragment filenames
Returns:
tuple: (fragment_contents, fragment_files)
- fragment_contents: Dict mapping section -> {(issue, type, counter): content}
- fragment_files: List of (filename, category) tuples
"""Parse fragment filenames to extract issue, type, and counter information.
def parse_newfragment_basename(
basename: str,
frag_type_names: Iterable[str]
) -> tuple[str, str, int] | tuple[None, None, None]:
"""
Parse a news fragment basename into components.
Args:
basename: Fragment filename without path
frag_type_names: Valid fragment type names
Returns:
tuple: (issue, category, counter) or (None, None, None) if invalid
Examples:
"123.feature" -> ("123", "feature", 0)
"456.bugfix.2" -> ("456", "bugfix", 2)
"fix-1.2.3.feature" -> ("fix-1.2.3", "feature", 0)
"+abc123.misc" -> ("+abc123", "misc", 0)
"""Split fragments by type and organize for rendering.
def split_fragments(
fragment_contents: dict[str, dict[tuple[str, str, int], str]],
types: Mapping[str, Mapping[str, Any]],
all_bullets: bool = True
) -> dict[str, dict[str, list[tuple[str, list[str]]]]]:
"""
Split and organize fragments by section and type.
Args:
fragment_contents: Raw fragment content by section
types: Fragment type configuration
all_bullets: Whether to format all entries as bullet points
Returns:
dict: Organized fragments as {section: {type: [(issue, [content_lines])]}}
"""Render organized fragments into final changelog format.
def render_fragments(
template: str,
issue_format: str | None,
fragments: Mapping[str, Mapping[str, Mapping[str, Sequence[str]]]],
definitions: Mapping[str, Mapping[str, Any]],
underlines: Sequence[str],
wrap: bool,
versiondata: Mapping[str, str],
top_underline: str = "=",
all_bullets: bool = False,
render_title: bool = True,
md_header_level: int = 1
) -> str:
"""
Render fragments using Jinja2 template.
Args:
template: Jinja2 template string for formatting
issue_format: Optional format string for issue links
fragments: Organized fragment data by section/type
definitions: Type definitions and formatting rules
underlines: Sequence of characters for section underlining
wrap: Whether to wrap text output
versiondata: Version information for template variables
top_underline: Character for top-level section underlines
all_bullets: Whether to use bullets for all content
render_title: Whether to render the release title
md_header_level: Header level for Markdown output
Returns:
Formatted changelog content as string
"""Write rendered content to news files with proper insertion and existing content handling.
def append_to_newsfile(
directory: str,
filename: str,
start_string: str,
top_line: str,
content: str,
single_file: bool
) -> None:
"""
Write content to directory/filename behind start_string.
Args:
directory: Directory containing the news file
filename: Name of the news file
start_string: Marker string for content insertion
top_line: Release header line to check for duplicates
content: Rendered changelog content to insert
single_file: Whether to append to existing file or create new
Raises:
ValueError: If top_line already exists in the file
"""
def _figure_out_existing_content(
news_file: Path,
start_string: str,
single_file: bool
) -> tuple[str, str]:
"""
Split existing news file into header and body parts.
Args:
news_file: Path to the news file
start_string: Marker string to split on
single_file: Whether this is a single file or per-release
Returns:
tuple: (header_content, existing_body_content)
"""Helper class for managing fragment directory paths.
class FragmentsPath:
"""
Helper for getting full paths to fragment directories.
Handles both explicit directory configuration and package-based
fragment location resolution.
"""
def __init__(self, base_directory: str, config: Config):
"""
Initialize path helper.
Args:
base_directory: Base project directory
config: Configuration object
"""
def __call__(self, section_directory: str = "") -> str:
"""
Get fragment directory path for a section.
Args:
section_directory: Section subdirectory name
Returns:
str: Full path to fragment directory
"""class IssueParts(NamedTuple):
"""Components of an issue identifier for sorting."""
prefix: str
number: int | None
suffix: str
def issue_key(issue: str) -> IssueParts:
"""
Generate sort key for issue identifiers.
Args:
issue: Issue identifier string
Returns:
IssueParts: Sortable components
Examples:
"123" -> IssueParts("", 123, "")
"issue-456" -> IssueParts("issue-", 456, "")
"+orphan" -> IssueParts("+", None, "orphan")
"""def entry_key(entry: tuple[str, Sequence[str]]) -> tuple[str, list[IssueParts]]:
"""
Generate sort key for changelog entries.
Args:
entry: (issue_list, content_lines) tuple
Returns:
tuple: (concatenated_issues, parsed_issue_parts)
"""
def bullet_key(entry: tuple[str, Sequence[str]]) -> int:
"""
Generate sort key for bullet point entries.
Args:
entry: (issue_list, content_lines) tuple
Returns:
int: Number of lines in entry (for consistent ordering)
"""def indent(text: str, prefix: str) -> str:
"""
Indent text lines with given prefix.
Args:
text: Text to indent
prefix: Prefix to add to each line
Returns:
str: Indented text
"""
def append_newlines_if_trailing_code_block(text: str) -> str:
"""
Add newlines after trailing code blocks for proper formatting.
Args:
text: Text content to process
Returns:
str: Text with proper code block spacing
"""def render_issue(issue_format: str | None, issue: str) -> str:
"""
Format issue references according to configuration.
Args:
issue_format: Format string with {issue} placeholder
issue: Issue identifier
Returns:
str: Formatted issue reference
Examples:
render_issue("#{issue}", "123") -> "#123"
render_issue("`#{issue} <url/{issue}>`_", "456") -> "`#456 <url/456>`_"
"""from towncrier._builder import find_fragments, split_fragments, render_fragments
from towncrier._settings import load_config_from_options
# Load configuration
base_directory, config = load_config_from_options(None, None)
# Find fragments
fragment_contents, fragment_files = find_fragments(
base_directory=base_directory,
config=config,
strict=True
)
# Organize fragments
fragments = split_fragments(
fragment_contents=fragment_contents,
types=config.types,
all_bullets=config.all_bullets
)
# Render to string
template = "# Version {version}\n\n{% for section in sections %}..."
rendered = render_fragments(
fragments=fragments,
config=config,
template=template,
project_name="My Project",
project_version="1.0.0",
project_date="2024-01-15"
)from towncrier._builder import FragmentsPath
# Create path helper
fragments_path = FragmentsPath(
base_directory="/path/to/project",
config=config
)
# Get fragment directories
main_fragments_dir = fragments_path() # Main section
api_fragments_dir = fragments_path("api") # API sectionfrom towncrier._builder import parse_newfragment_basename
fragment_types = ["feature", "bugfix", "doc", "removal", "misc"]
# Parse various fragment names
result1 = parse_newfragment_basename("123.feature", fragment_types)
# -> ("123", "feature", 0)
result2 = parse_newfragment_basename("fix-auth.bugfix.2", fragment_types)
# -> ("fix-auth", "bugfix", 2)
result3 = parse_newfragment_basename("+orphan.misc", fragment_types)
# -> ("+orphan", "misc", 0)When rendering fragments, these variables are available in Jinja2 templates:
name: Project nameversion: Project versiondate: Project datesections: Dictionary of section dataunderlines: RST underline charactersfragments: Organized fragment dataFragment processing handles these error scenarios:
Install with Tessl CLI
npx tessl i tessl/pypi-towncrier