Skip to content

Commit 7f0198a

Browse files
author
Marc Schöchlin
committed
Add logic to detecte duplicate paths
1 parent 91841f0 commit 7f0198a

File tree

4 files changed

+280
-58
lines changed

4 files changed

+280
-58
lines changed

NEWS.rst

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,205 @@ Release notes
55

66
.. towncrier release notes start
77
8+
towncrier 25.8.0.dev0 (2025-09-25)
9+
==================================
10+
11+
Features
12+
--------
13+
14+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
15+
- AA (`#1111 <https://github.com/twisted/towncrier/issues/1111>`_)
16+
17+
18+
Test Backend
19+
------------
20+
21+
No significant changes.
22+
23+
24+
Test Frontend
25+
-------------
26+
27+
No significant changes.
28+
29+
30+
towncrier 25.8.0.dev0 (2025-09-25)
31+
==================================
32+
33+
Features
34+
--------
35+
36+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
37+
38+
39+
Main Platform
40+
-------------
41+
42+
Features
43+
~~~~~~~~
44+
45+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
46+
47+
48+
Secondary
49+
---------
50+
51+
Bugfixes
52+
~~~~~~~~
53+
54+
- wollo (`#345 <https://github.com/twisted/towncrier/issues/345>`_)
55+
56+
57+
towncrier 25.8.0.dev0 (2025-09-25)
58+
==================================
59+
60+
Features
61+
--------
62+
63+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
64+
65+
66+
Main Platform
67+
-------------
68+
69+
Features
70+
~~~~~~~~
71+
72+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
73+
74+
75+
Secondary
76+
---------
77+
78+
Bugfixes
79+
~~~~~~~~
80+
81+
- wollo (`#345 <https://github.com/twisted/towncrier/issues/345>`_)
82+
83+
84+
towncrier 25.8.0.dev0 (2025-09-25)
85+
==================================
86+
87+
Features
88+
--------
89+
90+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
91+
92+
93+
Main Platform
94+
-------------
95+
96+
Features
97+
~~~~~~~~
98+
99+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
100+
101+
102+
Secondary
103+
---------
104+
105+
Bugfixes
106+
~~~~~~~~
107+
108+
- wollo (`#345 <https://github.com/twisted/towncrier/issues/345>`_)
109+
110+
111+
towncrier 25.8.0.dev0 (2025-09-25)
112+
==================================
113+
114+
Features
115+
--------
116+
117+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
118+
119+
120+
Main Platform
121+
-------------
122+
123+
Features
124+
~~~~~~~~
125+
126+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
127+
128+
129+
Secondary
130+
---------
131+
132+
Bugfixes
133+
~~~~~~~~
134+
135+
- wollo (`#345 <https://github.com/twisted/towncrier/issues/345>`_)
136+
137+
138+
towncrier 25.8.0.dev0 (2025-09-25)
139+
==================================
140+
141+
Features
142+
--------
143+
144+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
145+
146+
147+
Main Platform
148+
-------------
149+
150+
Features
151+
~~~~~~~~
152+
153+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
154+
155+
156+
Secondary
157+
---------
158+
159+
Bugfixes
160+
~~~~~~~~
161+
162+
- wollo (`#345 <https://github.com/twisted/towncrier/issues/345>`_)
163+
164+
165+
towncrier 25.8.0.dev0 (2025-09-25)
166+
==================================
167+
168+
Features
169+
--------
170+
171+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
172+
173+
174+
Main Platform
175+
-------------
176+
177+
Features
178+
~~~~~~~~
179+
180+
- wall (`#123 <https://github.com/twisted/towncrier/issues/123>`_)
181+
182+
183+
Secondary
184+
---------
185+
186+
No significant changes.
187+
188+
189+
towncrier 25.8.0.dev0 (2025-09-25)
190+
==================================
191+
192+
No significant changes.
193+
194+
195+
Main Platform
196+
-------------
197+
198+
No significant changes.
199+
200+
201+
Secondary
202+
---------
203+
204+
No significant changes.
205+
206+
8207
towncrier 25.8.0 (2025-08-30)
9208
=============================
10209

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ exclude = [
8181
issue_pattern = "\\d+"
8282

8383
[[tool.towncrier.section]]
84-
path = ""
84+
path = ""
8585

8686
[[tool.towncrier.section]]
87-
name = "Main Platform"
88-
path = ""
87+
name = "Test Backend"
88+
path = "backend-api"
8989

9090
[[tool.towncrier.section]]
91-
name = "Secondary"
92-
path = "secondary"
91+
name = "Test Frontend"
92+
path = "frontend"
9393

9494
[[tool.towncrier.type]]
9595
directory = "feature"

src/towncrier/_settings/load.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
import atexit
7+
import collections
78
import dataclasses
89
import os
910
import re
@@ -12,9 +13,10 @@
1213
from collections.abc import Mapping, Sequence
1314
from contextlib import ExitStack
1415
from pathlib import Path
15-
from typing import Any, Literal
16+
from typing import Any, Literal, Dict
1617

1718
from click import ClickException
19+
from openstack.image.v2.cache import Cache
1820

1921
from .._settings import fragment_types as ft
2022

@@ -58,6 +60,51 @@ class Config:
5860
ignore: list[str] | None = None
5961
issue_pattern: str = ""
6062

63+
@property
64+
def section_display_names(self) -> list[str]:
65+
return [x["display_name"] for x in self._section_data().values()]
66+
67+
def _section_data(self) -> Dict[str,Dict[str, Any]]:
68+
primary_addition = "(primary)"
69+
primary_exists = False
70+
selections_sections = collections.OrderedDict()
71+
nr_sections = len(self.sections)
72+
paths_seen = set()
73+
for section, path in self.sections.items():
74+
display_name = section
75+
if path in paths_seen:
76+
raise ConfigError(f"Duplicate path '{path}' for section '{section}'")
77+
paths_seen.add(path)
78+
data = {
79+
"display_name": display_name,
80+
"path": path,
81+
}
82+
83+
if section == "":
84+
display_name = "None"
85+
86+
if nr_sections == 1:
87+
display_name = f"{display_name} {primary_addition}"
88+
89+
if data["path"] == "" and not primary_exists:
90+
display_name = f"{display_name} {primary_addition}"
91+
primary_exists = True
92+
93+
data["display_name"] = display_name
94+
selections_sections[section] = data
95+
96+
if not primary_exists and nr_sections > 0:
97+
first_item = selections_sections.popitem(last=False)
98+
display_name = first_item["display_name"]
99+
first_item.update({"display_name": f"{display_name} {primary_addition}"})
100+
101+
return selections_sections
102+
103+
def get_section_for_display_name(self, section_display_name: str) -> str|None:
104+
for section, section_data in self._section_data().items():
105+
if section_display_name == section_data["display_name"]:
106+
return section
107+
return None
61108

62109
def check_issue_pattern(self, issue: str | None) -> bool|str:
63110
prompt = f"must match to {self.issue_pattern}"
@@ -69,6 +116,7 @@ def check_issue_pattern(self, issue: str | None) -> bool|str:
69116
else:
70117
return prompt
71118

119+
72120
class ConfigError(ClickException):
73121
def __init__(self, *args: str, **kwargs: str):
74122
self.failing_option = kwargs.get("failing_option")

src/towncrier/create.py

Lines changed: 27 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -107,64 +107,39 @@ def __main(
107107
if ext.lower() in (".rst", ".md"):
108108
filename_ext = ext
109109

110-
section_provided = section is not None
111-
if not section_provided:
112-
# Get the default section.
113-
if len(config.sections) == 1:
114-
section = next(iter(config.sections))
110+
if section is None:
111+
if len(config.section_display_names) == 1:
112+
section_display_name = config.section_display_names[0]
115113
else:
116-
# If there are multiple sections then the first without a path is the default
117-
# section, otherwise it's the first defined section.
118-
for (
119-
section_name,
120-
section_dir,
121-
) in config.sections.items(): # pragma: no branch
122-
if not section_dir:
123-
section = section_name
124-
break
125-
if section is None:
126-
section = list(config.sections.keys())[0]
114+
section_display_name = questionary.select(
115+
"Pick a section:",
116+
choices=config.section_display_names
117+
).ask()
118+
section = config.get_section_for_display_name(section_display_name)
119+
120+
if section.lower() == "none":
121+
section = ""
127122

128123
if section not in config.sections:
129-
# Raise a click exception with the correct parameter.
130-
section_param = None
131-
for p in ctx.command.params: # pragma: no branch
132-
if p.name == "section":
133-
section_param = p
134-
break
135-
expected_sections = ", ".join(f"'{s}'" for s in config.sections)
124+
section_param = [x for x in ctx.command.params if x.name == "section"][0]
125+
expected_sections = ", ".join(f"'{s}'" for s in config.section_display_names)
136126
raise click.BadParameter(
137-
f"expected one of {expected_sections}",
127+
f"'{section}' is not a valid section name, expected one of {expected_sections}",
138128
param=section_param,
139129
)
140-
section = cast(str, section)
141-
142-
if not filename:
143-
if not section_provided:
144-
sections = list(config.sections)
145-
if len(sections) > 1:
146-
primary_addtion = "(primary)"
147-
if len(sections) > 0:
148-
primary_addtion = f"NONE {primary_addtion}"
149-
sections[0] = str(sections[0]) + primary_addtion
150-
section = questionary.select(
151-
"Pick a section:",
152-
choices= sections
153-
).ask()
154-
section = section.replace(primary_addtion, "")
155-
156-
issue = questionary.text(
157-
"Issue number:",
158-
validate = config.check_issue_pattern
159-
).ask()
160-
fragment_type = questionary.select(
161-
"Fragment type?",
162-
choices= [type_name for type_name in config.types.keys()]
163-
).ask()
164-
165-
filename = f"{issue}.{fragment_type}"
166-
if edit is None and content == DEFAULT_CONTENT:
167-
edit = True
130+
131+
issue = questionary.text(
132+
"Issue number:",
133+
validate = config.check_issue_pattern
134+
).ask()
135+
fragment_type = questionary.select(
136+
"Fragment type:",
137+
choices= [type_name for type_name in config.types.keys()]
138+
).ask()
139+
140+
filename = f"{issue}.{fragment_type}"
141+
if edit is None and content == DEFAULT_CONTENT:
142+
edit = True
168143

169144
file_dir, file_basename = os.path.split(filename)
170145
if config.orphan_prefix and file_basename.startswith(f"{config.orphan_prefix}."):

0 commit comments

Comments
 (0)