From 84d0e79956f018e20d97c4c9fc14d57c8f9ed76c Mon Sep 17 00:00:00 2001 From: Yordan Miladinov Date: Mon, 27 Jan 2025 18:34:07 +0200 Subject: [PATCH 01/10] Prefix config environment vars to avoid conflicts --- docs/userguides/config.md | 49 +++++++++++++++++++++ src/ape/api/config.py | 6 +-- src/ape_cache/config.py | 3 ++ src/ape_compile/config.py | 3 ++ src/ape_compile/main.py | 10 +++++ src/ape_console/config.py | 4 ++ src/ape_ethereum/ecosystem.py | 4 +- src/ape_networks/config.py | 5 +++ src/ape_node/provider.py | 4 +- src/ape_test/config.py | 13 ++++++ tests/functional/test_config.py | 77 +++++++++++++++++++++++++++++++-- 11 files changed, 169 insertions(+), 9 deletions(-) create mode 100755 src/ape_compile/main.py diff --git a/docs/userguides/config.md b/docs/userguides/config.md index 919e070e6d..078f64112f 100644 --- a/docs/userguides/config.md +++ b/docs/userguides/config.md @@ -37,6 +37,55 @@ plugin: This helps keep your secrets out of Ape! +If a configuration is left unset (i.e., not included in the +`ape-config.(yaml|json|toml)` file, Ape will optionally inspect the +environment variables as a fallback, following the pattern +"_SETTING", where different types of configurations have +different prefixes. + +For example, the following config: + +```yaml +contracts_folder: src/qwe +test: + number_of_accounts: 3 + show_internal: True +compile: + exclude: + - "one" + - "two" + - "three" + include_dependencies: true +``` + +could be entirely defined with environment variables as follows: + +```shell +APE_CONTRACTS_FOLDER=src/contracts +APE_TEST_NUMBER_OF_ACCOUNTS=3 +APE_TEST_SHOW_INTERNAL=true +APE_COMPILE_EXCLUDE='["one", "two", "three"]' +APE_COMPILE_INCLUDE_DEPENDENCIES=true +``` + +Here is the complete list of supported prefixes: + +| Module | Prefix | +| ------------ | ------------ | +| ape | APE | +| ape_cache | APE_CACHE | +| ape_compile | APE_COMPILE | +| ape_console | APE_CONSOLE | +| ape_ethereum | APE_ETHEREUM | +| ape_networks | APE_NETWORKS | +| ape_node | APE_NODE | +| ape_test | APE_TEST | + +As this is an experimental feature, changes are likely to be +introduced in the future. Please rely primarily on file-based +configuration and use environment variables only if absolutely +necessary. + ## Base Path Change the base path if it is different than your project root. diff --git a/src/ape/api/config.py b/src/ape/api/config.py index 141b226d02..7ae7640140 100644 --- a/src/ape/api/config.py +++ b/src/ape/api/config.py @@ -66,7 +66,7 @@ class PluginConfig(BaseSettings): a config API must register a subclass of this class. """ - model_config = SettingsConfigDict(extra="allow") + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_") @classmethod def from_overrides( @@ -285,7 +285,7 @@ class ApeConfig(ExtraAttributesMixin, BaseSettings, ManagerAccessMixin): def __init__(self, *args, **kwargs): project_path = kwargs.get("project") - super(BaseSettings, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # NOTE: Cannot reference `self` at all until after super init. self._project_path = project_path @@ -350,7 +350,7 @@ def __init__(self, *args, **kwargs): """ # NOTE: Plugin configs are technically "extras". - model_config = SettingsConfigDict(extra="allow") + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_") @model_validator(mode="before") @classmethod diff --git a/src/ape_cache/config.py b/src/ape_cache/config.py index 264516b738..dc45f7f1a3 100644 --- a/src/ape_cache/config.py +++ b/src/ape_cache/config.py @@ -1,5 +1,8 @@ +from pydantic_settings import SettingsConfigDict + from ape.api.config import PluginConfig class CacheConfig(PluginConfig): size: int = 1024**3 # 1gb + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_CACHE_") diff --git a/src/ape_compile/config.py b/src/ape_compile/config.py index 012e590b04..ef0c749f5f 100644 --- a/src/ape_compile/config.py +++ b/src/ape_compile/config.py @@ -3,6 +3,7 @@ from typing import Union from pydantic import field_serializer, field_validator +from pydantic_settings import SettingsConfigDict from ape.api.config import ConfigEnum, PluginConfig from ape.utils.misc import SOURCE_EXCLUDE_PATTERNS @@ -53,6 +54,8 @@ class Config(PluginConfig): Extra selections to output. Outputs to ``.build/{key.lower()}``. """ + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_COMPILE_") + @field_validator("exclude", mode="before") @classmethod def validate_exclude(cls, value): diff --git a/src/ape_compile/main.py b/src/ape_compile/main.py new file mode 100755 index 0000000000..316b4b50c3 --- /dev/null +++ b/src/ape_compile/main.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +print(__package__) +print(__name__) + +import re + +match: re.Match | None = re.search(r'/(ape_\w+)/', __file__) +envprefix: str = f'{match.group(1).upper()}_' if match else 'APE_' +print(envprefix) diff --git a/src/ape_console/config.py b/src/ape_console/config.py index 48b432ced2..9066867714 100644 --- a/src/ape_console/config.py +++ b/src/ape_console/config.py @@ -1,6 +1,10 @@ +from pydantic_settings import SettingsConfigDict + from ape.api.config import PluginConfig class ConsoleConfig(PluginConfig): plugins: list[str] = [] """Additional IPython plugins to include in your session.""" + + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_CONSOLE_") diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 94aff193be..74524104f0 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -157,6 +157,8 @@ class NetworkConfig(PluginConfig): request_headers: dict = {} """Optionally config extra request headers whenever using this network.""" + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_ETHEREUM_") + @field_validator("gas_limit", mode="before") @classmethod def validate_gas_limit(cls, value): @@ -233,7 +235,7 @@ class BaseEthereumConfig(PluginConfig): # NOTE: This gets appended to Ape's root User-Agent string. request_headers: dict = {} - model_config = SettingsConfigDict(extra="allow") + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_ETHEREUM_") @model_validator(mode="before") @classmethod diff --git a/src/ape_networks/config.py b/src/ape_networks/config.py index 381cd268b2..a519ed64fe 100644 --- a/src/ape_networks/config.py +++ b/src/ape_networks/config.py @@ -1,5 +1,7 @@ from typing import Optional +from pydantic_settings import SettingsConfigDict + from ape.api.config import PluginConfig @@ -26,6 +28,8 @@ class CustomNetwork(PluginConfig): request_header: dict = {} """The HTTP request header.""" + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NETWORKS_") + @property def is_fork(self) -> bool: """ @@ -36,3 +40,4 @@ def is_fork(self) -> bool: class NetworksConfig(PluginConfig): custom: list[CustomNetwork] = [] + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NETWORKS_") diff --git a/src/ape_node/provider.py b/src/ape_node/provider.py index f8af5eeb08..579306de8f 100644 --- a/src/ape_node/provider.py +++ b/src/ape_node/provider.py @@ -297,7 +297,7 @@ class EthereumNetworkConfig(PluginConfig): # Make sure to run via `geth --dev` (or similar) local: dict = {**DEFAULT_SETTINGS.copy(), "chain_id": DEFAULT_TEST_CHAIN_ID} - model_config = SettingsConfigDict(extra="allow") + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NODE_") @field_validator("local", mode="before") @classmethod @@ -357,7 +357,7 @@ class EthereumNodeConfig(PluginConfig): Optionally specify request headers to use whenever using this provider. """ - model_config = SettingsConfigDict(extra="allow") + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_NODE_") @field_validator("call_trace_approach", mode="before") @classmethod diff --git a/src/ape_test/config.py b/src/ape_test/config.py index 3ce2609dfe..adb2541a6f 100644 --- a/src/ape_test/config.py +++ b/src/ape_test/config.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, NewType, Optional, Union from pydantic import NonNegativeInt, field_validator +from pydantic_settings import SettingsConfigDict from ape.api.config import PluginConfig from ape.utils.basemodel import ManagerAccessMixin @@ -19,11 +20,13 @@ class EthTesterProviderConfig(PluginConfig): chain_id: int = DEFAULT_TEST_CHAIN_ID auto_mine: bool = True + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") class GasExclusion(PluginConfig): contract_name: str = "*" # If only given method, searches across all contracts. method_name: Optional[str] = None # By default, match all methods in a contract + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") CoverageExclusion = NewType("CoverageExclusion", GasExclusion) @@ -48,6 +51,8 @@ class GasConfig(PluginConfig): Report-types to use. Currently, only supports `terminal`. """ + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") + @field_validator("reports", mode="before") @classmethod def validate_reports(cls, values): @@ -89,6 +94,8 @@ class CoverageReportsConfig(PluginConfig): Set to ``True`` to generate HTML coverage reports. """ + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") + @property def has_any(self) -> bool: return any(x not in ({}, None, False) for x in (self.html, self.terminal, self.xml)) @@ -119,6 +126,8 @@ class CoverageConfig(PluginConfig): use ``prefix_*`` to skip all items with a certain prefix. """ + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") + class IsolationConfig(PluginConfig): enable_session: bool = True @@ -146,6 +155,8 @@ class IsolationConfig(PluginConfig): Set to ``False`` to disable function isolation. """ + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") + def get_isolation(self, scope: "Scope") -> bool: return getattr(self, f"enable_{scope.name.lower()}") @@ -209,6 +220,8 @@ class ApeTestConfig(PluginConfig): ``False`` to disable all and ``True`` (default) to disable all. """ + model_config = SettingsConfigDict(extra="allow", env_prefix="APE_TEST_") + @field_validator("balance", mode="before") @classmethod def validate_balance(cls, value): diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 6d82b36598..64f74d3972 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -1,7 +1,7 @@ import os import re from pathlib import Path -from typing import TYPE_CHECKING, Optional, Union +from typing import Any, Callable, TYPE_CHECKING, Optional, Type, Union import pytest from pydantic import ValidationError @@ -10,9 +10,14 @@ from ape.api.config import ApeConfig, ConfigEnum, PluginConfig from ape.exceptions import ConfigError from ape.managers.config import CONFIG_FILE_NAME, merge_configs +from ape_console.config import ConsoleConfig +from ape_cache.config import CacheConfig +from ape_compile.config import Config as CompileConfig from ape.utils.os import create_tempdir -from ape_ethereum.ecosystem import EthereumConfig, NetworkConfig -from ape_networks import CustomNetwork +from ape_ethereum.ecosystem import BaseEthereumConfig, EthereumConfig, NetworkConfig, ForkedNetworkConfig +from ape_node.provider import EthereumNetworkConfig, EthereumNodeConfig +from ape_networks.config import CustomNetwork +from ape_test.config import ApeTestConfig, CoverageConfig, CoverageReportsConfig, EthTesterProviderConfig, GasConfig, GasExclusion, IsolationConfig from tests.functional.conftest import PROJECT_WITH_LONG_CONTRACTS_FOLDER if TYPE_CHECKING: @@ -618,3 +623,69 @@ def test_project_level_settings(project): assert project.config.my_string == "my_string" assert project.config.my_int == 123 assert project.config.my_bool is True + + +def test_model_validate_handles_environment_variables(): + def f(cls: Callable, attr: str, name: str, value: str, expected: Any = None): + expected = expected if expected is not None else value + before: str | None = os.environ.get(name) + os.environ[name] = value + try: + instance = cls() + assert hasattr(instance, attr) + assert getattr(instance, attr) == expected + finally: + if before is not None: + os.environ[name] = before + + # Test different config classes. + f(ApeConfig, "contracts_folder", "APE_CONTRACTS_FOLDER", "3465220869b2") + f(ApeConfig, "dependencies", "APE_DEPENDENCIES", '[{"a":1},{"b":2},{"c":3}]', [{"a": 1}, {"b": 2}, {"c": 3}]) + f(CacheConfig, "size", "APE_CACHE_SIZE", "8627", 8627) + f(CompileConfig, "include_dependencies", "APE_COMPILE_INCLUDE_DEPENDENCIES", "true", True) + f(ConsoleConfig, "plugins", "APE_CONSOLE_PLUGINS", '["a","b","c"]', ["a", "b", "c"]) + f(BaseEthereumConfig, "default_network", "APE_ETHEREUM_DEFAULT_NETWORK", "abe9e8293383") + f(ForkedNetworkConfig, "upstream_provider", "APE_ETHEREUM_UPSTREAM_PROVIDER", "411236f13659") + f(NetworkConfig, "required_confirmations", "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", "6498", 6498) + f(lambda: CustomNetwork(name="", chain_id=0, ecosystem=""), + "base_ecosystem_plugin", "APE_NETWORKS_BASE_ECOSYSTEM_PLUGIN", "ea5010088102") + f(EthereumNetworkConfig, "mainnet", "APE_NODE_MAINNET", '{"a":"b"}', {"a":"b"}) + f(EthereumNodeConfig, "executable", "APE_NODE_EXECUTABLE", "40613177e494") + f(ApeTestConfig, "balance", "APE_TEST_BALANCE", "4798", 4798) + f(CoverageConfig, "track", "APE_TEST_TRACK", "true", True) + f(CoverageReportsConfig, "terminal", "APE_TEST_TERMINAL", "false", False) + f(EthTesterProviderConfig, "chain_id", "APE_TEST_CHAIN_ID", "7925", 7925) + f(GasConfig, "reports", "APE_TEST_REPORTS", '["terminal"]', ["terminal"]) + f(GasExclusion, "method_name", "APE_TEST_METHOD_NAME", "32aa54e3c5d2") + f(IsolationConfig, "enable_session", "APE_TEST_ENABLE_SESSION", "false", False) + + # Assert that union types are handled. + f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0", 0) + f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0x100", 0x100) + f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "auto") + f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "max") + with pytest.raises(ValidationError, match=r"Value error, Invalid gas limit"): + f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "something") + + # Assert that various bool variants are parsed correctly. + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "0", False) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "False", False) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "fALSE", False) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "FALSE", False) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "1", True) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "True", True) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "tRUE", True) + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "TRUE", True) + + # We expect a failure when there's a type mismatch. + with pytest.raises( + ValidationError, + match=r"Input should be a valid boolean, unable to interpret input", + ): + f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "not a boolean", False) + + with pytest.raises( + ValidationError, + match=r"Input should be a valid integer, unable to parse string as an integer", + ): + f(NetworkConfig, "required_confirmations", "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", "not a number", 42) From b52ba33ca2f2ce721cfd193bc4a3ac2fd53eac21 Mon Sep 17 00:00:00 2001 From: antazoey Date: Mon, 27 Jan 2025 12:29:02 -0600 Subject: [PATCH 02/10] docs: adjust docs --- docs/userguides/config.md | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/userguides/config.md b/docs/userguides/config.md index 078f64112f..d83a6e9338 100644 --- a/docs/userguides/config.md +++ b/docs/userguides/config.md @@ -37,11 +37,9 @@ plugin: This helps keep your secrets out of Ape! -If a configuration is left unset (i.e., not included in the -`ape-config.(yaml|json|toml)` file, Ape will optionally inspect the -environment variables as a fallback, following the pattern -"_SETTING", where different types of configurations have -different prefixes. +Similarly, any config key-name can also be set with the same named environment variable (with a prefix). + +If a configuration is left unset (i.e., not included in the `ape-config.(yaml|json|toml)` file, Ape will inspect the environment variables as a fallback, following the pattern `APE__SETTING`, where different plugins define different prefixes. For example, the following config: @@ -68,27 +66,29 @@ APE_COMPILE_EXCLUDE='["one", "two", "three"]' APE_COMPILE_INCLUDE_DEPENDENCIES=true ``` -Here is the complete list of supported prefixes: +Notice the `ape-compile` and `ape-test` plugin include their plugin name `APE_COMPILE` and `APE_TEST` respectively where `contracts_folder` only has the prefix `APE_` since it is not part of a plugin. + +Here is the complete list of supported prefixes that come with Ape out-of-the-box: + +| Module/Plugin | Prefix | +|---------------| ------------ | +| ape | APE | +| ape_cache | APE_CACHE | +| ape_compile | APE_COMPILE | +| ape_console | APE_CONSOLE | +| ape_ethereum | APE_ETHEREUM | +| ape_networks | APE_NETWORKS | +| ape_node | APE_NODE | +| ape_test | APE_TEST | -| Module | Prefix | -| ------------ | ------------ | -| ape | APE | -| ape_cache | APE_CACHE | -| ape_compile | APE_COMPILE | -| ape_console | APE_CONSOLE | -| ape_ethereum | APE_ETHEREUM | -| ape_networks | APE_NETWORKS | -| ape_node | APE_NODE | -| ape_test | APE_TEST | +Each plugin outside the core package may define its own prefix, but the standard is `APE_PLUGINNAME_`. -As this is an experimental feature, changes are likely to be -introduced in the future. Please rely primarily on file-based -configuration and use environment variables only if absolutely -necessary. +Using environment variables assists in keeping secrets out of your config files. +However, the primary config should be file-driven and environment variables should only be used when necessary. ## Base Path -Change the base path if it is different than your project root. +Change the base path if it is different from your project root. For example, imagine a project structure like: ``` From d33c5424534cc7c9887355c6c627c1b426d00f68 Mon Sep 17 00:00:00 2001 From: antazoey Date: Mon, 27 Jan 2025 12:42:08 -0600 Subject: [PATCH 03/10] fix: base settings --- tests/functional/test_config.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 64f74d3972..32244ea6ea 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -134,6 +134,12 @@ def test_model_validate_path_contracts_folder(): assert cfg.contracts_folder == str(path) +def test_model_validate_handles_environment_variables(): + os.environ["APE_API_CONTRACTS_FOLDER"] = "contracts-env-var-test" + cfg = ApeConfig() + assert cfg.contracts_folder == "contracts-env-var-test" + + @pytest.mark.parametrize( "file", ("ape-config.yml", "ape-config.yaml", "ape-config.json", "pyproject.toml") ) @@ -157,7 +163,7 @@ def test_validate_file(file): assert "Excl*.json" in actual.compile.exclude -def test_validate_file_expands_env_vars(): +def test_validate_file_expands_environment_variables(): secret = "mycontractssecretfolder" env_var_name = "APE_TEST_CONFIG_SECRET_CONTRACTS_FOLDER" os.environ[env_var_name] = secret From 8876918953049b41341834e1e3b3bb6e9cc76337 Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 14:07:47 -0600 Subject: [PATCH 04/10] test: and lint --- src/ape_compile/main.py | 10 -- tests/functional/test_config.py | 179 +++++++++++++++++++------------- 2 files changed, 104 insertions(+), 85 deletions(-) delete mode 100755 src/ape_compile/main.py diff --git a/src/ape_compile/main.py b/src/ape_compile/main.py deleted file mode 100755 index 316b4b50c3..0000000000 --- a/src/ape_compile/main.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python - -print(__package__) -print(__name__) - -import re - -match: re.Match | None = re.search(r'/(ape_\w+)/', __file__) -envprefix: str = f'{match.group(1).upper()}_' if match else 'APE_' -print(envprefix) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 32244ea6ea..486affb450 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -1,7 +1,7 @@ import os import re from pathlib import Path -from typing import Any, Callable, TYPE_CHECKING, Optional, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Optional, Union import pytest from pydantic import ValidationError @@ -10,14 +10,27 @@ from ape.api.config import ApeConfig, ConfigEnum, PluginConfig from ape.exceptions import ConfigError from ape.managers.config import CONFIG_FILE_NAME, merge_configs -from ape_console.config import ConsoleConfig +from ape.utils.os import create_tempdir from ape_cache.config import CacheConfig from ape_compile.config import Config as CompileConfig -from ape.utils.os import create_tempdir -from ape_ethereum.ecosystem import BaseEthereumConfig, EthereumConfig, NetworkConfig, ForkedNetworkConfig -from ape_node.provider import EthereumNetworkConfig, EthereumNodeConfig +from ape_console.config import ConsoleConfig +from ape_ethereum.ecosystem import ( + BaseEthereumConfig, + EthereumConfig, + ForkedNetworkConfig, + NetworkConfig, +) from ape_networks.config import CustomNetwork -from ape_test.config import ApeTestConfig, CoverageConfig, CoverageReportsConfig, EthTesterProviderConfig, GasConfig, GasExclusion, IsolationConfig +from ape_node.provider import EthereumNetworkConfig, EthereumNodeConfig +from ape_test.config import ( + ApeTestConfig, + CoverageConfig, + CoverageReportsConfig, + EthTesterProviderConfig, + GasConfig, + GasExclusion, + IsolationConfig, +) from tests.functional.conftest import PROJECT_WITH_LONG_CONTRACTS_FOLDER if TYPE_CHECKING: @@ -135,9 +148,91 @@ def test_model_validate_path_contracts_folder(): def test_model_validate_handles_environment_variables(): - os.environ["APE_API_CONTRACTS_FOLDER"] = "contracts-env-var-test" - cfg = ApeConfig() - assert cfg.contracts_folder == "contracts-env-var-test" + def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = None): + expected = expected if expected is not None else value + before: str | None = os.environ.get(name) + os.environ[name] = value + try: + instance = cls() + assert hasattr(instance, attr) + assert getattr(instance, attr) == expected + finally: + if before is not None: + os.environ[name] = before + + # Test different config classes. + run_test(ApeConfig, "contracts_folder", "APE_CONTRACTS_FOLDER", "3465220869b2") + run_test( + ApeConfig, + "dependencies", + "APE_DEPENDENCIES", + '[{"a":1},{"b":2},{"c":3}]', + [{"a": 1}, {"b": 2}, {"c": 3}], + ) + run_test(CacheConfig, "size", "APE_CACHE_SIZE", "8627", 8627) + run_test( + CompileConfig, "include_dependencies", "APE_COMPILE_INCLUDE_DEPENDENCIES", "true", True + ) + run_test(ConsoleConfig, "plugins", "APE_CONSOLE_PLUGINS", '["a","b","c"]', ["a", "b", "c"]) + run_test(BaseEthereumConfig, "default_network", "APE_ETHEREUM_DEFAULT_NETWORK", "abe9e8293383") + run_test( + ForkedNetworkConfig, "upstream_provider", "APE_ETHEREUM_UPSTREAM_PROVIDER", "411236f13659" + ) + run_test( + NetworkConfig, "required_confirmations", "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", "6498", 6498 + ) + run_test( + lambda: CustomNetwork(name="", chain_id=0, ecosystem=""), + "base_ecosystem_plugin", + "APE_NETWORKS_BASE_ECOSYSTEM_PLUGIN", + "ea5010088102", + ) + run_test(EthereumNetworkConfig, "mainnet", "APE_NODE_MAINNET", '{"a":"b"}', {"a": "b"}) + run_test(EthereumNodeConfig, "executable", "APE_NODE_EXECUTABLE", "40613177e494") + run_test(ApeTestConfig, "balance", "APE_TEST_BALANCE", "4798", 4798) + run_test(CoverageConfig, "track", "APE_TEST_TRACK", "true", True) + run_test(CoverageReportsConfig, "terminal", "APE_TEST_TERMINAL", "false", False) + run_test(EthTesterProviderConfig, "chain_id", "APE_TEST_CHAIN_ID", "7925", 7925) + run_test(GasConfig, "reports", "APE_TEST_REPORTS", '["terminal"]', ["terminal"]) + run_test(GasExclusion, "method_name", "APE_TEST_METHOD_NAME", "32aa54e3c5d2") + run_test(IsolationConfig, "enable_session", "APE_TEST_ENABLE_SESSION", "false", False) + + # Assert that union types are handled. + run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0", 0) + run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0x100", 0x100) + run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "auto") + run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "max") + with pytest.raises(ValidationError, match=r"Value error, Invalid gas limit"): + run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "something") + + # Assert that various bool variants are parsed correctly. + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "0", False) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "False", False) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "fALSE", False) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "FALSE", False) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "1", True) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "True", True) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "tRUE", True) + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "TRUE", True) + + # We expect a failure when there's a type mismatch. + with pytest.raises( + ValidationError, + match=r"Input should be a valid boolean, unable to interpret input", + ): + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "not a boolean", False) + + with pytest.raises( + ValidationError, + match=r"Input should be a valid integer, unable to parse string as an integer", + ): + run_test( + NetworkConfig, + "required_confirmations", + "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", + "not a number", + 42, + ) @pytest.mark.parametrize( @@ -629,69 +724,3 @@ def test_project_level_settings(project): assert project.config.my_string == "my_string" assert project.config.my_int == 123 assert project.config.my_bool is True - - -def test_model_validate_handles_environment_variables(): - def f(cls: Callable, attr: str, name: str, value: str, expected: Any = None): - expected = expected if expected is not None else value - before: str | None = os.environ.get(name) - os.environ[name] = value - try: - instance = cls() - assert hasattr(instance, attr) - assert getattr(instance, attr) == expected - finally: - if before is not None: - os.environ[name] = before - - # Test different config classes. - f(ApeConfig, "contracts_folder", "APE_CONTRACTS_FOLDER", "3465220869b2") - f(ApeConfig, "dependencies", "APE_DEPENDENCIES", '[{"a":1},{"b":2},{"c":3}]', [{"a": 1}, {"b": 2}, {"c": 3}]) - f(CacheConfig, "size", "APE_CACHE_SIZE", "8627", 8627) - f(CompileConfig, "include_dependencies", "APE_COMPILE_INCLUDE_DEPENDENCIES", "true", True) - f(ConsoleConfig, "plugins", "APE_CONSOLE_PLUGINS", '["a","b","c"]', ["a", "b", "c"]) - f(BaseEthereumConfig, "default_network", "APE_ETHEREUM_DEFAULT_NETWORK", "abe9e8293383") - f(ForkedNetworkConfig, "upstream_provider", "APE_ETHEREUM_UPSTREAM_PROVIDER", "411236f13659") - f(NetworkConfig, "required_confirmations", "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", "6498", 6498) - f(lambda: CustomNetwork(name="", chain_id=0, ecosystem=""), - "base_ecosystem_plugin", "APE_NETWORKS_BASE_ECOSYSTEM_PLUGIN", "ea5010088102") - f(EthereumNetworkConfig, "mainnet", "APE_NODE_MAINNET", '{"a":"b"}', {"a":"b"}) - f(EthereumNodeConfig, "executable", "APE_NODE_EXECUTABLE", "40613177e494") - f(ApeTestConfig, "balance", "APE_TEST_BALANCE", "4798", 4798) - f(CoverageConfig, "track", "APE_TEST_TRACK", "true", True) - f(CoverageReportsConfig, "terminal", "APE_TEST_TERMINAL", "false", False) - f(EthTesterProviderConfig, "chain_id", "APE_TEST_CHAIN_ID", "7925", 7925) - f(GasConfig, "reports", "APE_TEST_REPORTS", '["terminal"]', ["terminal"]) - f(GasExclusion, "method_name", "APE_TEST_METHOD_NAME", "32aa54e3c5d2") - f(IsolationConfig, "enable_session", "APE_TEST_ENABLE_SESSION", "false", False) - - # Assert that union types are handled. - f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0", 0) - f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0x100", 0x100) - f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "auto") - f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "max") - with pytest.raises(ValidationError, match=r"Value error, Invalid gas limit"): - f(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "something") - - # Assert that various bool variants are parsed correctly. - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "0", False) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "False", False) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "fALSE", False) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "FALSE", False) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "1", True) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "True", True) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "tRUE", True) - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "TRUE", True) - - # We expect a failure when there's a type mismatch. - with pytest.raises( - ValidationError, - match=r"Input should be a valid boolean, unable to interpret input", - ): - f(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "not a boolean", False) - - with pytest.raises( - ValidationError, - match=r"Input should be a valid integer, unable to parse string as an integer", - ): - f(NetworkConfig, "required_confirmations", "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", "not a number", 42) From 5deb5002809dc1db2dc5bf7c0f964b80f1983a3e Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 14:09:22 -0600 Subject: [PATCH 05/10] docs: mdformat --- docs/userguides/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguides/config.md b/docs/userguides/config.md index d83a6e9338..768ae758b7 100644 --- a/docs/userguides/config.md +++ b/docs/userguides/config.md @@ -71,7 +71,7 @@ Notice the `ape-compile` and `ape-test` plugin include their plugin name `APE_CO Here is the complete list of supported prefixes that come with Ape out-of-the-box: | Module/Plugin | Prefix | -|---------------| ------------ | +| ------------- | ------------ | | ape | APE | | ape_cache | APE_CACHE | | ape_compile | APE_COMPILE | From 9e1a21a4db75b91bfb90b45db18b7fac2e81f8f2 Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 14:19:38 -0600 Subject: [PATCH 06/10] test: del some problematic ones --- tests/functional/test_config.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 486affb450..dbcbf98301 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -174,19 +174,12 @@ def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = No CompileConfig, "include_dependencies", "APE_COMPILE_INCLUDE_DEPENDENCIES", "true", True ) run_test(ConsoleConfig, "plugins", "APE_CONSOLE_PLUGINS", '["a","b","c"]', ["a", "b", "c"]) - run_test(BaseEthereumConfig, "default_network", "APE_ETHEREUM_DEFAULT_NETWORK", "abe9e8293383") run_test( ForkedNetworkConfig, "upstream_provider", "APE_ETHEREUM_UPSTREAM_PROVIDER", "411236f13659" ) run_test( NetworkConfig, "required_confirmations", "APE_ETHEREUM_REQUIRED_CONFIRMATIONS", "6498", 6498 ) - run_test( - lambda: CustomNetwork(name="", chain_id=0, ecosystem=""), - "base_ecosystem_plugin", - "APE_NETWORKS_BASE_ECOSYSTEM_PLUGIN", - "ea5010088102", - ) run_test(EthereumNetworkConfig, "mainnet", "APE_NODE_MAINNET", '{"a":"b"}', {"a": "b"}) run_test(EthereumNodeConfig, "executable", "APE_NODE_EXECUTABLE", "40613177e494") run_test(ApeTestConfig, "balance", "APE_TEST_BALANCE", "4798", 4798) From 45dd8698176f20e3ba2d282636cfd9103c123697 Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 14:26:12 -0600 Subject: [PATCH 07/10] docs: comment --- tests/functional/test_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_coverage.py b/tests/functional/test_coverage.py index ce9ae2a35d..63aa237b26 100644 --- a/tests/functional/test_coverage.py +++ b/tests/functional/test_coverage.py @@ -255,7 +255,7 @@ def init_profile(source_cov, src): try: # Hack in our mock compiler. - _ = compilers.registered_compilers # Ensure cache is exists. + _ = compilers.registered_compilers # Ensure cache exists. compilers.__dict__["registered_compilers"][mock_compiler.ext] = mock_compiler # Ensure our coverage tracker is using our new tmp project w/ the new src From 7ab3c696fc2c43195fb512ac51d5ab9dce9f0a5b Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 14:32:36 -0600 Subject: [PATCH 08/10] chore: lint --- tests/functional/test_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index dbcbf98301..949ee9f678 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -15,7 +15,6 @@ from ape_compile.config import Config as CompileConfig from ape_console.config import ConsoleConfig from ape_ethereum.ecosystem import ( - BaseEthereumConfig, EthereumConfig, ForkedNetworkConfig, NetworkConfig, From f400a17c4d9d14e39c1e9eb6b744a90bae35cde5 Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 14:39:34 -0600 Subject: [PATCH 09/10] test: rm some more --- tests/functional/test_config.py | 43 ++++++--------------------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index 949ee9f678..b9a466d32b 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -13,23 +13,10 @@ from ape.utils.os import create_tempdir from ape_cache.config import CacheConfig from ape_compile.config import Config as CompileConfig -from ape_console.config import ConsoleConfig -from ape_ethereum.ecosystem import ( - EthereumConfig, - ForkedNetworkConfig, - NetworkConfig, -) +from ape_ethereum.ecosystem import EthereumConfig, ForkedNetworkConfig, NetworkConfig from ape_networks.config import CustomNetwork from ape_node.provider import EthereumNetworkConfig, EthereumNodeConfig -from ape_test.config import ( - ApeTestConfig, - CoverageConfig, - CoverageReportsConfig, - EthTesterProviderConfig, - GasConfig, - GasExclusion, - IsolationConfig, -) +from ape_test.config import CoverageReportsConfig, GasConfig, GasExclusion from tests.functional.conftest import PROJECT_WITH_LONG_CONTRACTS_FOLDER if TYPE_CHECKING: @@ -153,7 +140,6 @@ def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = No os.environ[name] = value try: instance = cls() - assert hasattr(instance, attr) assert getattr(instance, attr) == expected finally: if before is not None: @@ -161,18 +147,10 @@ def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = No # Test different config classes. run_test(ApeConfig, "contracts_folder", "APE_CONTRACTS_FOLDER", "3465220869b2") - run_test( - ApeConfig, - "dependencies", - "APE_DEPENDENCIES", - '[{"a":1},{"b":2},{"c":3}]', - [{"a": 1}, {"b": 2}, {"c": 3}], - ) run_test(CacheConfig, "size", "APE_CACHE_SIZE", "8627", 8627) run_test( CompileConfig, "include_dependencies", "APE_COMPILE_INCLUDE_DEPENDENCIES", "true", True ) - run_test(ConsoleConfig, "plugins", "APE_CONSOLE_PLUGINS", '["a","b","c"]', ["a", "b", "c"]) run_test( ForkedNetworkConfig, "upstream_provider", "APE_ETHEREUM_UPSTREAM_PROVIDER", "411236f13659" ) @@ -181,13 +159,9 @@ def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = No ) run_test(EthereumNetworkConfig, "mainnet", "APE_NODE_MAINNET", '{"a":"b"}', {"a": "b"}) run_test(EthereumNodeConfig, "executable", "APE_NODE_EXECUTABLE", "40613177e494") - run_test(ApeTestConfig, "balance", "APE_TEST_BALANCE", "4798", 4798) - run_test(CoverageConfig, "track", "APE_TEST_TRACK", "true", True) run_test(CoverageReportsConfig, "terminal", "APE_TEST_TERMINAL", "false", False) - run_test(EthTesterProviderConfig, "chain_id", "APE_TEST_CHAIN_ID", "7925", 7925) run_test(GasConfig, "reports", "APE_TEST_REPORTS", '["terminal"]', ["terminal"]) run_test(GasExclusion, "method_name", "APE_TEST_METHOD_NAME", "32aa54e3c5d2") - run_test(IsolationConfig, "enable_session", "APE_TEST_ENABLE_SESSION", "false", False) # Assert that union types are handled. run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "0", 0) @@ -198,14 +172,11 @@ def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = No run_test(NetworkConfig, "gas_limit", "APE_ETHEREUM_GAS_LIMIT", "something") # Assert that various bool variants are parsed correctly. - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "0", False) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "False", False) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "fALSE", False) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "FALSE", False) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "1", True) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "True", True) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "tRUE", True) - run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", "TRUE", True) + for bool_val in ("0", "False", "fALSE", "FALSE"): + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", bool_val, False) + + for bool_val in ("1", "True", "tRUE", "TRUE"): + run_test(NetworkConfig, "is_mainnet", "APE_ETHEREUM_IS_MAINNET", bool_val, True) # We expect a failure when there's a type mismatch. with pytest.raises( From bf0ca81cae366b91f079b84ea653fb40b34b086e Mon Sep 17 00:00:00 2001 From: antazoey Date: Tue, 28 Jan 2025 15:13:41 -0600 Subject: [PATCH 10/10] test: wasnt prperly unsetting env --- tests/functional/test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index b9a466d32b..2c31d22381 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -144,6 +144,8 @@ def run_test(cls: Callable, attr: str, name: str, value: str, expected: Any = No finally: if before is not None: os.environ[name] = before + else: + os.environ.pop(name, None) # Test different config classes. run_test(ApeConfig, "contracts_folder", "APE_CONTRACTS_FOLDER", "3465220869b2")