@@ -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" ,
@@ -314,6 +318,8 @@ ignore = [
314318 " *.enc" ,
315319 " admin/**" ,
316320 " CHANGELOG.rst" ,
321+ " newsfragments" ,
322+ " newsfragments/**" ,
317323 " CODE_OF_CONDUCT.rst" ,
318324 " CONTRIBUTING.rst" ,
319325 " LICENSE" ,
@@ -404,6 +410,30 @@ report.exclude_also = [
404410report.fail_under = 100
405411report.show_missing = true
406412
413+ [tool .towncrier ]
414+ # The changelog and the per-release GitHub release notes are both built
415+ # from news fragments under ``newsfragments/``. The release workflow
416+ # runs ``towncrier build`` to assemble them; contributors add one
417+ # fragment file per user-facing change.
418+ directory = " newsfragments"
419+ filename = " CHANGELOG.rst"
420+ # Custom template so an assembled version reproduces the historical
421+ # style exactly: a bare ``<version>`` heading (no project name, no
422+ # date) followed by a flat bullet list with no per-type sub-headings.
423+ template = " docs/towncrier_template.rst.jinja"
424+ title_format = " {version}"
425+ # ``title_format`` underline first, then any nested headings. A bare
426+ # version such as ``2026.05.18`` underlined with ``-`` matches every
427+ # pre-towncrier entry in CHANGELOG.rst.
428+ underlines = [ " -" , " ~" , " ^" ]
429+ issue_format = " #{issue}"
430+ type = [
431+ # A single, unnamed fragment type keeps the assembled output as one
432+ # flat bullet list, matching the historical changelog (which never
433+ # grouped entries under "Features"/"Bugfixes"/... sub-headings).
434+ { directory = " change" , name = " " , showcontent = true },
435+ ]
436+
407437[tool .pydocstringformatter ]
408438write = true
409439split-summary-body = false
@@ -482,6 +512,9 @@ ignore_names = [
482512 " DatabaseDict" ,
483513 " VuMarkDatabaseDict" ,
484514 " VuMarkTargetDict" ,
515+ " towncrier_draft_autoversion_mode" ,
516+ " towncrier_draft_include_empty" ,
517+ " towncrier_draft_working_directory" ,
485518]
486519# Duplicate some of .gitignore
487520exclude = [ " .venv" ]
0 commit comments