Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/poetry/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from collections.abc import Mapping
from collections.abc import Sequence

from poetry.config.config_source import ConfigSource
from poetry.config.config_source import ConfigSource
from poetry.config.config_source import split_config_key


def boolean_validator(val: str) -> bool:
Expand Down Expand Up @@ -313,7 +314,7 @@ def get(self, setting_name: str, default: Any = None) -> Any:
"""
Retrieve a setting value.
"""
keys = setting_name.split(".")
keys = split_config_key(setting_name)
build_config_settings: Mapping[
NormalizedName, Mapping[str, str | Sequence[str]]
] = {}
Expand Down
28 changes: 28 additions & 0 deletions src/poetry/config/config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@ def add_property(self, key: str, value: Any) -> None: ...
def remove_property(self, key: str) -> None: ...


def split_config_key(key: str) -> list[str]:
parts: list[str] = []
current: list[str] = []
escaped = False

for char in key:
if escaped:
current.append(char)
escaped = False
elif char == "\\":
escaped = True
elif char == ".":
parts.append("".join(current))
current = []
else:
current.append(char)

if escaped:
current.append("\\")

parts.append("".join(current))
return parts


def escape_config_key(key: str) -> str:
return key.replace("\\", "\\\\").replace(".", "\\.")


@dataclasses.dataclass
class ConfigSourceMigration:
old_key: str
Expand Down
7 changes: 4 additions & 3 deletions src/poetry/config/dict_config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from poetry.config.config_source import ConfigSource
from poetry.config.config_source import PropertyNotFoundError
from poetry.config.config_source import split_config_key


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

def get_property(self, key: str) -> Any:
keys = key.split(".")
keys = split_config_key(key)
config = self._config

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

def add_property(self, key: str, value: Any) -> None:
keys = key.split(".")
keys = split_config_key(key)
config = self._config

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

def remove_property(self, key: str) -> None:
keys = key.split(".")
keys = split_config_key(key)

config = self._config
for i, key in enumerate(keys):
Expand Down
7 changes: 4 additions & 3 deletions src/poetry/config/file_config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from poetry.config.config_source import ConfigSource
from poetry.config.config_source import PropertyNotFoundError
from poetry.config.config_source import drop_empty_config_category
from poetry.config.config_source import split_config_key


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

def get_property(self, key: str) -> Any:
keys = key.split(".")
keys = split_config_key(key)

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

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

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

current_config = config
for i, key in enumerate(keys):
Expand Down
18 changes: 11 additions & 7 deletions src/poetry/console/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def handle(self) -> int:
from poetry.core.pyproject.exceptions import PyProjectError

from poetry.config.config import Config
from poetry.config.config_source import escape_config_key
from poetry.config.file_config_source import FileConfigSource
from poetry.locations import CONFIG_DIR
from poetry.toml.file import TOMLFile
Expand Down Expand Up @@ -182,7 +183,8 @@ def handle(self) -> int:
if config.get("repositories") is not None:
value = config.get("repositories")
else:
repo = config.get(f"repositories.{m.group(1)}")
repository = escape_config_key(m.group(1))
repo = config.get(f"repositories.{repository}")
if repo is None:
raise ValueError(f"There is no {m.group(1)} repository defined")

Expand Down Expand Up @@ -221,20 +223,22 @@ def handle(self) -> int:
if m:
if not m.group(1):
raise ValueError("You cannot remove the [repositories] section")
repository = escape_config_key(m.group(1))

if self.option("unset"):
repo = config.get(f"repositories.{m.group(1)}")
if repo is None:
try:
config.config_source.get_property(f"repositories.{repository}")
except PropertyNotFoundError:
raise ValueError(f"There is no {m.group(1)} repository defined")

config.config_source.remove_property(f"repositories.{m.group(1)}")
config.config_source.remove_property(f"repositories.{repository}")

return 0

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

config.config_source.add_property(f"repositories.{m.group(1)}.url", url)
config.config_source.add_property(f"repositories.{repository}.url", url)

return 0

Expand Down Expand Up @@ -286,9 +290,9 @@ def handle(self) -> int:
return 0

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

if self.option("unset"):
Expand Down
4 changes: 3 additions & 1 deletion src/poetry/publishing/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing import TYPE_CHECKING

from poetry.config.config_source import escape_config_key
from poetry.publishing.uploader import Uploader
from poetry.utils.authenticator import Authenticator

