Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add option to migrate outdated configs #9830

Merged
merged 8 commits into from
Nov 18, 2024
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ Without `--` this command will fail if `${GITLAB_JOB_TOKEN}` starts with a hyphe
* `--unset`: Remove the configuration element named by `setting-key`.
* `--list`: Show the list of current config variables.
* `--local`: Set/Get settings that are specific to a project (in the local configuration file `poetry.toml`).
* `--migrate`: Migrate outdated configuration settings.

## run

Expand Down
15 changes: 15 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ This also works for secret settings, like credentials:
export POETRY_HTTP_BASIC_MY_REPOSITORY_PASSWORD=secret
```

## Migrate outdated configs

If poetry renames or remove config options it might be necessary to migrate explicit set options. This is possible
by running:

```bash
poetry config --migrate
```

If you need to migrate a local config run:

```bash
poetry config --migrate --local
```

## Default Directories

Poetry uses the following default directories:
Expand Down
86 changes: 86 additions & 0 deletions src/poetry/config/config_source.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,99 @@
from __future__ import annotations

import dataclasses
import json

from abc import ABC
from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import Any

from cleo.io.null_io import NullIO


if TYPE_CHECKING:
from cleo.io.io import IO


UNSET = object()


class PropertyNotFoundError(ValueError):
pass


class ConfigSource(ABC):
@abstractmethod
def get_property(self, key: str) -> Any: ...

@abstractmethod
def add_property(self, key: str, value: Any) -> None: ...

@abstractmethod
def remove_property(self, key: str) -> None: ...


@dataclasses.dataclass
class ConfigSourceMigration:
old_key: str
new_key: str | None
value_migration: dict[Any, Any] = dataclasses.field(default_factory=dict)

def dry_run(self, config_source: ConfigSource, io: IO | None = None) -> bool:
io = io or NullIO()

try:
old_value = config_source.get_property(self.old_key)
except PropertyNotFoundError:
return False

new_value = (
self.value_migration[old_value] if self.value_migration else old_value
)

msg = f"<c1>{self.old_key}</c1> = <c2>{json.dumps(old_value)}</c2>"

if self.new_key is not None and new_value is not UNSET:
msg += f" -> <c1>{self.new_key}</c1> = <c2>{json.dumps(new_value)}</c2>"
elif self.new_key is None:
msg += " -> <c1>Removed from config</c1>"
elif self.new_key and new_value is UNSET:
msg += f" -> <c1>{self.new_key}</c1> = <c2>Not explicit set</c2>"

io.write_line(msg)

return True

def apply(self, config_source: ConfigSource) -> None:
try:
old_value = config_source.get_property(self.old_key)
except PropertyNotFoundError:
return

new_value = (
self.value_migration[old_value] if self.value_migration else old_value
)

config_source.remove_property(self.old_key)

if self.new_key is not None and new_value is not UNSET:
config_source.add_property(self.new_key, new_value)


def drop_empty_config_category(
keys: list[str], config: dict[Any, Any]
) -> dict[Any, Any]:
config_ = {}

for key, value in config.items():
if not keys or key != keys[0]:
config_[key] = value
continue
if keys and key == keys[0]:
if isinstance(value, dict):
value = drop_empty_config_category(keys[1:], value)

if value != {}:
config_[key] = value

return config_
14 changes: 14 additions & 0 deletions src/poetry/config/dict_config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any

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


class DictConfigSource(ConfigSource):
Expand All @@ -13,6 +14,19 @@ def __init__(self) -> None:
def config(self) -> dict[str, Any]:
return self._config

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

for i, key in enumerate(keys):
if key not in config:
raise PropertyNotFoundError(f"Key {'.'.join(keys)} not in config")

if i == len(keys) - 1:
return config[key]

config = config[key]

def add_property(self, key: str, value: Any) -> None:
keys = key.split(".")
config = self._config
Expand Down
20 changes: 20 additions & 0 deletions src/poetry/config/file_config_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from tomlkit import table

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


if TYPE_CHECKING:
Expand All @@ -30,6 +32,20 @@ def name(self) -> str:
def file(self) -> TOMLFile:
return self._file

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

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

for i, key in enumerate(keys):
if key not in config:
raise PropertyNotFoundError(f"Key {'.'.join(keys)} not in config")

if i == len(keys) - 1:
return config[key]

config = config[key]

def add_property(self, key: str, value: Any) -> None:
with self.secure() as toml:
config: dict[str, Any] = toml
Expand Down Expand Up @@ -62,6 +78,10 @@ def remove_property(self, key: str) -> None:

current_config = current_config[key]

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

@contextmanager
def secure(self) -> Iterator[TOMLDocument]:
if self.file.exists():
Expand Down
51 changes: 51 additions & 0 deletions src/poetry/console/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from poetry.config.config import boolean_normalizer
from poetry.config.config import boolean_validator
from poetry.config.config import int_normalizer
from poetry.config.config_source import UNSET
from poetry.config.config_source import ConfigSourceMigration
from poetry.console.commands.command import Command


Expand All @@ -25,6 +27,17 @@

from poetry.config.config_source import ConfigSource

CONFIG_MIGRATIONS = [
ConfigSourceMigration(
old_key="experimental.system-git-client", new_key="system-git-client"
),
ConfigSourceMigration(
old_key="virtualenvs.prefer-active-python",
new_key="virtualenvs.use-poetry-python",
value_migration={True: UNSET, False: True},
),
]


class ConfigCommand(Command):
name = "config"
Expand All @@ -39,6 +52,7 @@ class ConfigCommand(Command):
option("list", None, "List configuration settings."),
option("unset", None, "Unset configuration setting."),
option("local", None, "Set/Get from the project's local configuration."),
option("migrate", None, "Migrate outdated configuration settings."),
]

help = """\
Expand Down Expand Up @@ -98,6 +112,9 @@ def handle(self) -> int:
from poetry.locations import CONFIG_DIR
from poetry.toml.file import TOMLFile

if self.option("migrate"):
self._migrate()

config = Config.create()
config_file = TOMLFile(CONFIG_DIR / "config.toml")

Expand Down Expand Up @@ -325,3 +342,37 @@ def _list_configuration(
message = f"<c1>{k + key}</c1> = <c2>{json.dumps(value)}</c2>"

self.line(message)

def _migrate(self) -> None:
from poetry.config.file_config_source import FileConfigSource
from poetry.locations import CONFIG_DIR
from poetry.toml.file import TOMLFile

config_file = TOMLFile(CONFIG_DIR / "config.toml")

if self.option("local"):
config_file = TOMLFile(self.poetry.file.path.parent / "poetry.toml")
if not config_file.exists():
raise RuntimeError("No local config file found")

config_source = FileConfigSource(config_file)

self.io.write_line("Checking for required migrations ...")

required_migrations = [
migration
for migration in CONFIG_MIGRATIONS
if migration.dry_run(config_source, io=self.io)
]

if not required_migrations:
self.io.write_line("Already up to date.")
return

if not self.io.is_interactive() or self.confirm(
"Proceed with migration?: ", False
):
for migration in required_migrations:
migration.apply(config_source)

self.io.write_line("Config migration successfully done.")
Loading
Loading