Skip to content

Commit 98be024

Browse files
author
Louis Shawn
committed
fix(config): support dotted repository names
1 parent 029685c commit 98be024

12 files changed

Lines changed: 134 additions & 27 deletions

File tree

src/poetry/config/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
from collections.abc import Mapping
3030
from collections.abc import Sequence
3131

32-
from poetry.config.config_source import ConfigSource
32+
from poetry.config.config_source import ConfigSource
33+
from poetry.config.config_source import split_config_key
3334

3435

3536
def boolean_validator(val: str) -> bool:
@@ -313,7 +314,7 @@ def get(self, setting_name: str, default: Any = None) -> Any:
313314
"""
314315
Retrieve a setting value.
315316
"""
316-
keys = setting_name.split(".")
317+
keys = split_config_key(setting_name)
317318
build_config_settings: Mapping[
318319
NormalizedName, Mapping[str, str | Sequence[str]]
319320
] = {}

src/poetry/config/config_source.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,34 @@ def add_property(self, key: str, value: Any) -> None: ...
3333
def remove_property(self, key: str) -> None: ...
3434

3535

36+
def split_config_key(key: str) -> list[str]:
37+
parts: list[str] = []
38+
current: list[str] = []
39+
escaped = False
40+
41+
for char in key:
42+
if escaped:
43+
current.append(char)
44+
escaped = False
45+
elif char == "\\":
46+
escaped = True
47+
elif char == ".":
48+
parts.append("".join(current))
49+
current = []
50+
else:
51+
current.append(char)
52+
53+
if escaped:
54+
current.append("\\")
55+
56+
parts.append("".join(current))
57+
return parts
58+
59+
60+
def escape_config_key(key: str) -> str:
61+
return key.replace("\\", "\\\\").replace(".", "\\.")
62+
63+
3664
@dataclasses.dataclass
3765
class ConfigSourceMigration:
3866
old_key: str

src/poetry/config/dict_config_source.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from poetry.config.config_source import ConfigSource
66
from poetry.config.config_source import PropertyNotFoundError
7+
from poetry.config.config_source import split_config_key
78

89

910
class DictConfigSource(ConfigSource):
@@ -15,7 +16,7 @@ def config(self) -> dict[str, Any]:
1516
return self._config
1617

1718
def get_property(self, key: str) -> Any:
18-
keys = key.split(".")
19+
keys = split_config_key(key)
1920
config = self._config
2021

2122
for i, key in enumerate(keys):
@@ -28,7 +29,7 @@ def get_property(self, key: str) -> Any:
2829
config = config[key]
2930

3031
def add_property(self, key: str, value: Any) -> None:
31-
keys = key.split(".")
32+
keys = split_config_key(key)
3233
config = self._config
3334

3435
for i, key in enumerate(keys):
@@ -42,7 +43,7 @@ def add_property(self, key: str, value: Any) -> None:
4243
config = config[key]
4344

4445
def remove_property(self, key: str) -> None:
45-
keys = key.split(".")
46+
keys = split_config_key(key)
4647

4748
config = self._config
4849
for i, key in enumerate(keys):

src/poetry/config/file_config_source.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
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_config_key
1314

1415

1516
if TYPE_CHECKING:
@@ -33,7 +34,7 @@ def file(self) -> TOMLFile:
3334
return self._file
3435

3536
def get_property(self, key: str) -> Any:
36-
keys = key.split(".")
37+
keys = split_config_key(key)
3738

3839
config = self.file.read() if self.file.exists() else {}
3940

@@ -49,7 +50,7 @@ def get_property(self, key: str) -> Any:
4950
def add_property(self, key: str, value: Any) -> None:
5051
with self.secure() as toml:
5152
config: dict[str, Any] = toml
52-
keys = key.split(".")
53+
keys = split_config_key(key)
5354

5455
for i, key in enumerate(keys):
5556
if key not in config and i < len(keys) - 1:
@@ -64,7 +65,7 @@ def add_property(self, key: str, value: Any) -> None:
6465
def remove_property(self, key: str) -> None:
6566
with self.secure() as toml:
6667
config: dict[str, Any] = toml
67-
keys = key.split(".")
68+
keys = split_config_key(key)
6869

6970
current_config = config
7071
for i, key in enumerate(keys):

src/poetry/console/commands/config.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def handle(self) -> int:
113113

114114
from poetry.core.pyproject.exceptions import PyProjectError
115115

