0
# Fragment Processing
1
2
Core functionality for discovering, parsing, and rendering news fragments into formatted changelog entries.
3
4
## Capabilities
5
6
### Fragment Discovery
7
8
Find and load news fragment files from the filesystem.
9
10
```python { .api }
11
def find_fragments(
12
base_directory: str,
13
config: Config,
14
strict: bool = False
15
) -> tuple[dict[str, dict[tuple[str, str, int], str]], list[tuple[str, str]]]:
16
"""
17
Find and load news fragment files.
18
19
Args:
20
base_directory: Base directory to search from
21
config: Configuration object with fragment settings
22
strict: If True, fail on invalid fragment filenames
23
24
Returns:
25
tuple: (fragment_contents, fragment_files)
26
- fragment_contents: Dict mapping section -> {(issue, type, counter): content}
27
- fragment_files: List of (filename, category) tuples
28
"""
29
```
30
31
### Fragment Parsing
32
33
Parse fragment filenames to extract issue, type, and counter information.
34
35
```python { .api }
36
def parse_newfragment_basename(
37
basename: str,
38
frag_type_names: Iterable[str]
39
) -> tuple[str, str, int] | tuple[None, None, None]:
40
"""
41
Parse a news fragment basename into components.
42
43
Args:
44
basename: Fragment filename without path
45
frag_type_names: Valid fragment type names
46
47
Returns:
48
tuple: (issue, category, counter) or (None, None, None) if invalid
49
50
Examples:
51
"123.feature" -> ("123", "feature", 0)
52
"456.bugfix.2" -> ("456", "bugfix", 2)
53
"fix-1.2.3.feature" -> ("fix-1.2.3", "feature", 0)
54
"+abc123.misc" -> ("+abc123", "misc", 0)
55
"""
56
```
57
58
### Fragment Organization
59
60
Split fragments by type and organize for rendering.
61
62
```python { .api }
63
def split_fragments(
64
fragment_contents: dict[str, dict[tuple[str, str, int], str]],
65
types: Mapping[str, Mapping[str, Any]],
66
all_bullets: bool = True
67
) -> dict[str, dict[str, list[tuple[str, list[str]]]]]:
68
"""
69
Split and organize fragments by section and type.
70
71
Args:
72
fragment_contents: Raw fragment content by section
73
types: Fragment type configuration
74
all_bullets: Whether to format all entries as bullet points
75
76
Returns:
77
dict: Organized fragments as {section: {type: [(issue, [content_lines])]}}
78
"""
79
```
80
81
### Fragment Rendering
82
83
Render organized fragments into final changelog format.
84
85
```python { .api }
86
def render_fragments(
87
template: str,
88
issue_format: str | None,
89
fragments: Mapping[str, Mapping[str, Mapping[str, Sequence[str]]]],
90
definitions: Mapping[str, Mapping[str, Any]],
91
underlines: Sequence[str],
92
wrap: bool,
93
versiondata: Mapping[str, str],
94
top_underline: str = "=",
95
all_bullets: bool = False,
96
render_title: bool = True,
97
md_header_level: int = 1
98
) -> str:
99
"""
100
Render fragments using Jinja2 template.
101
102
Args:
103
template: Jinja2 template string for formatting
104
issue_format: Optional format string for issue links
105
fragments: Organized fragment data by section/type
106
definitions: Type definitions and formatting rules
107
underlines: Sequence of characters for section underlining
108
wrap: Whether to wrap text output
109
versiondata: Version information for template variables
110
top_underline: Character for top-level section underlines
111
all_bullets: Whether to use bullets for all content
112
render_title: Whether to render the release title
113
md_header_level: Header level for Markdown output
114
115
Returns:
116
Formatted changelog content as string
117
"""
118
```
119
120
### News File Writing
121
122
Write rendered content to news files with proper insertion and existing content handling.
123
124
```python { .api }
125
def append_to_newsfile(
126
directory: str,
127
filename: str,
128
start_string: str,
129
top_line: str,
130
content: str,
131
single_file: bool
132
) -> None:
133
"""
134
Write content to directory/filename behind start_string.
135
136
Args:
137
directory: Directory containing the news file
138
filename: Name of the news file
139
start_string: Marker string for content insertion
140
top_line: Release header line to check for duplicates
141
content: Rendered changelog content to insert
142
single_file: Whether to append to existing file or create new
143
144
Raises:
145
ValueError: If top_line already exists in the file
146
"""
147
148
def _figure_out_existing_content(
149
news_file: Path,
150
start_string: str,
151
single_file: bool
152
) -> tuple[str, str]:
153
"""
154
Split existing news file into header and body parts.
155
156
Args:
157
news_file: Path to the news file
158
start_string: Marker string to split on
159
single_file: Whether this is a single file or per-release
160
161
Returns:
162
tuple: (header_content, existing_body_content)
163
"""
164
```
165
166
## Fragment Path Utilities
167
168
### FragmentsPath Class
169
170
Helper class for managing fragment directory paths.
171
172
```python { .api }
173
class FragmentsPath:
174
"""
175
Helper for getting full paths to fragment directories.
176
177
Handles both explicit directory configuration and package-based
178
fragment location resolution.
179
"""
180
181
def __init__(self, base_directory: str, config: Config):
182
"""
183
Initialize path helper.
184
185
Args:
186
base_directory: Base project directory
187
config: Configuration object
188
"""
189
190
def __call__(self, section_directory: str = "") -> str:
191
"""
192
Get fragment directory path for a section.
193
194
Args:
195
section_directory: Section subdirectory name
196
197
Returns:
198
str: Full path to fragment directory
199
"""
200
```
201
202
## Sorting and Organization
203
204
### Issue Sorting
205
206
```python { .api }
207
class IssueParts(NamedTuple):
208
"""Components of an issue identifier for sorting."""
209
prefix: str
210
number: int | None
211
suffix: str
212
213
def issue_key(issue: str) -> IssueParts:
214
"""
215
Generate sort key for issue identifiers.
216
217
Args:
218
issue: Issue identifier string
219
220
Returns:
221
IssueParts: Sortable components
222
223
Examples:
224
"123" -> IssueParts("", 123, "")
225
"issue-456" -> IssueParts("issue-", 456, "")
226
"+orphan" -> IssueParts("+", None, "orphan")
227
"""
228
```
229
230
### Entry Sorting
231
232
```python { .api }
233
def entry_key(entry: tuple[str, Sequence[str]]) -> tuple[str, list[IssueParts]]:
234
"""
235
Generate sort key for changelog entries.
236
237
Args:
238
entry: (issue_list, content_lines) tuple
239
240
Returns:
241
tuple: (concatenated_issues, parsed_issue_parts)
242
"""
243
244
def bullet_key(entry: tuple[str, Sequence[str]]) -> int:
245
"""
246
Generate sort key for bullet point entries.
247
248
Args:
249
entry: (issue_list, content_lines) tuple
250
251
Returns:
252
int: Number of lines in entry (for consistent ordering)
253
"""
254
```
255
256
## Content Processing
257
258
### Text Formatting
259
260
```python { .api }
261
def indent(text: str, prefix: str) -> str:
262
"""
263
Indent text lines with given prefix.
264
265
Args:
266
text: Text to indent
267
prefix: Prefix to add to each line
268
269
Returns:
270
str: Indented text
271
"""
272
273
def append_newlines_if_trailing_code_block(text: str) -> str:
274
"""
275
Add newlines after trailing code blocks for proper formatting.
276
277
Args:
278
text: Text content to process
279
280
Returns:
281
str: Text with proper code block spacing
282
"""
283
```
284
285
### Issue Reference Rendering
286
287
```python { .api }
288
def render_issue(issue_format: str | None, issue: str) -> str:
289
"""
290
Format issue references according to configuration.
291
292
Args:
293
issue_format: Format string with {issue} placeholder
294
issue: Issue identifier
295
296
Returns:
297
str: Formatted issue reference
298
299
Examples:
300
render_issue("#{issue}", "123") -> "#123"
301
render_issue("`#{issue} <url/{issue}>`_", "456") -> "`#456 <url/456>`_"
302
"""
303
```
304
305
## Usage Examples
306
307
### Basic Fragment Processing
308
309
```python
310
from towncrier._builder import find_fragments, split_fragments, render_fragments
311
from towncrier._settings import load_config_from_options
312
313
# Load configuration
314
base_directory, config = load_config_from_options(None, None)
315
316
# Find fragments
317
fragment_contents, fragment_files = find_fragments(
318
base_directory=base_directory,
319
config=config,
320
strict=True
321
)
322
323
# Organize fragments
324
fragments = split_fragments(
325
fragment_contents=fragment_contents,
326
types=config.types,
327
all_bullets=config.all_bullets
328
)
329
330
# Render to string
331
template = "# Version {version}\n\n{% for section in sections %}..."
332
rendered = render_fragments(
333
fragments=fragments,
334
config=config,
335
template=template,
336
project_name="My Project",
337
project_version="1.0.0",
338
project_date="2024-01-15"
339
)
340
```
341
342
### Custom Fragment Directory
343
344
```python
345
from towncrier._builder import FragmentsPath
346
347
# Create path helper
348
fragments_path = FragmentsPath(
349
base_directory="/path/to/project",
350
config=config
351
)
352
353
# Get fragment directories
354
main_fragments_dir = fragments_path() # Main section
355
api_fragments_dir = fragments_path("api") # API section
356
```
357
358
### Fragment Filename Parsing
359
360
```python
361
from towncrier._builder import parse_newfragment_basename
362
363
fragment_types = ["feature", "bugfix", "doc", "removal", "misc"]
364
365
# Parse various fragment names
366
result1 = parse_newfragment_basename("123.feature", fragment_types)
367
# -> ("123", "feature", 0)
368
369
result2 = parse_newfragment_basename("fix-auth.bugfix.2", fragment_types)
370
# -> ("fix-auth", "bugfix", 2)
371
372
result3 = parse_newfragment_basename("+orphan.misc", fragment_types)
373
# -> ("+orphan", "misc", 0)
374
```
375
376
## Template Variables
377
378
When rendering fragments, these variables are available in Jinja2 templates:
379
380
- `name`: Project name
381
- `version`: Project version
382
- `date`: Project date
383
- `sections`: Dictionary of section data
384
- `underlines`: RST underline characters
385
- `fragments`: Organized fragment data
386
387
## Error Handling
388
389
Fragment processing handles these error scenarios:
390
391
- **Invalid fragment filenames**: Optionally strict validation
392
- **Missing fragment directories**: Graceful fallback
393
- **Template rendering errors**: Jinja2 syntax or variable errors
394
- **File encoding issues**: UTF-8 encoding problems
395
- **Content parsing errors**: Malformed fragment content