Skip to content

Commit 6c4b348

Browse files
authored
Replace raw source strings with typed SourceInfo dataclasses (#652)
* Replace raw source strings with typed SourceInfo dataclasses
1 parent 3cbe664 commit 6c4b348

12 files changed

Lines changed: 198 additions & 72 deletions

File tree

codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/ConfigGenerator.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,9 @@ private void generateConfig(GenerationContext context, PythonWriter writer) {
343343

344344
// Only add config resolution imports if there are descriptor properties
345345
if (hasDescriptors) {
346-
writer.addDependency(SmithyPythonDependency.SMITHY_CORE);
347-
writer.addImport("smithy_core.config.property", "ConfigProperty");
348-
writer.addImport("smithy_core.config.resolver", "ConfigResolver");
346+
writer.addDependency(SmithyPythonDependency.SMITHY_AWS_CORE);
347+
writer.addImport("smithy_aws_core.config.property", "ConfigProperty");
348+
writer.addImport("smithy_aws_core.config.resolver", "ConfigResolver");
349349
writer.addImport("smithy_aws_core.config.sources", "EnvironmentSource");
350350

351351
// Add validator and resolver imports for properties that use descriptors
@@ -580,17 +580,19 @@ public void write(PythonWriter writer, String previousText, ConfigSection sectio
580580
}
581581

582582
writer.write(previousText);
583+
writer.addImport("smithy_aws_core.config.source_info", "SourceInfo");
583584

584585
writer.write("""
585586
586-
def get_source(self, key: str) -> str | None:
587+
def get_source(self, key: str) -> SourceInfo | None:
587588
\"""Get the source that provided a configuration value.
588589
589590
Args:
590591
key: The configuration key (e.g., 'region', 'retry_strategy')
591592
592593
Returns:
593-
The source name ('instance', 'environment', etc.),
594+
The source info (SimpleSource('source_name') or
595+
ComplexSource({"retry_mode": "source1", "max_attempts": "source2"})),
594596
or None if the key hasn't been resolved yet.
595597
\"""
596598
cached = self.__dict__.get(f'_cache_{key}')

packages/smithy-aws-core/src/smithy_aws_core/config/custom_resolvers.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
from smithy_core.config.resolver import ConfigResolver
54
from smithy_core.retries import RetryStrategyOptions
65

6+
from smithy_aws_core.config.resolver import ConfigResolver
7+
from smithy_aws_core.config.source_info import ComplexSource, SourceName
78
from smithy_aws_core.config.validators import validate_max_attempts, validate_retry_mode
89

910

1011
def resolve_retry_strategy(
1112
resolver: ConfigResolver,
12-
) -> tuple[RetryStrategyOptions | None, str | None]:
13+
) -> tuple[RetryStrategyOptions | None, ComplexSource | None]:
1314
"""Resolve retry strategy from multiple config keys.
1415
1516
Resolves both retry_mode and max_attempts from sources and constructs
@@ -23,7 +24,7 @@ def resolve_retry_strategy(
2324
are resolved. Returns (None, None) if both values are missing.
2425
2526
For mixed sources, the source name includes both component sources:
26-
"retry_mode=environment, max_attempts=config_file"
27+
{"retry_mode": "environment", "max_attempts": "default"}
2728
"""
2829

2930
retry_mode, mode_source = resolver.get("retry_mode")
@@ -45,6 +46,13 @@ def resolve_retry_strategy(
4546
)
4647

4748
# Construct mixed source string showing where each component came from
48-
source = f"retry_mode={mode_source or 'default'}, max_attempts={attempts_source or 'default'}"
49+
source = ComplexSource(
50+
{
51+
"retry_mode": mode_source.name if mode_source else SourceName.DEFAULT,
52+
"max_attempts": attempts_source.name
53+
if attempts_source
54+
else SourceName.DEFAULT,
55+
}
56+
)
4957

5058
return (options, source)

packages/smithy-core/src/smithy_core/config/property.py renamed to packages/smithy-aws-core/src/smithy_aws_core/config/property.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from collections.abc import Callable
44
from typing import Any
55

6-
from smithy_core.config.resolver import ConfigResolver
6+
from smithy_aws_core.config.resolver import ConfigResolver
7+
from smithy_aws_core.config.source_info import SimpleSource, SourceInfo, SourceName
78

89

910
class ConfigProperty:
@@ -27,8 +28,9 @@ def __init__(self):
2728
def __init__(
2829
self,
2930
key: str,
30-
validator: Callable[[Any, str | None], Any] | None = None,
31-
resolver_func: Callable[[ConfigResolver], tuple[Any, str | None]] | None = None,
31+
validator: Callable[[Any, SourceInfo | None], Any] | None = None,
32+
resolver_func: Callable[[ConfigResolver], tuple[Any, SourceInfo | None]]
33+
| None = None,
3234
default_value: Any = None,
3335
):
3436
"""Initialize config property descriptor.
@@ -78,7 +80,7 @@ def __get__(self, obj: Any, objtype: type | None = None) -> Any:
7880

7981
if value is None:
8082
value = self.default_value
81-
source = "default"
83+
source = SimpleSource(SourceName.DEFAULT)
8284

8385
if self.validator:
8486
value = self.validator(value, source)
@@ -99,7 +101,11 @@ def __set__(self, obj: Any, value: Any) -> None:
99101
# Determine source based on when the value was set
100102
# If cache already exists, it means it was not set during initialization
101103
# In that case source will be set to in-code
102-
source = "in-code" if hasattr(obj, self.cache_attr) else "instance"
104+
source = (
105+
SimpleSource(SourceName.IN_CODE)
106+
if hasattr(obj, self.cache_attr)
107+
else SimpleSource(SourceName.INSTANCE)
108+
)
103109
if self.validator:
104110
value = self.validator(value, source)
105111

packages/smithy-core/src/smithy_core/config/resolver.py renamed to packages/smithy-aws-core/src/smithy_aws_core/config/resolver.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from smithy_core.interfaces.config import ConfigSource
77

8+
from smithy_aws_core.config.source_info import SimpleSource
9+
810

911
class ConfigResolver:
1012
"""Resolves configuration values from multiple sources.
@@ -21,7 +23,7 @@ def __init__(self, sources: Sequence[ConfigSource]) -> None:
2123
"""
2224
self._sources = sources
2325

24-
def get(self, key: str) -> tuple[Any, str | None]:
26+
def get(self, key: str) -> tuple[Any, SimpleSource | None]:
2527
"""Resolve a configuration value from sources by iterating through them in precedence order.
2628
2729
:param key: The configuration key to resolve (e.g., 'retry_mode')
@@ -32,5 +34,5 @@ def get(self, key: str) -> tuple[Any, str | None]:
3234
for source in self._sources:
3335
value = source.get(key)
3436
if value is not None:
35-
return (value, source.name)
37+
return (value, SimpleSource(source.name))
3638
return (None, None)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from dataclasses import dataclass
5+
from enum import StrEnum
6+
7+
8+
class SourceName(StrEnum):
9+
"""Known source names for config value provenance tracking."""
10+
11+
INSTANCE = "instance" # value provided via Config constructor
12+
13+
IN_CODE = "in-code" # value set via setter after Config construction
14+
15+
ENVIRONMENT = "environment" # value resolved from environment variable
16+
17+
DEFAULT = "default" # value fall back to default
18+
19+
20+
@dataclass(frozen=True)
21+
class SimpleSource:
22+
"""Source info for a config value resolved from a single source.
23+
24+
Examples: region from environment, max_attempts from config file.
25+
"""
26+
27+
# TODO: Currently only environment variable is implemented as a config
28+
# source. Tests use raw strings (e.g., "environment", "config_file") as
29+
# source names to simulate multi-source scenarios. Once additional
30+
# config sources are implemented, update the `name` parameter type
31+
# from `str` to `SourceName` and replace raw strings in tests with
32+
# the corresponding enum values.
33+
name: str
34+
35+
36+
@dataclass(frozen=True)
37+
class ComplexSource:
38+
"""Source info for a config value resolved from multiple sources.
39+
40+
Used when a config property is composed of multiple sources.
41+
Example: retry_strategy is composed of retry_mode and max_attempts and they both
42+
could be from different sources: {"retry_mode": "environment", "max_attempts": "config_file"}
43+
"""
44+
45+
components: dict[str, str]
46+
47+
48+
SourceInfo = SimpleSource | ComplexSource

packages/smithy-aws-core/src/smithy_aws_core/config/sources.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# SPDX-License-Identifier: Apache-2.0
33
import os
44

5+
from smithy_aws_core.config.source_info import SourceName
6+
57

68
class EnvironmentSource:
79
"""Configuration from environment variables."""
@@ -16,7 +18,7 @@ def __init__(self, prefix: str = "AWS_"):
1618
@property
1719
def name(self) -> str:
1820
"""Returns the source name."""
19-
return "environment"
21+
return SourceName.ENVIRONMENT
2022

2123
def get(self, key: str) -> str | None:
2224
"""Returns a configuration value from environment variables.

packages/smithy-aws-core/src/smithy_aws_core/config/validators.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
from smithy_core.interfaces.retries import RetryStrategy
88
from smithy_core.retries import RetryStrategyOptions, RetryStrategyType
99

10+
from smithy_aws_core.config.source_info import SourceInfo
11+
1012

1113
class ConfigValidationError(ValueError):
1214
"""Raised when a configuration value fails validation."""
1315

14-
def __init__(self, key: str, value: Any, reason: str, source: str | None = None):
16+
def __init__(
17+
self, key: str, value: Any, reason: str, source: SourceInfo | None = None
18+
):
1519
self.key = key
1620
self.value = value
1721
self.reason = reason
@@ -23,7 +27,7 @@ def __init__(self, key: str, value: Any, reason: str, source: str | None = None)
2327
super().__init__(msg)
2428

2529

26-
def validate_region(region: str | None, source: str | None = None) -> str:
30+
def validate_region(region: str | None, source: SourceInfo | None = None) -> str:
2731
"""Validate region name format.
2832
2933
:param region: The value to validate
@@ -53,7 +57,7 @@ def validate_region(region: str | None, source: str | None = None) -> str:
5357
return region
5458

5559

56-
def validate_retry_mode(retry_mode: str, source: str | None = None) -> str:
60+
def validate_retry_mode(retry_mode: str, source: SourceInfo | None = None) -> str:
5761
"""Validate retry mode.
5862
5963
Valid values: 'standard'
@@ -85,7 +89,9 @@ def validate_retry_mode(retry_mode: str, source: str | None = None) -> str:
8589
return retry_mode
8690

8791

88-
def validate_max_attempts(max_attempts: str | int, source: str | None = None) -> int:
92+
def validate_max_attempts(
93+
max_attempts: str | int, source: SourceInfo | None = None
94+
) -> int:
8995
"""Validate and convert max_attempts to integer.
9096
9197
:param max_attempts: The max attempts value (string or int)
@@ -117,7 +123,7 @@ def validate_max_attempts(max_attempts: str | int, source: str | None = None) ->
117123

118124

119125
def validate_retry_strategy(
120-
value: Any, source: str | None = None
126+
value: Any, source: SourceInfo | None = None
121127
) -> RetryStrategy | RetryStrategyOptions:
122128
"""Validate retry strategy configuration.
123129

packages/smithy-aws-core/tests/unit/config/test_custom_resolver.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from typing import Any
55

66
from smithy_aws_core.config.custom_resolvers import resolve_retry_strategy
7-
from smithy_core.config.resolver import ConfigResolver
7+
from smithy_aws_core.config.resolver import ConfigResolver
8+
from smithy_aws_core.config.source_info import ComplexSource
89
from smithy_core.retries import RetryStrategyOptions
910

1011

@@ -39,7 +40,9 @@ def test_resolves_from_both_values(self) -> None:
3940
assert isinstance(result, RetryStrategyOptions)
4041
assert result.retry_mode == "standard"
4142
assert result.max_attempts == 3
42-
assert source_name == "retry_mode=environment, max_attempts=environment"
43+
assert source_name == ComplexSource(
44+
{"retry_mode": "environment", "max_attempts": "environment"}
45+
)
4346

4447
def test_tracks_different_sources_for_each_component(self) -> None:
4548
source1 = StubSource("environment", {"retry_mode": "standard"})
@@ -51,7 +54,9 @@ def test_tracks_different_sources_for_each_component(self) -> None:
5154
assert isinstance(result, RetryStrategyOptions)
5255
assert result.retry_mode == "standard"
5356
assert result.max_attempts == 5
54-
assert source_name == "retry_mode=environment, max_attempts=config_file"
57+
assert source_name == ComplexSource(
58+
{"retry_mode": "environment", "max_attempts": "config_file"}
59+
)
5560

5661
def test_converts_max_attempts_string_to_int(self) -> None:
5762
source = StubSource(
@@ -76,7 +81,9 @@ def test_returns_strategy_when_only_retry_mode_set(self) -> None:
7681
# None for max_attempts means the RetryStrategy will use its
7782
# own default max_attempts value for the set retry_mode
7883
assert result.max_attempts is None
79-
assert source_name == "retry_mode=environment, max_attempts=default"
84+
assert source_name == ComplexSource(
85+
{"retry_mode": "environment", "max_attempts": "default"}
86+
)
8087

8188
def test_returns_strategy_when_only_max_attempts_set(self) -> None:
8289
source = StubSource("environment", {"max_attempts": "5"})
@@ -87,7 +94,9 @@ def test_returns_strategy_when_only_max_attempts_set(self) -> None:
8794
assert isinstance(result, RetryStrategyOptions)
8895
assert result.max_attempts == 5
8996
assert result.retry_mode == "standard"
90-
assert source_name == "retry_mode=default, max_attempts=environment"
97+
assert source_name == ComplexSource(
98+
{"retry_mode": "default", "max_attempts": "environment"}
99+
)
91100

92101
def test_returns_none_when_both_values_missing(self) -> None:
93102
source = StubSource("environment", {})

0 commit comments

Comments
 (0)