Skip to content

Commit 56bb755

Browse files
committed
Small fixes and updates
1 parent b7b15b4 commit 56bb755

File tree

4 files changed

+275
-8
lines changed

4 files changed

+275
-8
lines changed

ddev/src/ddev/cli/config/override.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# (C) Datadog, Inc. 2025-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from __future__ import annotations
5+
6+
from typing import TYPE_CHECKING
7+
8+
import click
9+
10+
if TYPE_CHECKING:
11+
from ddev.cli.application import Application
12+
13+
14+
def repo_to_override(app: Application) -> str:
15+
from ddev.utils.metadata import (
16+
InvalidMetadataError,
17+
PyProjectNotFoundError,
18+
RepoNotFoundError,
19+
ValidRepo,
20+
pyproject_metadata,
21+
)
22+
23+
repos_map = {
24+
ValidRepo.CORE: "core",
25+
ValidRepo.EXTRAS: "extras",
26+
ValidRepo.INTERNAL: "internal",
27+
ValidRepo.AGENT: "agent",
28+
ValidRepo.MARKETPLACE: "marketplace",
29+
ValidRepo.INTEGRATIONS_INTERNAL_CORE: "integrations-internal-core",
30+
}
31+
32+
try:
33+
metadata = pyproject_metadata()
34+
if metadata is None:
35+
raise RepoNotFoundError()
36+
repo = repos_map[metadata.repo]
37+
except (PyProjectNotFoundError, RepoNotFoundError):
38+
app.display_error(
39+
"The current repo could not be inferred. Either this is not a repository or the root of "
40+
"the repo is missing the ddev tool configuration in its pyproject.toml file."
41+
)
42+
43+
repo = app.prompt(
44+
"What repo are you trying to override? ",
45+
type=click.Choice(list(repos_map.values())),
46+
show_choices=True,
47+
)
48+
except InvalidMetadataError as e:
49+
from rich.markup import escape
50+
51+
# Ensure escaping to avoid rich reading the table name as style markup
52+
app.display_error(escape(str(e)))
53+
app.abort()
54+
except Exception as e:
55+
app.display_error(f"An unexpected error occurred: {e}")
56+
app.abort()
57+
58+
return repo
59+
60+
61+
@click.command()
62+
@click.pass_obj
63+
def override(app: Application):
64+
"""
65+
Overrides the repo configuration with a `.ddev.toml` file in the current working directory.
66+
67+
The command tries to identify the repo you are in by reading the `repo` field in the `[tool.ddev]` table in
68+
the `pyproject.toml` file located at the root of your git repository.
69+
70+
If the current directory is not part of a git repository, the repository root does not have a `pyproject.toml`
71+
file, or the file exists but has no `[tool.ddev]` table, you will be prompted to specify which repo
72+
configuration to override.
73+
"""
74+
from rich.syntax import Syntax
75+
76+
from ddev.config.file import DDEV_TOML, RootConfig, deep_merge_with_list_handling
77+
from ddev.config.utils import scrub_config
78+
from ddev.utils.fs import Path
79+
from ddev.utils.toml import dumps_toml_data
80+
81+
app.config_file.overrides_path = Path.cwd() / DDEV_TOML
82+
repo = repo_to_override(app)
83+
84+
local_repo_config = {
85+
"repo": repo,
86+
"repos": {repo: str(app.config_file.overrides_path.resolve().parent)},
87+
}
88+
89+
if app.config_file.overrides_path.exists():
90+
app.display_info("Local config file already exists. Updating...")
91+
local_config = app.config_file.overrides_model.raw_data
92+
config = deep_merge_with_list_handling(local_config, local_repo_config)
93+
else:
94+
config = local_repo_config
95+
96+
app.config_file.overrides_model = RootConfig(config)
97+
app.config_file.update()
98+
99+
app.display_success(f"Local repo configuration added in {app.config_file.pretty_overrides_path}\n")
100+
app.display("Local config content:")
101+
scrub_config(app.config_file.overrides_model.raw_data)
102+
app.output(
103+
Syntax(dumps_toml_data(app.config_file.overrides_model.raw_data).rstrip(), "toml", background_color="default")
104+
)

