@@ -47,6 +47,19 @@ class MyConfig(PluginConfig):
47
47
"""
48
48
49
49
50
+ def _find_config_yaml_files (base_path : Path ) -> list [Path ]:
51
+ """
52
+ Find all ape config file in the given path.
53
+ """
54
+ found : list [Path ] = []
55
+ if (base_path / "ape-config.yaml" ).is_file ():
56
+ found .append (base_path / "ape-config.yaml" )
57
+ if (base_path / "ape-config.yml" ).is_file ():
58
+ found .append (base_path / "ape-config.yml" )
59
+
60
+ return found
61
+
62
+
50
63
class PluginConfig (BaseSettings ):
51
64
"""
52
65
A base plugin configuration class. Each plugin that includes
@@ -56,7 +69,9 @@ class PluginConfig(BaseSettings):
56
69
model_config = SettingsConfigDict (extra = "allow" )
57
70
58
71
@classmethod
59
- def from_overrides (cls , overrides : dict ) -> "PluginConfig" :
72
+ def from_overrides (
73
+ cls , overrides : dict , plugin_name : Optional [str ] = None , project_path : Optional [Path ] = None
74
+ ) -> "PluginConfig" :
60
75
default_values = cls ().model_dump ()
61
76
62
77
def update (root : dict , value_map : dict ):
@@ -72,7 +87,54 @@ def update(root: dict, value_map: dict):
72
87
try :
73
88
return cls .model_validate (data )
74
89
except ValidationError as err :
75
- raise ConfigError (str (err )) from err
90
+ plugin_name = plugin_name or cls .__name__ .replace ("Config" , "" ).lower ()
91
+ if problems := cls ._find_plugin_config_problems (
92
+ err , plugin_name , project_path = project_path
93
+ ):
94
+ raise ConfigError (problems ) from err
95
+ else :
96
+ raise ConfigError (str (err )) from err
97
+
98
+ @classmethod
99
+ def _find_plugin_config_problems (
100
+ cls , err : ValidationError , plugin_name : str , project_path : Optional [Path ] = None
101
+ ) -> Optional [str ]:
102
+ # Attempt showing line-nos for failed plugin config validation.
103
+ # This is trickier than root-level data since by this time, we
104
+ # no longer are aware of which files are responsible for which config.
105
+ ape = ManagerAccessMixin
106
+
107
+ # First, try checking the root config file ALONE. It is important to do
108
+ # w/o any data from the project-level config to isolate the source of the problem.
109
+ raw_global_data = ape .config_manager .global_config .model_dump (by_alias = True )
110
+ if plugin_name in raw_global_data :
111
+ try :
112
+ cls .model_validate (raw_global_data [plugin_name ])
113
+ except Exception :
114
+ if problems := cls ._find_plugin_config_problems_from_file (
115
+ err , ape .config_manager .DATA_FOLDER
116
+ ):
117
+ return problems
118
+
119
+ # No issues found with global; try the local project.
120
+ # NOTE: No need to isolate project-data w/o root-data because we have already
121
+ # determined root-level data is OK.
122
+ project_path = project_path or ape .local_project .path
123
+ if problems := cls ._find_plugin_config_problems_from_file (err , project_path ):
124
+ return problems
125
+
126
+ return None
127
+
128
+ @classmethod
129
+ def _find_plugin_config_problems_from_file (
130
+ cls , err : ValidationError , base_path : Path
131
+ ) -> Optional [str ]:
132
+ cfg_files = _find_config_yaml_files (base_path )
133
+ for cfg_file in cfg_files :
134
+ if problems := _get_problem_with_config (err .errors (), cfg_file ):
135
+ return problems
136
+
137
+ return None
76
138
77
139
@only_raise_attribute_error
78
140
def __getattr__ (self , attr_name : str ) -> Any :
@@ -221,6 +283,12 @@ class ApeConfig(ExtraAttributesMixin, BaseSettings, ManagerAccessMixin):
221
283
The top-level config.
222
284
"""
223
285
286
+ def __init__ (self , * args , ** kwargs ):
287
+ project_path = kwargs .get ("project" )
288
+ super (BaseSettings , self ).__init__ (* args , ** kwargs )
289
+ # NOTE: Cannot reference `self` at all until after super init.
290
+ self ._project_path = project_path
291
+
224
292
contracts_folder : Optional [str ] = None
225
293
"""
226
294
The path to the folder containing the contract source files.
@@ -437,7 +505,9 @@ def get_plugin_config(self, name: str) -> Optional[PluginConfig]:
437
505
438
506
if cls != ConfigDict :
439
507
# NOTE: Will raise if improperly provided keys
440
- config = cls .from_overrides (cfg )
508
+ config = cls .from_overrides (
509
+ cfg , plugin_name = plugin_name , project_path = self ._project_path
510
+ )
441
511
else :
442
512
# NOTE: Just use it directly as a dict if `ConfigDict` is passed
443
513
config = cfg
@@ -470,15 +540,19 @@ def get_custom_ecosystem_config(self, name: str) -> Optional[PluginConfig]:
470
540
from ape_ethereum import EthereumConfig
471
541
472
542
ethereum = cast (EthereumConfig , self .get_plugin_config ("ethereum" ))
473
- return ethereum .from_overrides (override )
543
+ return ethereum .from_overrides (
544
+ override , plugin_name = name , project_path = self ._project_path
545
+ )
474
546
475
547
return None
476
548
477
549
def get_unknown_config (self , name : str ) -> PluginConfig :
478
550
# This happens when a plugin is not installed but still configured.
479
551
result = (self .__pydantic_extra__ or {}).get (name , PluginConfig ())
480
552
if isinstance (result , dict ):
481
- return PluginConfig .from_overrides (result )
553
+ return PluginConfig .from_overrides (
554
+ result , plugin_name = name , project_path = self ._project_path
555
+ )
482
556
483
557
return result
484
558
0 commit comments