116-
from poetry.config.config import Config
116+
from poetry.config.config import Config
117+
from poetry.config.config_source import escape_config_key
117118
from poetry.config.file_config_source import FileConfigSource
118119
from poetry.locations import CONFIG_DIR
119120
from poetry.toml.file import TOMLFile
@@ -182,7 +183,8 @@ def handle(self) -> int:
182183
if config.get("repositories") is not None:
183184
value = config.get("repositories")
184185
else:
185-
repo = config.get(f"repositories.{m.group(1)}")
186+
repository = escape_config_key(m.group(1))
187+
repo = config.get(f"repositories.{repository}")
186188
if repo is None:
187189
raise ValueError(f"There is no {m.group(1)} repository defined")
188190

@@ -221,20 +223,23 @@ def handle(self) -> int:
221223
if m:
222224
if not m.group(1):
223225
raise ValueError("You cannot remove the [repositories] section")
226+
repository = escape_config_key(m.group(1))
224227

225228
if self.option("unset"):
226-
repo = config.get(f"repositories.{m.group(1)}")
229+
repo = config.get(f"repositories.{repository}")
227230
if repo is None:
228231
raise ValueError(f"There is no {m.group(1)} repository defined")
229232

230-
config.config_source.remove_property(f"repositories.{m.group(1)}")
233+
config.config_source.remove_property(f"repositories.{repository}")
231234

232235
return 0
233236

234237
if len(values) == 1:
235238
url = values[0]
236239

237-
config.config_source.add_property(f"repositories.{m.group(1)}.url", url)
240+
config.config_source.add_property(
241+
f"repositories.{repository}.url", url
242+
)
238243

239244
return 0
240245

@@ -286,9 +291,9 @@ def handle(self) -> int:
286291
return 0
287292

288293
# handle certs
289-
m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)", self.argument("key"))
294+
m = re.match(r"certificates\.(.+)\.(cert|client-cert)", self.argument("key"))
290295
if m:
291-
repository = m.group(1)
296+
repository = escape_config_key(m.group(1))
292297
key = m.group(2)
293298

294299
if self.option("unset"):

src/poetry/publishing/publisher.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING
66

77
from poetry.publishing.uploader import Uploader
8+
from poetry.config.config_source import escape_config_key
89
from poetry.utils.authenticator import Authenticator
910

1011

