Add support for config resolver#738
Conversation
| result = await _resolve_str(ctx, env_vars=env_vars, profile_keys=profile_keys) | ||
| if result.value is UNSET: | ||
| return Resolved(value=UNSET, source=ConfigSource.DEFAULT) # type: ignore[arg-type] | ||
| if result.value is None: |
There was a problem hiding this comment.
Nit: Is this dead code? _resolve_str may never return Resolved with a None value?
| sources_found.append(attempts_result.source) | ||
|
|
||
| source = ( | ||
| _strongest_source(*sources_found) if sources_found else ConfigSource.DEFAULT |
There was a problem hiding this comment.
Since we only have source_of("retry_strategy_options"), do we lose the ability to tell where retry_mode vs max_attempts each came from? E.g. retry_mode from env but max_attempts from profile would both just report ENV? I forgot if we discussed this yesterday - please refresh my memory if we did. I'm curious about this because it may affect another PR - which depends on provenance tracking, and needs to know if the max_attempts itself has been set by customer or not. If we've decided to keep provenance at the options level (rather than per-field) for now, then the other PR may consider a different way to determine if the customer has set the max_attempts manually.
There was a problem hiding this comment.
Yes, for composite resolvers like retry_strategy_options, we report the strongest source across the fields that compose it (example: if retry_mode comes from ENV and max_attempts from PROFILE, the source is ENV). This is something the team decided upon. If your other PR needs to know specifically whether max_attempts was user-set, we can discuss that.
# Conflicts: # packages/smithy-aws-core/src/smithy_aws_core/config/exceptions.py
34efc14 to
feb09c2
Compare
jonathan343
left a comment
There was a problem hiding this comment.
Thanks Ujjwal. Overall, everything seems functional after testing with different direct, env var, and config file setups.
I let some comments calling out some gaps and bugs in the implementation. Let me know if you have any questions. I'll continue to think about if this is the optimal interface we want to expose.
| assert result.source == ConfigSource.ENV | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_uses_defaults_when_nothing_found(self): |
There was a problem hiding this comment.
There are a few tests in this file that build SharedConfigContext(env={}) with no file_path overrides. This causes them to eventually read the real ~/.aws/config and ~/.aws/credentials files on the runners machine. If the machine's [default] profile sets retry_mode or max_attempts, the assertions will fail.
This issue affects the following tests:
test_uses_defaults_when_nothing_foundtest_partial_resolution_uses_defaults_for_missingtest_max_attempts_unset_when_not_found.
| assert config.region == "us-west-2" | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_resolves_retry_from_env(self): |
There was a problem hiding this comment.
This test is also environment dependent. It relies on the test runner's machine to define region = ... in the ~/.aws/config file. If it doesn't it will fail with the following error:
smithy_aws_core.config.exceptions.ConfigError: Invalid value for 'region': None. Region is required and must be set.
Tests are run on various different dev, CI, and containerized setups, so they need to be deterministic and not reliant on the environment they're run in.
| sources_found: list[ConfigSource] = [] | ||
|
|
||
| if mode_result.value is not UNSET and mode_result.value is not None: | ||
| kwargs["retry_mode"] = mode_result.value |
There was a problem hiding this comment.
We need to validate that the configured retry_mode value is one we accept/support. I'd recommend validating against typing.get_args(RetryStrategyType) or something similar so it stays aligned with the actual supported list. If something is configured that isn't in that list, we should raise an error.
Here is an example of me being able to configure a mode that doesn't exist:
config file:
# ~/.aws/config
[default]
retry_mode = fake-mode(smithy-python) $ python -m asyncio
asyncio REPL 3.12.13 (main, Mar 25 2026, 03:16:06) [Clang 22.1.1 ] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from smithy_aws_core.config.aws_config import AsyncAwsConfig
>>> config = await AsyncAwsConfig.resolve()
>>> config
AsyncAwsConfig(region='us-west-2', retry_strategy_options=RetryStrategyOptions(retry_mode='fake-mode', max_attempts=None))For reference, this is what botocore raises:
botocore.exceptions.InvalidRetryModeError: Invalid value provided to "mode": "fake-mode" must be one of: ('legacy', 'standard', 'adaptive')
| sources_found.append(mode_result.source) | ||
|
|
||
| if attempts_result.value is not UNSET and attempts_result.value is not None: | ||
| kwargs["max_attempts"] = attempts_result.value |
There was a problem hiding this comment.
I also think we should validate the these ranges. Here is an example of me sucessfully configuring max_attempts = -1 which shouldn't be allowed:
config file:
# ~/.aws/config
[default]
max_attempts = -1(smithy-python) $ python -m asyncio
asyncio REPL 3.12.13 (main, Mar 25 2026, 03:16:06) [Clang 22.1.1 ] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from smithy_aws_core.config.aws_config import AsyncAwsConfig
>>> config = await AsyncAwsConfig.resolve()
>>> config
AsyncAwsConfig(region='us-west-2', retry_strategy_options=RetryStrategyOptions(retry_mode='standard', max_attempts=-1))Botocore error for reference:
botocore.exceptions.InvalidMaxRetryAttemptsError: Value provided to "max_attempts": -1 must be an integer greater than or equal to 1.
| _REGION_PATTERN = re.compile(r"^(?![0-9]+$)(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$") | ||
|
|
||
|
|
||
| def validate_region(value: object) -> None: |
There was a problem hiding this comment.
It seems like we're only validating on the initial AsyncAwsConfig.resolve(...) call. When users set values directly after resolution, I feel like we should be revalidating.
Example 1: config rejects a bad value at resolution time:
(smithy-python) $ python -m asyncio
asyncio REPL 3.12.13 (main, Mar 25 2026, 03:16:06) [Clang 22.1.1 ] on darwin
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> from smithy_aws_core.config.aws_config import AsyncAwsConfig
>>> config = await AsyncAwsConfig.resolve(region="bad-value!")
Traceback (most recent call last):
File "/Users/gytndd/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/concurrent/futures/_base.py", line 456, in result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
File "/Users/gytndd/.local/share/uv/python/cpython-3.12.13-macos-aarch64-none/lib/python3.12/concurrent/futures/_base.py", line 401, in __get_result
raise self._exception
File "<console>", line 1, in <module>
File "/Users/gytndd/dev/GitHub/temp6/smithy-python/packages/smithy-aws-core/src/smithy_aws_core/config/aws_config.py", line 96, in resolve
await instance._resolve_fields(overrides)
File "/Users/gytndd/dev/GitHub/temp6/smithy-python/packages/smithy-aws-core/src/smithy_aws_core/config/aws_config.py", line 156, in _resolve_fields
spec.validator(getattr(self, field_name))
File "/Users/gytndd/dev/GitHub/temp6/smithy-python/packages/smithy-aws-core/src/smithy_aws_core/config/validators.py", line 26, in validate_region
raise ConfigError(
smithy_aws_core.config.exceptions.ConfigError: Invalid value for 'region': 'bad-value!'. Must be a valid AWS region identifier.Example 2: we config a valid region, then successfully overwrite with a bad region:
>>> config = await AsyncAwsConfig.resolve(region="us-east-1")
>>> config.region
'us-east-1'
>>> config.region = "bad-value!"
>>> config.region
'bad-value!'| except (ValueError, TypeError): | ||
| raise ConfigError( | ||
| f"Invalid integer value {result.value!r} for config key. " | ||
| "Expected a valid integer." | ||
| ) |
There was a problem hiding this comment.
nit - We should catch the error and raise from it:
except (ValueError, TypeError) as e:
raise ConfigError(...) from e| cls, | ||
| *, | ||
| profile: str | None = None, | ||
| env: Mapping[str, str] | None = None, |
There was a problem hiding this comment.
It doesn't seem like we're gaining the full value of env since it doesn't get propagated to the methods lower in the call stack. Specifically, it gets lost through parsed_profiles() → load_config() → _resolve_config_paths() which just use os.environ.
Could we thread ctx.env all the way through load_config/_resolve_config_paths (an optional env param defaulting to os.environ) so all env lookups share one source?
There was a problem hiding this comment.
I have some concerns about this after investigating an env mapping override for our new credential chain.
Does the env mapping provide any value outside of environment injection for tests? Correct me if I'm wrong, but I don't think we want or need to expose this for customers to provide custom mappings in the config. Should we not always use the os environment?
If the above is true, keeping this adds unnecessary plumbing which can be replaced by monkeypatch in tests.
| :param credentials_file_path: Override path for credentials file. | ||
| """ | ||
| self._env: Mapping[str, str] = env if env is not None else os.environ | ||
| self._fs: FileSystem = fs if fs is not None else DefaultFileSystem() |
There was a problem hiding this comment.
I don't really get why this file abstraction was introduced. It seems to be stored here but is never used. Can you explain to me why this was introduced and what value it's supposed to bring?
Issue #, if available:
Description of changes:
This change implements the async config resolution pipeline with
AsyncAwsConfigusing aresolve()classmethod as the only construction path. It addsSharedConfigContextfor holding env vars and memoized config file data,FieldSpecfor per-field resolution metadata, and provenance tracking viaConfigSource. Resolvers for region andretry_strategy_optionsare included along with validators for both fields.Testing:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.