Skip to content

Commit 5da4cc3

Browse files
committed
refactor: simplify env prefix convention by removing double-underscore requirement and legacy reservation rule
Signed-off-by: tercel <tercel.yi@gmail.com>
1 parent 5f0a14d commit 5da4cc3

4 files changed

Lines changed: 27 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.15.1] - 2026-03-31
9+
10+
### Changed
11+
12+
- **Env prefix convention simplified** — Removed the `^APCORE_[A-Z0-9]` reservation rule from `Config._validate_env_prefix()`. Sub-packages now use single-underscore prefixes (`APCORE_MCP`, `APCORE_OBSERVABILITY`, `APCORE_SYS`) instead of the double-underscore form. Only the exact `APCORE` prefix is reserved for the core namespace.
13+
- Built-in namespace env prefixes: `APCORE__OBSERVABILITY``APCORE_OBSERVABILITY`, `APCORE__SYS``APCORE_SYS`.
14+
15+
---
16+
817
## [0.15.0] - 2026-03-30
918

1019
### Added
@@ -17,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1726
- **`config.mount(namespace, from_file=...|from_dict=...)`** — Attach external config sources to a namespace without a unified YAML file. Primary integration path for third-party packages with existing config systems.
1827
- **`Config.registered_namespaces()`** — Class-level introspection; returns names of all registered namespaces.
1928
- **Unified YAML with namespace partitioning** — Single YAML file with namespace-keyed top-level sections. Automatic mode detection: legacy mode (no `apcore:` key, fully backward compatible) vs. namespace mode (`apcore:` key present). `_config` is a reserved meta-namespace (`strict`, `allow_unknown`).
20-
- **Per-namespace env override with longest-prefix-match dispatch** — Each namespace declares its own `env_prefix`. `APCORE__` double-underscore convention for apcore sub-packages (e.g., `APCORE__OBSERVABILITY`, `APCORE__SYS`) to avoid collision with the existing single-underscore `APCORE_` prefix used for flat keys.
29+
- **Per-namespace env override with longest-prefix-match dispatch** — Each namespace declares its own `env_prefix`. Apcore sub-packages use `APCORE_` prefixed names (e.g., `APCORE_OBSERVABILITY`, `APCORE_SYS`); the longest-prefix-match dispatch algorithm resolves any ambiguity with the core `APCORE` prefix.
2130
- **Hot-reload namespace support**`config.reload()` re-reads YAML, re-detects mode, re-applies namespace defaults and env overrides, re-validates, and re-reads mounted files.
2231
- **New error codes**`CONFIG_NAMESPACE_DUPLICATE`, `CONFIG_NAMESPACE_RESERVED`, `CONFIG_ENV_PREFIX_CONFLICT`, `CONFIG_MOUNT_ERROR`, `CONFIG_BIND_ERROR`
2332

@@ -30,8 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3039
- **New error code**`ERROR_FORMATTER_DUPLICATE`
3140

3241
#### Built-in Namespace Registrations (§9.15)
33-
- **`observability` namespace** (`APCORE__OBSERVABILITY` env prefix) — apcore pre-registers this namespace, promoting the existing `apcore.observability.*` flat config keys (tracing, metrics, logging, error_history, platform_notify) into a named subtree. Adapter packages (apcore-mcp, apcore-a2a, apcore-cli) should read from this namespace rather than independent logging defaults.
34-
- **`sys_modules` namespace** (`APCORE__SYS` env prefix) — apcore pre-registers this namespace, promoting the existing `apcore.sys_modules.*` flat keys into a named subtree. `register_sys_modules()` prefers `config.namespace("sys_modules")` in namespace mode with `config.get("sys_modules.*")` legacy fallback. Both registrations are 1:1 migrations of existing keys; there are no breaking changes.
42+
- **`observability` namespace** (`APCORE_OBSERVABILITY` env prefix) — apcore pre-registers this namespace, promoting the existing `apcore.observability.*` flat config keys (tracing, metrics, logging, error_history, platform_notify) into a named subtree. Adapter packages (apcore-mcp, apcore-a2a, apcore-cli) should read from this namespace rather than independent logging defaults.
43+
- **`sys_modules` namespace** (`APCORE_SYS` env prefix) — apcore pre-registers this namespace, promoting the existing `apcore.sys_modules.*` flat keys into a named subtree. `register_sys_modules()` prefers `config.namespace("sys_modules")` in namespace mode with `config.get("sys_modules.*")` legacy fallback. Both registrations are 1:1 migrations of existing keys; there are no breaking changes.
3544

