Skip to content

Commit 90f56be

Browse files
authored
feat: use pydantic settings object to manage settings (#41)
1 parent a5152e8 commit 90f56be

File tree

6 files changed

+166
-42
lines changed

6 files changed

+166
-42
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ from conda_forge_feedstock_ops.rerender import rerender
2121
commit_msg = rerender(path_to_feedstock)
2222
```
2323

24+
## Settings
25+
26+
You can customize the behavior of the package by setting environment variables as described in [settings.py](conda_forge_feedstock_ops/settings.py).
27+
2428
## Container Setup
2529

2630
This package works by running commands inside of a container on-the-fly in order to

conda_forge_feedstock_ops/container_utils.py

+8-42
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,15 @@
66
from collections.abc import Iterable
77
from typing import Callable, Optional
88

9-
from ._version import __version__
9+
from conda_forge_feedstock_ops.settings import FeedstockOpsSettings
1010

1111
logger = logging.getLogger(__name__)
1212

1313
DEFAULT_CONTAINER_TMPFS_SIZE_MB = 6000
1414

15-
CONTAINER_PROXY_MODE = os.environ.get(
16-
"CF_FEEDSTOCK_OPS_CONTAINER_PROXY_MODE", "false"
17-
).lower() in ("yes", "true", "t", "1")
18-
"""
19-
Whether to use a proxy that is locally configured for all requests inside the container.
20-
Set the environment variable `CF_FEEDSTOCK_OPS_CONTAINER_PROXY_MODE` to 'true' to enable this feature.
21-
"""
22-
23-
PROXY_IN_CONTAINER = os.environ.get(
24-
"CF_FEEDSTOCK_OPS_PROXY_IN_CONTAINER", "http://host.docker.internal:8080"
25-
)
26-
"""
27-
The hostname of the proxy to use in the container.
28-
The default value of 'http://host.docker.internal:8080' is the default value for Docker Desktop on Windows and macOS.
29-
It also works for OrbStack.
30-
31-
For podman, use http://host.containers.internal:8080.
32-
For GitHub Actions, use http://172.17.0.1:8080, see https://stackoverflow.com/a/65505308
33-
"""
34-
3515

3616
def get_default_container_name():
37-
"""Get the default container name for feedstock ops.
38-
39-
The image is stored at `condaforge/conda-forge-feedstock-ops`.
40-
41-
If the environment variable `CF_FEEDSTOCK_OPS_CONTAINER_NAME` is set, then that name is used.
42-
43-
If the environment variable `CF_FEEDSTOCK_OPS_CONTAINER_TAG` is set, then that tag is pulled.
44-
Otherwise, we pull the tag `__version__`.
45-
"""
46-
cname = (
47-
f"{os.environ.get('CF_FEEDSTOCK_OPS_CONTAINER_NAME', 'condaforge/conda-forge-feedstock-ops')}"
48-
+ f":{os.environ.get('CF_FEEDSTOCK_OPS_CONTAINER_TAG', __version__)}"
49-
)
50-
51-
return cname
17+
return FeedstockOpsSettings().container_full_name
5218

5319

5420
class ContainerRuntimeError(RuntimeError):
@@ -118,14 +84,16 @@ def get_default_log_level_args(logger):
11884

11985

12086
def _get_proxy_mode_container_args():
121-
if not CONTAINER_PROXY_MODE:
87+
settings = FeedstockOpsSettings()
88+
if not settings.container_proxy_mode:
12289
return []
90+
12391
assert os.environ["SSL_CERT_FILE"] == os.environ["REQUESTS_CA_BUNDLE"]
12492
return [
12593
"-e",
126-
f"http_proxy={PROXY_IN_CONTAINER}",
94+
f"http_proxy={settings.proxy_in_container}",
12795
"-e",
128-
f"https_proxy={PROXY_IN_CONTAINER}",
96+
f"https_proxy={settings.proxy_in_container}",
12997
"-e",
13098
f"no_proxy={os.environ.get('no_proxy', '')}",
13199
"-e",
@@ -290,9 +258,7 @@ def should_use_container(use_container: Optional[bool] = None):
290258
bool
291259
Whether to use a container.
292260
"""
293-
in_container = (
294-
os.environ.get("CF_FEEDSTOCK_OPS_IN_CONTAINER", "false").lower() == "true"
295-
)
261+
in_container = FeedstockOpsSettings().in_container
296262
if use_container is None:
297263
use_container = not in_container
298264

conda_forge_feedstock_ops/settings.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import sys
2+
from typing import Annotated, Self
3+
4+
from pydantic import AfterValidator, AnyHttpUrl, Field, model_validator
5+
from pydantic_settings import BaseSettings, SettingsConfigDict
6+
7+
from ._version import __version__
8+
9+
# https://github.com/pydantic/pydantic/issues/7186#issuecomment-1691594032
10+
HttpProxyUrl = Annotated[AnyHttpUrl, AfterValidator(lambda x: str(x).rstrip("/"))]
11+
12+
13+
def get_docker_host_hostname() -> str:
14+
"""
15+
Get the default value for the `proxy_in_container` setting.
16+
https://stackoverflow.com/a/65505308
17+
"""
18+
if sys.platform == "linux":
19+
return "http://172.17.0.1:8080"
20+
return "http://host.docker.internal:8080"
21+
22+
23+
class FeedstockOpsSettings(BaseSettings):
24+
"""
25+
The global settings object read from environment variables.
26+
27+
To change a setting, set the environment variable `CF_FEEDSTOCK_OPS_<SETTING_NAME>`.
28+
Keys are case-insensitive. For example, to set the `container_name` setting, set the
29+
environment variable `CF_FEEDSTOCK_OPS_CONTAINER_NAME`.
30+
31+
Developer note: Please note that consumers of this library might want to change some settings in between function
32+
invocations. Therefore, don't store the settings object in a global variable.
33+
"""
34+
35+
model_config = SettingsConfigDict(env_prefix="CF_FEEDSTOCK_OPS_")
36+
37+
container_name: str = "condaforge/conda-forge-feedstock-ops"
38+
"""
39+
The Docker image name to use for the container.
40+
"""
41+
42+
container_tag: str = __version__
43+
"""
44+
The Docker image tag to use for the container. Defaults to the current version of this package.
45+
"""
46+
47+
in_container: bool = False
48+
"""
49+
Whether the code is already running inside a container.
50+
"""
51+
52+
@property
53+
def container_full_name(self) -> str:
54+
"""
55+
The full name of the Docker image to use for the container in the format `container_name:container_tag`.
56+
"""
57+
return f"{self.container_name}:{self.container_tag}"
58+
59+
container_proxy_mode: bool = False
60+
"""
61+
Whether to use a proxy that is locally configured for all requests inside the container.
62+
"""
63+
64+
proxy_in_container: HttpProxyUrl = Field(default_factory=get_docker_host_hostname)
65+
"""
66+
The hostname of the proxy to use in the container.
67+
The default value should reference the Docker host's hostname and works for
68+
- Docker Desktop on Windows and macOS
69+
- OrbStack
70+
71+
For podman, set this manually to http://host.containers.internal:8080.
72+
"""
73+
74+
@model_validator(mode="after")
75+
def check_forgotten_container_proxy_mode(self) -> Self:
76+
if (
77+
"proxy_in_container" in self.model_fields_set
78+
and not self.container_proxy_mode
79+
):
80+
raise ValueError(
81+
"The `proxy_in_container` setting requires `container_proxy_mode` to be enabled."
82+
)
83+
return self

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies:
99
- click
1010
- conda-build >=3.27
1111
- conda-smithy >=3.40
12+
- pydantic-settings >=2.8.1,<3.0.0
1213
- python-rapidjson
1314
- pyyaml
1415
- rattler-build-conda-compat >=0.0.2,<2.0.0a0

tests/conftest.py

+7
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,10 @@ def use_containers():
9999
os.environ.pop("CF_FEEDSTOCK_OPS_IN_CONTAINER", None)
100100
else:
101101
os.environ["CF_FEEDSTOCK_OPS_IN_CONTAINER"] = old_in_container
102+
103+
104+
@pytest.fixture
105+
def temporary_env_variables():
106+
old_env = os.environ.copy()
107+
yield
108+
os.environ = old_env

tests/test_settings.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import os
2+
from unittest import mock
3+
4+
import pytest
5+
from pydantic import ValidationError
6+
7+
from conda_forge_feedstock_ops._version import __version__
8+
from conda_forge_feedstock_ops.settings import FeedstockOpsSettings
9+
10+
11+
class TestFeedstockOpsSettings:
12+
def test_example_parsing(self, temporary_env_variables):
13+
os.environ["CF_FEEDSTOCK_OPS_CONTAINER_NAME"] = "test-container"
14+
os.environ["CF_FEEDSTOCK_OPS_CONTAINER_TAG"] = "test-tag"
15+
os.environ["CF_FEEDSTOCK_OPS_IN_CONTAINER"] = "true"
16+
os.environ["CF_FEEDSTOCK_OPS_CONTAINER_PROXY_MODE"] = "true"
17+
os.environ["CF_FEEDSTOCK_OPS_PROXY_IN_CONTAINER"] = "http://proxy:8000"
18+
19+
settings = FeedstockOpsSettings()
20+
21+
assert settings.container_name == "test-container"
22+
assert settings.container_tag == "test-tag"
23+
assert settings.in_container is True
24+
assert settings.container_proxy_mode is True
25+
assert str(settings.proxy_in_container) == "http://proxy:8000"
26+
27+
assert settings.container_full_name == "test-container:test-tag"
28+
29+
@pytest.mark.parametrize(
30+
"platform, expected_proxy",
31+
[
32+
["linux", "http://172.17.0.1:8080"],
33+
["darwin", "http://host.docker.internal:8080"],
34+
["win32", "http://host.docker.internal:8080"],
35+
],
36+
)
37+
def test_default_values(
38+
self, platform: str, expected_proxy: str, temporary_env_variables
39+
):
40+
os.environ.clear()
41+
42+
with mock.patch("conda_forge_feedstock_ops.settings.sys.platform", platform):
43+
settings = FeedstockOpsSettings()
44+
45+
assert settings.container_name == "condaforge/conda-forge-feedstock-ops"
46+
assert settings.container_tag == __version__
47+
assert settings.in_container is False
48+
assert settings.container_proxy_mode is False
49+
assert str(settings.proxy_in_container) == expected_proxy
50+
assert (
51+
settings.container_full_name
52+
== f"{settings.container_name}:{settings.container_tag}"
53+
)
54+
55+
def test_check_forgotten_container_proxy_mode(self, temporary_env_variables):
56+
os.environ.clear()
57+
58+
os.environ["CF_FEEDSTOCK_OPS_PROXY_IN_CONTAINER"] = "http://proxy:8000"
59+
60+
with pytest.raises(
61+
ValidationError, match="requires `container_proxy_mode` to be enabled"
62+
):
63+
FeedstockOpsSettings()

0 commit comments

Comments
 (0)