Expand Down Expand Up @@ -49,7 +50,8 @@ def publish(
repository_name = "pypi"
else:
# Retrieving config information
url = self._poetry.config.get(f"repositories.{repository_name}.url")
repository = escape_config_key(repository_name)
url = self._poetry.config.get(f"repositories.{repository}.url")
if url is None:
raise RuntimeError(f"Repository {repository_name} is not defined")

Expand Down
11 changes: 7 additions & 4 deletions src/poetry/utils/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from poetry.__version__ import __version__
from poetry.config.config import Config
from poetry.config.config_source import escape_config_key
from poetry.console.exceptions import ConsoleMessage
from poetry.console.exceptions import PoetryRuntimeError
from poetry.exceptions import PoetryError
Expand Down Expand Up @@ -50,12 +51,13 @@ def create(
cls, repository: str, config: Config | None
) -> RepositoryCertificateConfig:
config = config if config else Config.create()
repository_key = escape_config_key(repository)

verify: str | bool = config.get(
f"certificates.{repository}.verify",
config.get(f"certificates.{repository}.cert", True),
f"certificates.{repository_key}.verify",
config.get(f"certificates.{repository_key}.cert", True),
)
client_cert: str = config.get(f"certificates.{repository}.client-cert")
client_cert: str = config.get(f"certificates.{repository_key}.client-cert")

return cls(
cert=Path(verify) if isinstance(verify, str) else None,
Expand Down Expand Up @@ -389,7 +391,8 @@ def configured_repositories(self) -> dict[str, AuthenticatorRepositoryConfig]:
if self._configured_repositories is None:
self._configured_repositories = {}
for repository_name in self._config.get("repositories", []):
url = self._config.get(f"repositories.{repository_name}.url")
repository = escape_config_key(repository_name)
url = self._config.get(f"repositories.{repository}.url")
self._configured_repositories[repository_name] = (
AuthenticatorRepositoryConfig(repository_name, url)
)
Expand Down
20 changes: 13 additions & 7 deletions src/poetry/utils/password_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import TYPE_CHECKING

from poetry.config.config import Config
from poetry.config.config_source import escape_config_key
from poetry.utils.threading import atomic_cached_property


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

def set_pypi_token(self, repo_name: str, token: str) -> None:
repository = escape_config_key(repo_name)
if not self.use_keyring:
self.warn_plaintext_credentials_stored()
self._config.auth_config_source.add_property(
f"pypi-token.{repo_name}", token
f"pypi-token.{repository}", token
)
else:
self.keyring.set_password(repo_name, "__token__", token)
Expand All @@ -228,7 +230,8 @@ def get_pypi_token(self, repo_name: str) -> str | None:
:param repo_name: Name of repository.
:return: Returns a token as a string if found, otherwise None.
"""
token: str | None = self._config.get(f"pypi-token.{repo_name}")
repository = escape_config_key(repo_name)
token: str | None = self._config.get(f"pypi-token.{repository}")
if token:
return token

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

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

def get_http_auth(self, repo_name: str) -> HTTPAuthCredential:
username = self._config.get(f"http-basic.{repo_name}.username")
password = self._config.get(f"http-basic.{repo_name}.password")
repository = escape_config_key(repo_name)
username = self._config.get(f"http-basic.{repository}.username")
password = self._config.get(f"http-basic.{repository}.password")

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

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

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

self._config.auth_config_source.remove_property(f"http-basic.{repo_name}")
repository = escape_config_key(repo_name)
self._config.auth_config_source.remove_property(f"http-basic.{repository}")

def get_credential(
self, *names: str, username: str | None = None
Expand Down
19 changes: 19 additions & 0 deletions tests/config/test_dict_config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,22 @@ def test_dict_config_source_get_property_should_raise_if_not_found() -> None:
PropertyNotFoundError, match=r"Key virtualenvs\.use-poetry-python not in config"
):
_ = config_source.get_property("virtualenvs.use-poetry-python")


def test_dict_config_source_escaped_dot_key() -> None:
config_source = DictConfigSource()

config_source.add_property(
"repositories.foo\\.bar.url", "https://example.com/simple"
)
assert config_source._config == {
"repositories": {"foo.bar": {"url": "https://example.com/simple"}}
}

assert (
config_source.get_property("repositories.foo\\.bar.url")
== "https://example.com/simple"
)

config_source.remove_property("repositories.foo\\.bar.url")
assert config_source._config == {"repositories": {"foo.bar": {}}}
18 changes: 18 additions & 0 deletions tests/config/test_file_config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,21 @@ def test_file_config_source_get_property_should_raise_if_not_found(
PropertyNotFoundError, match=r"Key virtualenvs\.use-poetry-python not in config"
):
_ = config_source.get_property("virtualenvs.use-poetry-python")


def test_file_config_source_escaped_dot_key(tmp_path: Path) -> None:
config = tmp_path.joinpath("config.toml")
config.touch()

config_source = FileConfigSource(TOMLFile(config))
config_source.add_property(
"repositories.foo\\.bar.url", "https://example.com/simple"
)

assert config_source._file.read() == {
"repositories": {"foo.bar": {"url": "https://example.com/simple"}}
}
assert (
config_source.get_property("repositories.foo\\.bar.url")
== "https://example.com/simple"
)
7 changes: 7 additions & 0 deletions tests/console/commands/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,13 @@ def test_display_single_setting(
assert tester.io.fetch_output() == expected


def test_repositories_setting_with_dot_in_name(tester: CommandTester) -> None:
tester.execute("repositories.foo.bar https://bar.com/simple/")
tester.execute("repositories.foo.bar")

assert tester.io.fetch_output() == "{'url': 'https://bar.com/simple/'}\n"


def test_display_single_local_setting(
command_tester_factory: CommandTesterFactory, fixture_dir: FixtureDirGetter
) -> None:
Expand Down
Loading