@@ -49,7 +50,8 @@ def publish(
4950
repository_name = "pypi"
5051
else:
5152
# Retrieving config information
52-
url = self._poetry.config.get(f"repositories.{repository_name}.url")
53+
repository = escape_config_key(repository_name)
54+
url = self._poetry.config.get(f"repositories.{repository}.url")
5355
if url is None:
5456
raise RuntimeError(f"Repository {repository_name} is not defined")
5557

src/poetry/utils/authenticator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from poetry.__version__ import __version__
2424
from poetry.config.config import Config
25+
from poetry.config.config_source import escape_config_key
2526
from poetry.console.exceptions import ConsoleMessage
2627
from poetry.console.exceptions import PoetryRuntimeError
2728
from poetry.exceptions import PoetryError
@@ -50,12 +51,13 @@ def create(
5051
cls, repository: str, config: Config | None
5152
) -> RepositoryCertificateConfig:
5253
config = config if config else Config.create()
54+
repository_key = escape_config_key(repository)
5355

5456
verify: str | bool = config.get(
55-
f"certificates.{repository}.verify",
56-
config.get(f"certificates.{repository}.cert", True),
57+
f"certificates.{repository_key}.verify",
58+
config.get(f"certificates.{repository_key}.cert", True),
5759
)
58-
client_cert: str = config.get(f"certificates.{repository}.client-cert")
60+
client_cert: str = config.get(f"certificates.{repository_key}.client-cert")
5961

6062
return cls(
6163
cert=Path(verify) if isinstance(verify, str) else None,
@@ -389,7 +391,8 @@ def configured_repositories(self) -> dict[str, AuthenticatorRepositoryConfig]:
389391
if self._configured_repositories is None:
390392
self._configured_repositories = {}
391393
for repository_name in self._config.get("repositories", []):
392-
url = self._config.get(f"repositories.{repository_name}.url")
394+
repository = escape_config_key(repository_name)
395+
url = self._config.get(f"repositories.{repository}.url")
393396
self._configured_repositories[repository_name] = (
394397
AuthenticatorRepositoryConfig(repository_name, url)
395398
)

src/poetry/utils/password_manager.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import TYPE_CHECKING
99

1010
from poetry.config.config import Config
11+
from poetry.config.config_source import escape_config_key
1112
from poetry.utils.threading import atomic_cached_property
1213

1314

@@ -210,10 +211,11 @@ def warn_plaintext_credentials_stored() -> None:
210211
logger.warning("Using a plaintext file to store credentials")
211212

212213
def set_pypi_token(self, repo_name: str, token: str) -> None:
214+
repository = escape_config_key(repo_name)
213215
if not self.use_keyring:
214216
self.warn_plaintext_credentials_stored()
215217
self._config.auth_config_source.add_property(
216-
f"pypi-token.{repo_name}", token
218+
f"pypi-token.{repository}", token
217219
)
218220
else:
219221
self.keyring.set_password(repo_name, "__token__", token)
@@ -228,7 +230,8 @@ def get_pypi_token(self, repo_name: str) -> str | None:
228230
:param repo_name: Name of repository.
229231
:return: Returns a token as a string if found, otherwise None.
230232
"""
231-
token: str | None = self._config.get(f"pypi-token.{repo_name}")
233+
repository = escape_config_key(repo_name)
234+
token: str | None = self._config.get(f"pypi-token.{repository}")
232235
if token:
233236
return token
234237

@@ -240,14 +243,15 @@ def get_pypi_token(self, repo_name: str) -> str | None:
240243
def delete_pypi_token(self, repo_name: str) -> None:
241244
if not self.use_keyring:
242245
return self._config.auth_config_source.remove_property(
243-
f"pypi-token.{repo_name}"
246+
f"pypi-token.{escape_config_key(repo_name)}"
244247
)
245248

246249
self.keyring.delete_password(repo_name, "__token__")
247250

248251
def get_http_auth(self, repo_name: str) -> HTTPAuthCredential:
249-
username = self._config.get(f"http-basic.{repo_name}.username")
250-
password = self._config.get(f"http-basic.{repo_name}.password")
252+
repository = escape_config_key(repo_name)
253+
username = self._config.get(f"http-basic.{repository}.username")
254+
password = self._config.get(f"http-basic.{repository}.password")
251255

252256
if password is None and self.use_keyring:
253257
password = self.keyring.get_password(repo_name, username)
@@ -264,7 +268,8 @@ def set_http_password(self, repo_name: str, username: str, password: str) -> Non
264268
else:
265269
self.keyring.set_password(repo_name, username, password)
266270

267-
self._config.auth_config_source.add_property(f"http-basic.{repo_name}", auth)
271+
repository = escape_config_key(repo_name)
272+
self._config.auth_config_source.add_property(f"http-basic.{repository}", auth)
268273

269274
def delete_http_password(self, repo_name: str) -> None:
270275
auth = self.get_http_auth(repo_name)
@@ -275,7 +280,8 @@ def delete_http_password(self, repo_name: str) -> None:
275280
with suppress(PoetryKeyringError):
276281
self.keyring.delete_password(repo_name, auth.username)
277282

278-
self._config.auth_config_source.remove_property(f"http-basic.{repo_name}")
283+
repository = escape_config_key(repo_name)
284+
self._config.auth_config_source.remove_property(f"http-basic.{repository}")
279285

280286
def get_credential(
281287
self, *names: str, username: str | None = None

tests/config/test_dict_config_source.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,20 @@ def test_dict_config_source_get_property_should_raise_if_not_found() -> None:
6666
PropertyNotFoundError, match=r"Key virtualenvs\.use-poetry-python not in config"
6767
):
6868
_ = config_source.get_property("virtualenvs.use-poetry-python")
69+
70+
71+
def test_dict_config_source_escaped_dot_key() -> None:
72+
config_source = DictConfigSource()
73+
74+
config_source.add_property("repositories.foo\\.bar.url", "https://example.com/simple")
75+
assert config_source._config == {
76+
"repositories": {"foo.bar": {"url": "https://example.com/simple"}}
77+
}
78+
79+
assert (
80+
config_source.get_property("repositories.foo\\.bar.url")
81+
== "https://example.com/simple"
82+
)
83+
84+
config_source.remove_property("repositories.foo\\.bar.url")
85+
assert config_source._config == {"repositories": {"foo.bar": {}}}

tests/config/test_file_config_source.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,19 @@ def test_file_config_source_get_property_should_raise_if_not_found(
8989
PropertyNotFoundError, match=r"Key virtualenvs\.use-poetry-python not in config"
9090
):
9191
_ = config_source.get_property("virtualenvs.use-poetry-python")
92+
93+
94+
def test_file_config_source_escaped_dot_key(tmp_path: Path) -> None:
95+
config = tmp_path.joinpath("config.toml")
96+
config.touch()
97+
98+
config_source = FileConfigSource(TOMLFile(config))
99+
config_source.add_property("repositories.foo\\.bar.url", "https://example.com/simple")
100+
101+
assert config_source._file.read() == {
102+
"repositories": {"foo.bar": {"url": "https://example.com/simple"}}
103+
}
104+
assert (
105+
config_source.get_property("repositories.foo\\.bar.url")
106+
== "https://example.com/simple"
107+
)

0 commit comments

Comments
 (0)