ddev/src/ddev/config/file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def path(self, value: Path):
281281

282282
@property
283283
def model(self) -> RootConfig:
284-
return cast(RootConfig, self.global_model)
284+
return self.global_model
285285

286286
def save(self, content=None):
287287
import tomli_w
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# (C) Datadog, Inc. 2024-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from collections.abc import Generator
5+
6+
import pytest
7+
8+
from ddev.config.file import DDEV_TOML, ConfigFileWithOverrides
9+
from ddev.utils.fs import Path
10+
from ddev.utils.toml import dump_toml_data, load_toml_file
11+
from tests.helpers.git import ClonedRepo
12+
from tests.helpers.runner import CliRunner, Result
13+
14+
15+
@pytest.fixture
16+
def repo_with_ddev_tool_config(repository_as_cwd: ClonedRepo) -> Generator[ClonedRepo, None, None]:
17+
pyproject_path = repository_as_cwd.path / "pyproject.toml"
18+
pyproject = load_toml_file(pyproject_path)
19+
pyproject["tool"]["ddev"] = {"repo": "integrations-core"}
20+
dump_toml_data(pyproject, pyproject_path)
21+
22+
yield repository_as_cwd
23+
24+
25+
def test_create_new_overrides_config(
26+
ddev: CliRunner, config_file: ConfigFileWithOverrides, helpers, repo_with_ddev_tool_config: ClonedRepo
27+
):
28+
temp_dir = repo_with_ddev_tool_config.path
29+
30+
result = ddev("config", "override")
31+
local_path = str(temp_dir).replace("\\", "\\\\")
32+
33+
expected_output = helpers.dedent(
34+
f"""
35+
Local repo configuration added in {config_file.pretty_overrides_path}
36+
37+
Local config content:
38+
repo = "core"
39+
40+
[repos]
41+
core = "{local_path}"
42+
"""
43+
)
44+
# Reload new values
45+
config_file.load()
46+
47+
assert result.exit_code == 0, result.output
48+
assert result.output == expected_output
49+
50+
# Verify the config was actually created
51+
assert config_file.overrides_path.exists()
52+
assert config_file.overrides_model.raw_data["repos"]["core"] == str(config_file.overrides_path.parent)
53+
assert config_file.overrides_model.raw_data["repo"] == "core"
54+
55+
56+
def test_update_existing_local_config(
57+
ddev: CliRunner, config_file: ConfigFileWithOverrides, helpers, repo_with_ddev_tool_config: ClonedRepo
58+
):
59+
ddev_path = repo_with_ddev_tool_config.path / DDEV_TOML
60+
existing_config = helpers.dedent(
61+
"""
62+
[orgs.default]
63+
api_key = "test_key"
64+
65+
[repos]
66+
core = "/old/path"
67+
"""
68+
)
69+
ddev_path.write_text(existing_config)
70+
71+
result = ddev("config", "override")
72+
local_path = str(ddev_path.parent).replace("\\", "\\\\")
73+
74+
assert result.exit_code == 0, result.output
75+
assert result.output == helpers.dedent(
76+
f"""
77+
Local config file already exists. Updating...
78+
Local repo configuration added in {config_file.pretty_overrides_path}
79+
80+
Local config content:
81+
repo = "core"
82+
83+
[orgs.default]
84+
api_key = "*****"
85+
86+
[repos]
87+
core = "{local_path}"
88+
"""
89+
)
90+
91+
# Verify the config was updated correctly
92+
config_file.load()
93+
assert config_file.overrides_model.raw_data["repos"]["core"] == str(repo_with_ddev_tool_config.path)
94+
assert config_file.overrides_model.raw_data["repo"] == "core"
95+
assert config_file.overrides_model.raw_data["orgs"]["default"]["api_key"] == "test_key"
96+
97+
98+
def assert_valid_local_config(
99+
config_file: ConfigFileWithOverrides, repo_path: Path, result: Result, expected_output: str
100+
):
101+
assert result.exit_code == 0
102+
assert "The current repo could not be inferred" in result.output
103+
assert "What repo are you trying to override?" in result.output
104+
assert expected_output in result.output
105+
assert config_file.overrides_model.raw_data["repos"]["extras"] == str(repo_path)
106+
assert config_file.overrides_model.raw_data["repo"] == "extras"
107+
108+
109+
def test_not_in_repo_ask_user(ddev: CliRunner, config_file: ConfigFileWithOverrides, helpers, overrides_config: Path):
110+
result = ddev("config", "override", input="extras")
111+
extras_path = str(config_file.overrides_path.parent).replace("\\", "\\\\")
112+
113+
expected_output = helpers.dedent(
114+
f"""
115+
Local config file already exists. Updating...
116+
Local repo configuration added in {config_file.pretty_overrides_path}
117+
118+
Local config content:
119+
repo = "extras"
120+
121+
[repos]
122+
extras = "{extras_path}"
123+
"""
124+
)
125+
# Reload new values
126+
config_file.load()
127+
assert_valid_local_config(config_file, overrides_config.parent, result, expected_output)
128+
129+
130+
def test_pyproject_not_found_ask_user(
131+
ddev: CliRunner, config_file: ConfigFileWithOverrides, helpers, repository_as_cwd: ClonedRepo
132+
):
133+
(repository_as_cwd.path / "pyproject.toml").unlink()
134+
result = ddev("config", "override", input="extras")
135+
extras_path = str(config_file.overrides_path.parent).replace("\\", "\\\\")
136+
137+
expected_output = helpers.dedent(
138+
f"""
139+
Local repo configuration added in {config_file.pretty_overrides_path}
140+
141+
Local config content:
142+
repo = "extras"
143+
144+
[repos]
145+
extras = "{extras_path}"
146+
"""
147+
)
148+
# Reload new values
149+
config_file.load()
150+
assert_valid_local_config(config_file, config_file.overrides_path.parent, result, expected_output)
151+
152+
153+
def test_misconfigured_pyproject_fails(
154+
ddev: CliRunner, config_file: ConfigFileWithOverrides, helpers, repository_as_cwd: ClonedRepo
155+
):
156+
# Setup wrongly configured pyproject.toml
157+
pyproject_path = repository_as_cwd.path / "pyproject.toml"
158+
pyproject = load_toml_file(pyproject_path)
159+
pyproject["tool"]["ddev"] = {"repo": "wrong-repo"}
160+
dump_toml_data(pyproject, pyproject_path)
161+
162+
result = ddev("config", "override")
163+
assert result.exit_code == 1
164+
assert "Invalid ddev metadata found in pyproject.toml" in result.output
165+
assert "[tool.ddev.repo] is 'wrong-repo': Input should be 'integrations-core'" in result.output

ddev/tests/test__utils.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,15 @@
66

77

88
def test_cloned_repo(repository, local_repo):
9-
integrations = sorted(
9+
integrations = {
1010
entry.name for entry in repository.path.iterdir() if (repository.path / entry.name / 'manifest.json').is_file()
11-
)
12-
expected_integrations = sorted(
11+
}
12+
expected_integrations = {
1313
entry.name for entry in local_repo.iterdir() if (local_repo / entry.name / 'manifest.json').is_file()
14-
)
14+
}
1515

1616
# Note: We are checking that the number of integrations is +- 1 from the `master`
1717
# branch as a workaround for scenarios where the current branch adds/removes
1818
# an integration and there has a different integration count than master.
19-
if len(integrations) != len(expected_integrations):
20-
assert abs(len(integrations) - len(expected_integrations)) == 1
21-
else:
19+
if abs(len(integrations) - len(expected_integrations)) > 1:
2220
assert integrations == expected_integrations

0 commit comments

Comments
 (0)