3645
#### Event Type Naming Convention and Collision Fix (§9.16)
3746
- **Canonical event names** — Two confirmed event type collisions in apcore-python are resolved:

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,18 @@ apcore pre-registers two namespaces that promote its existing flat config keys:
142142

143143
| Namespace | Env prefix | Keys |
144144
|-----------|-----------|------|
145-
| `observability` | `APCORE__OBSERVABILITY` | tracing, metrics, logging, error_history, platform_notify |
146-
| `sys_modules` | `APCORE__SYS` | thresholds.error_rate, thresholds.latency_p99_ms |
145+
| `observability` | `APCORE_OBSERVABILITY` | tracing, metrics, logging, error_history, platform_notify |
146+
| `sys_modules` | `APCORE_SYS` | thresholds.error_rate, thresholds.latency_p99_ms |
147147

148148
### Environment Variable Conventions
149149

150150
| Pattern | When to use | Example |
151151
|---------|------------|---------|
152152
| `APCORE_KEY_NAME` | Override a flat top-level apcore key (existing convention) | `APCORE_EXECUTOR_DEFAULT__TIMEOUT=5000` |
153-
| `APCORE__NAMESPACE` prefix | Override keys inside a registered namespace (new convention) | `APCORE__OBSERVABILITY_TRACING_ENABLED=true` |
153+
| `APCORE_NAMESPACE` prefix | Override keys inside a registered namespace | `APCORE_OBSERVABILITY_TRACING_ENABLED=true` |
154154
| Custom prefix declared in `register_namespace` | Third-party packages with their own prefix | `MY_PLUGIN__TIMEOUT_MS=3000` |
155155

156-
The double-underscore separator (`__`) in `APCORE__` avoids collisions with the existing single-underscore `APCORE_` flat-key prefix. Within each namespace, a single `_` maps to `.` and `__` maps to a literal `_`.
156+
The longest-prefix-match dispatch algorithm ensures that `APCORE_OBSERVABILITY_TRACING_ENABLED` routes to the `observability` namespace (not to a core flat key). Within each namespace, a single `_` maps to `.` and `__` maps to a literal `_`.
157157

158158
### New Error Codes (0.15.0)
159159

src/apcore/config.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import dataclasses
1111
import logging
1212
import os
13-
import re
13+
1414
import sys
1515
import threading
1616
from pathlib import Path
@@ -154,9 +154,6 @@
154154

155155
_RESERVED_NAMESPACES: frozenset[str] = frozenset({"apcore", "_config"})
156156

157-
#: Pattern that matches the APCORE_ legacy prefix — env_prefix must not match.
158-
_APCORE_RESERVED_ENV_PATTERN: re.Pattern[str] = re.compile(r"^APCORE_[A-Z0-9]")
159-
160157

