Skip to content

Commit 8d84f20

Browse files
yosofbadrradoering
andauthored
fix: support periods in repository names (#10845)
The config source methods (get_property, add_property, remove_property) blindly split keys on periods, which breaks repository names containing periods (e.g. "my.repo"). This causes http-basic, pypi-token, repositories, and certificates config entries to be incorrectly nested. The fix adds a split_key() helper and updates the ConfigSource interface to accept either a dotted string or a pre-split list of key segments. All call sites that embed repository names now pass lists to preserve names with periods as single key segments. Co-authored-by: Randy Döring <30527984+radoering@users.noreply.github.com>
1 parent 720d3cd commit 8d84f20

12 files changed

Lines changed: 309 additions & 86 deletions

File tree

src/poetry/config/config.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from packaging.utils import NormalizedName
1717
from packaging.utils import canonicalize_name
1818

19+
from poetry.config.config_source import split_key
1920
from poetry.config.dict_config_source import DictConfigSource
2021
from poetry.config.file_config_source import FileConfigSource
2122
from poetry.locations import CONFIG_DIR
@@ -309,34 +310,35 @@ def installer_max_workers(self) -> int:
309310
return default_max_workers
310311
return min(default_max_workers, int(desired_max_workers))
311312

312-
def get(self, setting_name: str, default: Any = None) -> Any:
313+
def get(self, setting_name: str | Sequence[str], default: Any = None) -> Any:
313314
"""
314315
Retrieve a setting value.
316+
317+
The setting_name can be a dotted string (e.g. "virtualenvs.create")
318+
or a list of key segments (e.g. ["http-basic", "my.repo", "username"])
319+
when any segment may contain periods.
315320
"""
316-
keys = setting_name.split(".")
321+
keys = split_key(setting_name)
317322
build_config_settings: Mapping[
318323
NormalizedName, Mapping[str, str | Sequence[str]]
319324
] = {}
320325

321326
# Looking in the environment if the setting
322327
# is set via a POETRY_* environment variable
323328
if self._use_environment:
324-
if setting_name == "repositories":
329+
if keys == ["repositories"]:
325330
# repositories setting is special for now
326331
repositories = self._get_environment_repositories()
327332
if repositories:
328333
return repositories
329334

330-
build_config_settings_key = "installer.build-config-settings"
331-
if setting_name == build_config_settings_key or setting_name.startswith(
332-
f"{build_config_settings_key}."
333-
):
335+
if keys[:2] == ["installer", "build-config-settings"]:
334336
build_config_settings = self._get_environment_build_config_settings()
335337
else:
336338
env = "POETRY_" + "_".join(k.upper().replace("-", "_") for k in keys)
337339
env_value = os.getenv(env)
338340
if env_value is not None:
339-
return self.process(self._get_normalizer(setting_name)(env_value))
341+
return self.process(self._get_normalizer(keys)(env_value))
340342

341343
value = self._config
342344

@@ -355,7 +357,7 @@ def get(self, setting_name: str, default: Any = None) -> Any:
355357
if self._use_environment and isinstance(value, dict):
356358
# this is a configuration table, it is likely that we missed env vars
357359
# in order to capture them recurse, eg: virtualenvs.options
358-
return {k: self.get(f"{setting_name}.{k}") for k in value}
360+
return {k: self.get([*keys, k]) for k in value}
359361

360362
return self.process(value)
361363

@@ -376,7 +378,9 @@ def resolve_from_config(match: re.Match[str]) -> str:
376378
return re.sub(r"{(.+?)}", resolve_from_config, value)
377379

378380
@staticmethod
379-
def _get_normalizer(name: str) -> Callable[[str], Any]:
381+
def _get_normalizer(name: str | Sequence[str]) -> Callable[[str], Any]:
382+
if not isinstance(name, str):
383+
name = ".".join(name)
380384
if name in {
381385
"virtualenvs.create",
382386
"virtualenvs.in-project",

src/poetry/config/config_source.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,42 @@
1212

1313

1414
if TYPE_CHECKING:
15+
from collections.abc import Sequence
16+
1517
from cleo.io.io import IO
1618

1719

1820
UNSET = object()
1921

2022

23+
def split_key(key: str | Sequence[str]) -> list[str]:
24+
"""Split a config key into its component parts.
25+
26+
If the key is a string, split on periods.
27+
28+
If the key contains segments with periods
29+
(e.g. repository names like "my.repo"),
30+
this should be handled before calling this function
31+
so that a list or tuple of the segments is passed.
32+
"""
33+
if isinstance(key, str):
34+
return key.split(".")
35+
return list(key)
36+
37+
2138
class PropertyNotFoundError(ValueError):
2239
pass
2340

2441

2542
class ConfigSource(ABC):
2643
@abstractmethod
27-
def get_property(self, key: str) -> Any: ...
44+
def get_property(self, key: str | Sequence[str]) -> Any: ...
2845

2946
@abstractmethod
30-
def add_property(self, key: str, value: Any) -> None: ...
47+
def add_property(self, key: str | Sequence[str], value: Any) -> None: ...
3148

3249
@abstractmethod
33-
def remove_property(self, key: str) -> None: ...
50+
def remove_property(self, key: str | Sequence[str]) -> None: ...
3451

3552

3653
@dataclasses.dataclass
@@ -81,7 +98,7 @@ def apply(self, config_source: ConfigSource) -> None:
8198

8299

83100
def drop_empty_config_category(
84-
keys: list[str], config: dict[Any, Any]
101+
keys: Sequence[str], config: dict[Any, Any]
85102
) -> dict[Any, Any]:
86103
config_ = {}
87104

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from __future__ import annotations
22

3+
from typing import TYPE_CHECKING
34
from typing import Any
45

56
from poetry.config.config_source import ConfigSource
67
from poetry.config.config_source import PropertyNotFoundError
8+
from poetry.config.config_source import split_key
9+
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Sequence
713

814

915
class DictConfigSource(ConfigSource):
@@ -14,44 +20,44 @@ def __init__(self) -> None:
1420
def config(self) -> dict[str, Any]:
1521
return self._config
1622

17-
def get_property(self, key: str) -> Any:
18-
keys = key.split(".")
23+
def get_property(self, key: str | Sequence[str]) -> Any:
24+
keys = split_key(key)
1925
config = self._config
2026

21-
for i, key in enumerate(keys):
22-
if key not in config:
27+
for i, sub_key in enumerate(keys):
28+
if sub_key not in config:
2329
raise PropertyNotFoundError(f"Key {'.'.join(keys)} not in config")
2430

2531
if i == len(keys) - 1:
26-
return config[key]
32+
return config[sub_key]
2733

28-
config = config[key]
34+
config = config[sub_key]
2935

30-
def add_property(self, key: str, value: Any) -> None:
31-
keys = key.split(".")
36+
def add_property(self, key: str | Sequence[str], value: Any) -> None:
37+
keys = split_key(key)
3238
config = self._config
3339

34-
for i, key in enumerate(keys):
35-
if key not in config and i < len(keys) - 1:
36-
config[key] = {}
40+
for i, sub_key in enumerate(keys):
41+
if sub_key not in config and i < len(keys) - 1:
42+
config[sub_key] = {}
3743

3844
if i == len(keys) - 1:
39-
config[key] = value
45+
config[sub_key] = value
4046
break
4147

42-
config = config[key]
48+
config = config[sub_key]
4349

44-
def remove_property(self, key: str) -> None:
45-
keys = key.split(".")
50+
def remove_property(self, key: str | Sequence[str]) -> None:
51+
keys = split_key(key)
4652

4753
config = self._config
48-
for i, key in enumerate(keys):
49-
if key not in config:
54+
for i, sub_key in enumerate(keys):
55+
if sub_key not in config:
5056
return
5157

5258
if i == len(keys) - 1:
53-
del config[key]
59+
del config[sub_key]
5460

5561
break
5662

57-
config = config[key]
63+
config = config[sub_key]

src/poetry/config/file_config_source.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
from poetry.config.config_source import ConfigSource
1111
from poetry.config.config_source import PropertyNotFoundError
1212
from poetry.config.config_source import drop_empty_config_category
13+
from poetry.config.config_source import split_key
1314

1415

1516
if TYPE_CHECKING:
1617
from collections.abc import Iterator
18+
from collections.abc import Sequence
1719

1820
from tomlkit.toml_document import TOMLDocument
1921

@@ -32,51 +34,51 @@ def name(self) -> str:
3234
def file(self) -> TOMLFile:
3335
return self._file
3436

35-
def get_property(self, key: str) -> Any:
36-
keys = key.split(".")
37+
def get_property(self, key: str | Sequence[str]) -> Any:
38+
keys = split_key(key)
3739

3840
config = self.file.read() if self.file.exists() else {}
3941

40-
for i, key in enumerate(keys):
41-
if key not in config:
42+
for i, sub_key in enumerate(keys):
43+
if sub_key not in config:
4244
raise PropertyNotFoundError(f"Key {'.'.join(keys)} not in config")
4345

4446
if i == len(keys) - 1:
45-
return config[key]
47+
return config[sub_key]
4648

47-
config = config[key]
49+
config = config[sub_key]
4850

49-
def add_property(self, key: str, value: Any) -> None:
51+
def add_property(self, key: str | Sequence[str], value: Any) -> None:
5052
with self.secure() as toml:
5153
config: dict[str, Any] = toml
52-
keys = key.split(".")
54+
keys = split_key(key)
5355

54-
for i, key in enumerate(keys):
55-
if key not in config and i < len(keys) - 1:
56-
config[key] = table()
56+
for i, sub_key in enumerate(keys):
57+
if sub_key not in config and i < len(keys) - 1:
58+
config[sub_key] = table()
5759

5860
if i == len(keys) - 1:
59-
config[key] = value
61+
config[sub_key] = value
6062
break
6163

62-
config = config[key]
64+
config = config[sub_key]
6365

64-
def remove_property(self, key: str) -> None:
66+
def remove_property(self, key: str | Sequence[str]) -> None:
6567
with self.secure() as toml:
6668
config: dict[str, Any] = toml
67-
keys = key.split(".")
69+
keys = split_key(key)
6870

6971
current_config = config
70-
for i, key in enumerate(keys):
71-
if key not in current_config:
72+
for i, sub_key in enumerate(keys):
73+
if sub_key not in current_config:
7274
return
7375

7476
if i == len(keys) - 1:
75-
del current_config[key]
77+
del current_config[sub_key]
7678

7779
break
7880

79-
current_config = current_config[key]
81+
current_config = current_config[sub_key]
8082

8183
current_config = drop_empty_config_category(keys=keys[:-1], config=config)
8284
config.clear()

src/poetry/console/commands/config.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ def handle(self) -> int:
150150
if self.argument("value") and self.option("unset"):
151151
raise RuntimeError("You can not combine a setting value with --unset")
152152

153+
repo_regex = r"^repos?(?:itories)?(?:\.(.+?)(?:\.(url))?)?$"
154+
153155
# show the value if no value is provided
154156
if not self.argument("value") and not self.option("unset"):
155157
if setting_key.split(".")[0] in self.LIST_PROHIBITED_SETTINGS:
@@ -176,17 +178,18 @@ def handle(self) -> int:
176178
f"No build config settings configured for <c1>{package_name}</>."
177179
)
178180
return 0
179-
elif m := re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")):
181+
elif m := re.match(repo_regex, self.argument("key")):
180182
if not m.group(1):
181183
value = {}
182184
if config.get("repositories") is not None:
183185
value = config.get("repositories")
184186
else:
185-
repo = config.get(f"repositories.{m.group(1)}")
187+
repo_name = m.group(1)
188+
repo = config.get(["repositories", repo_name])
186189
if repo is None:
187-
raise ValueError(f"There is no {m.group(1)} repository defined")
190+
raise ValueError(f"There is no {repo_name} repository defined")
188191

189-
value = repo
192+
value = repo.get(sub_key, "") if (sub_key := m.group(2)) else repo
190193

191194
self.line(str(value))
192195
else:
@@ -217,24 +220,34 @@ def handle(self) -> int:
217220
)
218221

