@@ -96,9 +96,13 @@ optional-dependencies.dev = [
9696 " sphinx-toolbox==4.2.0rc1" ,
9797 " sphinxcontrib-httpdomain==2.0.0" ,
9898 " sphinxcontrib-spelling==8.0.2" ,
99+ # ``sphinxcontrib-towncrier`` renders unreleased news fragments
100+ # into docs/source/unreleased.rst during Sphinx builds.
101+ " sphinxcontrib-towncrier==0.5.0a0" ,
99102 " strict-kwargs==2026.5.20" ,
100103 " sybil==10.0.1" ,
101104 " tenacity==9.1.4" ,
105+ " towncrier==25.8.0" ,
102106 " ty==0.0.38" ,
103107 " types-docker==7.1.0.20260518" ,
104108 " types-pyyaml==6.0.12.20260518" ,
@@ -315,6 +319,8 @@ ignore = [
315319 " *.enc" ,
316320 " admin/**" ,
317321 " CHANGELOG.rst" ,
322+ " newsfragments" ,
323+ " newsfragments/**" ,
318324 " CODE_OF_CONDUCT.rst" ,
319325 " CONTRIBUTING.rst" ,
320326 " LICENSE" ,
@@ -405,6 +411,30 @@ report.exclude_also = [
405411report.fail_under = 100
406412report.show_missing = true
407413
414+ [tool .towncrier ]
415+ # The changelog and the per-release GitHub release notes are both built
416+ # from news fragments under ``newsfragments/``. The release workflow
417+ # runs ``towncrier build`` to assemble them; contributors add one
418+ # fragment file per user-facing change.
419+ directory = " newsfragments"
420+ filename = " CHANGELOG.rst"
421+ # Custom template so an assembled version reproduces the historical
422+ # style exactly: a bare ``<version>`` heading (no project name, no
423+ # date) followed by a flat bullet list with no per-type sub-headings.
424+ template = " docs/towncrier_template.rst.jinja"
425+ title_format = " {version}"
426+ # ``title_format`` underline first, then any nested headings. A bare
427+ # version such as ``2026.05.18`` underlined with ``-`` matches every
428+ # pre-towncrier entry in CHANGELOG.rst.
429+ underlines = [ " -" , " ~" , " ^" ]
430+ issue_format = " #{issue}"
431+ type = [
432+ # A single, unnamed fragment type keeps the assembled output as one
433+ # flat bullet list, matching the historical changelog (which never
434+ # grouped entries under "Features"/"Bugfixes"/... sub-headings).
435+ { directory = " change" , name = " " , showcontent = true },
436+ ]
437+
408438[tool .pydocstringformatter ]
409439write = true
410440split-summary-body = false
@@ -483,6 +513,9 @@ ignore_names = [
483513 " DatabaseDict" ,
484514 " VuMarkDatabaseDict" ,
485515 " VuMarkTargetDict" ,
516+ " towncrier_draft_autoversion_mode" ,
517+ " towncrier_draft_include_empty" ,
518+ " towncrier_draft_working_directory" ,
486519]
487520# Duplicate some of .gitignore
488521exclude = [ " .venv" ]
0 commit comments