161158
@dataclasses.dataclass
162159
class _NamespaceRegistration:
@@ -427,14 +424,14 @@ def register_namespace(
427424
Args:
428425
name: Namespace name (must not be reserved or already registered).
429426
schema: Optional JSON Schema dict or path to a JSON Schema file.
430-
env_prefix: Optional env var prefix (e.g. ``"APCORE__OBSERVABILITY"``).
427+
env_prefix: Optional env var prefix (e.g. ``"APCORE_OBSERVABILITY"``).
431428
defaults: Optional default values for this namespace.
432429
433430
Raises:
434431
ConfigNamespaceReservedError: If ``name`` is a reserved namespace.
435432
ConfigNamespaceDuplicateError: If ``name`` is already registered.
436433
ConfigEnvPrefixConflictError: If ``env_prefix`` conflicts with an
437-
existing prefix or matches the legacy ``APCORE_[A-Z0-9]`` pattern.
434+
existing prefix.
438435
"""
439436
if name in _RESERVED_NAMESPACES:
440437
raise ConfigNamespaceReservedError(name=name)
@@ -455,9 +452,7 @@ def register_namespace(
455452

456453
@classmethod
457454
def _validate_env_prefix(cls, env_prefix: str) -> None:
458-
"""Raise ConfigEnvPrefixConflictError if env_prefix is already in use or reserved."""
459-
if _APCORE_RESERVED_ENV_PATTERN.match(env_prefix):
460-
raise ConfigEnvPrefixConflictError(env_prefix=env_prefix)
455+
"""Raise ConfigEnvPrefixConflictError if env_prefix is already in use."""
461456
for reg in _GLOBAL_NS_REGISTRY.values():
462457
if reg.env_prefix and reg.env_prefix == env_prefix:
463458
raise ConfigEnvPrefixConflictError(env_prefix=env_prefix)
@@ -928,7 +923,7 @@ def _instantiate_model(model_class: type, data: dict[str, Any], namespace: str)
928923

929924
Config.register_namespace(
930925
"observability",
931-
env_prefix="APCORE__OBSERVABILITY",
926+
env_prefix="APCORE_OBSERVABILITY",
932927
defaults={
933928
"tracing": {
934929
"enabled": False,
@@ -958,7 +953,7 @@ def _instantiate_model(model_class: type, data: dict[str, Any], namespace: str)
958953

959954
Config.register_namespace(
960955
"sys_modules",
961-
env_prefix="APCORE__SYS",
956+
env_prefix="APCORE_SYS",
962957
defaults={
963958
"enabled": True,
964959
"health": {"enabled": True},

tests/test_config_bus.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,9 @@ def test_register_reserved_config_raises(self) -> None:
8787
with pytest.raises(ConfigNamespaceReservedError):
8888
Config.register_namespace("_config")
8989

90-
def test_register_env_prefix_conflict_legacy_pattern(self) -> None:
91-
# Must not match ^APCORE_[A-Z0-9]
92-
with pytest.raises(ConfigEnvPrefixConflictError):
93-
Config.register_namespace("envns", env_prefix="APCORE_FOO")
94-
95-
def test_register_env_prefix_double_underscore_ok(self) -> None:
96-
# APCORE__SOMETHING is fine (double underscore)
97-
Config.register_namespace("envns2", env_prefix="APCORE__ENVNS2")
90+
def test_register_env_prefix_apcore_subpackage_ok(self) -> None:
91+
# APCORE_SOMETHING is fine — longest-prefix-match disambiguates.
92+
Config.register_namespace("envns2", env_prefix="APCORE_ENVNS2")
9893
names = [r["name"] for r in Config.registered_namespaces()]
9994
assert "envns2" in names
10095

@@ -130,12 +125,12 @@ def test_sys_modules_registered(self) -> None:
130125
def test_observability_has_correct_env_prefix(self) -> None:
131126
namespaces = Config.registered_namespaces()
132127
ns = next(r for r in namespaces if r["name"] == "observability")
133-
assert ns["env_prefix"] == "APCORE__OBSERVABILITY"
128+
assert ns["env_prefix"] == "APCORE_OBSERVABILITY"
134129

135130
def test_sys_modules_has_correct_env_prefix(self) -> None:
136131
namespaces = Config.registered_namespaces()
137132
ns = next(r for r in namespaces if r["name"] == "sys_modules")
138-
assert ns["env_prefix"] == "APCORE__SYS"
133+
assert ns["env_prefix"] == "APCORE_SYS"
139134

140135

141136
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)