219222
# handle repositories
220-
m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key"))
223+
m = re.match(repo_regex, self.argument("key"))
221224
if m:
222225
if not m.group(1):
223226
raise ValueError("You cannot remove the [repositories] section")
224227

228+
repo_name = m.group(1)
229+
225230
if self.option("unset"):
226-
repo = config.get(f"repositories.{m.group(1)}")
231+
repo = config.get(["repositories", repo_name])
227232
if repo is None:
228-
raise ValueError(f"There is no {m.group(1)} repository defined")
233+
raise ValueError(f"There is no {repo_name} repository defined")
229234

230-
config.config_source.remove_property(f"repositories.{m.group(1)}")
235+
if m.group(2):
236+
# Unset a specific sub-property
237+
config.config_source.remove_property(
238+
["repositories", repo_name, m.group(2)]
239+
)
240+
else:
241+
config.config_source.remove_property(["repositories", repo_name])
231242

232243
return 0
233244

234245
if len(values) == 1:
235246
url = values[0]
236247

237-
config.config_source.add_property(f"repositories.{m.group(1)}.url", url)
248+
config.config_source.add_property(
249+
["repositories", repo_name, "url"], url
250+
)
238251

239252
return 0
240253

@@ -286,14 +299,14 @@ def handle(self) -> int:
286299
return 0
287300

288301
# handle certs
289-
m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)", self.argument("key"))
302+
m = re.match(r"certificates\.(.+)\.(cert|client-cert)$", self.argument("key"))
290303
if m:
291304
repository = m.group(1)
292305
key = m.group(2)
293306

294307
if self.option("unset"):
295308
config.auth_config_source.remove_property(
296-
f"certificates.{repository}.{key}"
309+
["certificates", repository, key]
297310
)
298311

299312
return 0
@@ -305,7 +318,7 @@ def handle(self) -> int:
305318
new_value = boolean_normalizer(values[0])
306319

307320
config.auth_config_source.add_property(
308-
f"certificates.{repository}.{key}", new_value
321+
["certificates", repository, key], new_value
309322
)
310323
else:
311324
raise ValueError("You must pass exactly 1 value")

src/poetry/publishing/publisher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def publish(
5555
repository_name = "pypi"
5656
else:
5757
# Retrieving config information
58-
url = self._poetry.config.get(f"repositories.{repository_name}.url")
58+
url = self._poetry.config.get(["repositories", repository_name, "url"])
5959
if url is None:
6060
raise RuntimeError(f"Repository {repository_name} is not defined")
6161
url = _normalize_legacy_repository_url(url)

0 commit comments

Comments
 (0)