@@ -45,14 +45,53 @@ def path(self):
45
45
@dataclass
46
46
class Value :
47
47
value : Any
48
- meta : Optional [ Meta ] = field (compare = False , default = None , repr = False )
48
+ meta : Meta = field (compare = False , repr = False )
49
49
50
50
def __repr__ (self ):
51
51
return f"'{ self } '"
52
52
53
53
def __str__ (self ) -> str :
54
54
return str (self .value )
55
55
56
+ def get_sources (self ):
57
+ return {self .meta .source : self .meta .path ()}
58
+
59
+
60
+ class String :
61
+ """
62
+ Wrapper around string, that can interpolate, and keep the
63
+ original source of those interpolations.
64
+ """
65
+
66
+ def __init__ (self , template , matches , context ):
67
+
68
+ from .interpolate import _resolve_value
69
+
70
+ index , buf = 0 , ""
71
+ self .meta = defaultdict (set )
72
+ for match in matches :
73
+ start , end = match .span (0 )
74
+ val = _resolve_value (match , context )
75
+ self ._add_source (val )
76
+ buf += template [index :start ] + str (val )
77
+ index = end
78
+ value = buf + template [index :]
79
+ self .value = value .replace (r"\${" , "${" )
80
+
81
+ def __repr__ (self ) -> str :
82
+ return str (self .value )
83
+
84
+ def _add_source (self , val : Union [Value , "String" ]):
85
+ # string might have been built from multiple sources
86
+ if isinstance (val , Value ) and val .meta and val .meta .source :
87
+ self .meta [val .meta .source ].add (val .meta .path ())
88
+ if isinstance (val , String ) and val .meta :
89
+ for source , keys in self .meta .items ():
90
+ self .meta [source ].update (keys )
91
+
92
+ def get_sources (self ):
93
+ return self .meta
94
+
56
95
57
96
class Container :
58
97
meta : Meta
@@ -66,7 +105,7 @@ def _convert(self, key, value):
66
105
meta = Meta .update_path (self .meta , key )
67
106
if value is None or isinstance (value , (int , float , str , bytes , bool )):
68
107
return Value (value , meta = meta )
69
- elif isinstance (value , (CtxList , CtxDict , Value )):
108
+ elif isinstance (value , (CtxList , CtxDict , Value , String )):
70
109
return value
71
110
elif isinstance (value , (list , dict )):
72
111
container = CtxDict if isinstance (value , dict ) else CtxList
@@ -108,6 +147,9 @@ def select(self, key: str):
108
147
) from exc
109
148
return d .select (rems [0 ]) if rems else d
110
149
150
+ def get_sources (self ):
151
+ return {}
152
+
111
153
112
154
class CtxList (Container , MutableSequence ):
113
155
_key_transform = staticmethod (int )
@@ -120,6 +162,9 @@ def __init__(self, values: Sequence, meta: Meta = None):
120
162
def insert (self , index : int , value ):
121
163
self .data .insert (index , self ._convert (index , value ))
122
164
165
+ def get_sources (self ):
166
+ return {self .meta .source : self .meta .path ()}
167
+
123
168
124
169
class CtxDict (Container , MutableMapping ):
125
170
def __init__ (self , mapping : Mapping = None , meta : Meta = None , ** kwargs ):
@@ -158,10 +203,15 @@ def track(self):
158
203
self ._track = False
159
204
160
205
def _track_data (self , node ):
161
- if isinstance (node , (Value , CtxList )):
162
- meta = node .meta
163
- if meta and meta .source and self ._track :
164
- self ._tracked_data [meta .source ].add (meta .path ())
206
+ if not self ._track :
207
+ return
208
+
209
+ for source , keys in node .get_sources ().items ():
210
+ if not source :
211
+ continue
212
+ params_file = self ._tracked_data [source ]
213
+ keys = [keys ] if isinstance (keys , str ) else keys
214
+ params_file .update (keys )
165
215
166
216
@property
167
217
def tracked (self ):
0 commit comments