Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
rewritten_yaml.py
1 # Copyright (c) 2019 Intel Corporation
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 from collections.abc import Generator
16 import tempfile
17 from typing import Optional, TypeAlias, Union
18 
19 import launch
20 import yaml
21 
22 YamlValue: TypeAlias = Union[str, int, float, bool]
23 
24 
26 
27  def __init__(self, dictionary: dict[str, YamlValue], key: str):
28  self.dictionarydictionary = dictionary
29  self.dictKeydictKey = key
30 
31  def key(self) -> str:
32  return self.dictKeydictKey
33 
34  def setValue(self, value: YamlValue) -> None:
35  self.dictionarydictionary[self.dictKeydictKey] = value
36 
37 
38 class RewrittenYaml(launch.Substitution):
39  """
40  Substitution that modifies the given YAML file.
41 
42  Used in launch system
43  """
44 
45  def __init__(
46  self,
47  source_file: launch.SomeSubstitutionsType,
48  param_rewrites: dict[str, launch.SomeSubstitutionsType],
49  root_key: Optional[launch.SomeSubstitutionsType] = None,
50  key_rewrites: Optional[dict[str, launch.SomeSubstitutionsType]] = None,
51  value_rewrites: Optional[dict[str, launch.SomeSubstitutionsType]] = None,
52  convert_types: bool = False,
53  out_dir: Optional[launch.SomeSubstitutionsType] = None,
54  ) -> None:
55  super().__init__()
56  """
57  Construct the substitution
58 
59  :param: source_file the original YAML file to modify
60  :param: param_rewrites mappings to replace
61  :param: root_key if provided, the contents are placed under this key
62  :param: key_rewrites keys of mappings to replace
63  :param: value_rewrites values to replace
64  :param: convert_types whether to attempt converting the string to a number or boolean
65  :param: out_dir if provided, the directory where the temporary YAML file will be created
66  """
67 
68  # import here to avoid loop
69  from launch.utilities import normalize_to_list_of_substitutions
70 
71  self.__source_file: list[launch.Substitution] = \
72  normalize_to_list_of_substitutions(source_file)
73  self.__param_rewrites__param_rewrites = {}
74  self.__key_rewrites__key_rewrites = {}
75  self.__value_rewrites__value_rewrites = {}
76  self.__convert_types__convert_types = convert_types
77  self.__root_key__root_key = None
78  self.__out_dir__out_dir = None
79 
80  for key in param_rewrites:
81  self.__param_rewrites__param_rewrites[key] = normalize_to_list_of_substitutions(
82  param_rewrites[key]
83  )
84  if key_rewrites is not None:
85  for key in key_rewrites:
86  self.__key_rewrites__key_rewrites[key] = normalize_to_list_of_substitutions(
87  key_rewrites[key]
88  )
89  if value_rewrites is not None:
90  for value in value_rewrites:
91  self.__value_rewrites__value_rewrites[value] = normalize_to_list_of_substitutions(
92  value_rewrites[value]
93  )
94  if root_key is not None:
95  self.__root_key__root_key = normalize_to_list_of_substitutions(root_key)
96 
97  if out_dir is not None:
98  self.__out_dir__out_dir = normalize_to_list_of_substitutions(out_dir)
99 
100  @property
101  def name(self) -> list[launch.Substitution]:
102  """Getter for name."""
103  return self.__source_file
104 
105  def describe(self) -> str:
106  """Return a description of this substitution as a string."""
107  return ''
108 
109  def perform(self, context: launch.LaunchContext) -> str:
110  yaml_filename = launch.utilities.perform_substitutions(context, self.namename)
111 
112  out_dir = None
113  if self.__out_dir__out_dir is not None:
114  out_dir = launch.utilities.perform_substitutions(context, self.__out_dir__out_dir)
115 
116  rewritten_yaml = tempfile.NamedTemporaryFile(mode='w', delete=False, dir=out_dir)
117  param_rewrites, keys_rewrites, value_rewrites = self.resolve_rewritesresolve_rewrites(context)
118 
119  with open(yaml_filename, 'r') as yaml_file:
120  data = yaml.safe_load(yaml_file)
121 
122  self.substitute_paramssubstitute_params(data, param_rewrites)
123  self.add_paramsadd_params(data, param_rewrites)
124  self.substitute_keyssubstitute_keys(data, keys_rewrites)
125  self.substitute_valuessubstitute_values(data, value_rewrites)
126  if self.__root_key__root_key is not None:
127  root_key = launch.utilities.perform_substitutions(context, self.__root_key__root_key)
128  if root_key:
129  data = {root_key: data}
130  yaml.dump(data, rewritten_yaml)
131  rewritten_yaml.close()
132  return rewritten_yaml.name
133 
134  def resolve_rewrites(self, context: launch.LaunchContext) -> \
135  tuple[dict[str, str], dict[str, str], dict[str, str]]:
136  resolved_params = {}
137  for key in self.__param_rewrites__param_rewrites:
138  resolved_params[key] = launch.utilities.perform_substitutions(
139  context, self.__param_rewrites__param_rewrites[key]
140  )
141  resolved_keys = {}
142  for key in self.__key_rewrites__key_rewrites:
143  resolved_keys[key] = launch.utilities.perform_substitutions(
144  context, self.__key_rewrites__key_rewrites[key]
145  )
146  resolved_values = {}
147  for value in self.__value_rewrites__value_rewrites:
148  resolved_values[value] = launch.utilities.perform_substitutions(
149  context, self.__value_rewrites__value_rewrites[value]
150  )
151  return resolved_params, resolved_keys, resolved_values
152 
153  def substitute_params(self, yaml: dict[str, YamlValue],
154  param_rewrites: dict[str, str]) -> None:
155  # substitute leaf-only parameters
156  for key in self.getYamlLeafKeysgetYamlLeafKeys(yaml):
157  if key.key() in param_rewrites:
158  raw_value = param_rewrites[key.key()]
159  key.setValue(self.convertconvert(raw_value))
160 
161  # substitute total path parameters
162  yaml_paths = self.pathifypathify(yaml)
163  for path in yaml_paths:
164  if path in param_rewrites:
165  # this is an absolute path (ex. 'key.keyA.keyB.val')
166  rewrite_val = self.convertconvert(param_rewrites[path])
167  yaml_keys = path.split('.')
168  yaml = self.updateYamlPathValsupdateYamlPathVals(yaml, yaml_keys, rewrite_val)
169 
170  def add_params(self, yaml: dict[str, YamlValue],
171  param_rewrites: dict[str, str]) -> None:
172  # add new total path parameters
173  yaml_paths = self.pathifypathify(yaml)
174  for path in param_rewrites:
175  if not path in yaml_paths: # noqa: E713
176  new_val = self.convertconvert(param_rewrites[path])
177  yaml_keys = path.split('.')
178  if 'ros__parameters' in yaml_keys:
179  yaml = self.updateYamlPathValsupdateYamlPathVals(yaml, yaml_keys, new_val)
180 
181  def substitute_values(
182  self, yaml: dict[str, YamlValue],
183  value_rewrites: dict[str, str]) -> None:
184 
185  def process_value(value: YamlValue) -> YamlValue:
186  if isinstance(value, dict):
187  for k, v in list(value.items()):
188  value[k] = process_value(v)
189  return value
190  elif isinstance(value, list):
191  return [process_value(v) for v in value]
192  elif str(value) in value_rewrites:
193  return self.convertconvert(value_rewrites[str(value)])
194  return value
195 
196  for key in list(yaml.keys()):
197  yaml[key] = process_value(yaml[key])
198 
199  def updateYamlPathVals(
200  self, yaml: dict[str, YamlValue],
201  yaml_key_list: list[str], rewrite_val: YamlValue) -> dict[str, YamlValue]:
202 
203  for key in yaml_key_list:
204  if key == yaml_key_list[-1]:
205  yaml[key] = rewrite_val
206  break
207  key = yaml_key_list.pop(0)
208  if isinstance(yaml, list):
209  yaml[int(key)] = self.updateYamlPathValsupdateYamlPathVals(
210  yaml[int(key)], yaml_key_list, rewrite_val
211  )
212  else:
213  yaml[key] = self.updateYamlPathValsupdateYamlPathVals( # type: ignore[assignment]
214  yaml.get(key, {}), # type: ignore[arg-type]
215  yaml_key_list,
216  rewrite_val
217  )
218  return yaml
219 
220  def substitute_keys(
221  self, yaml: dict[str, YamlValue], key_rewrites: dict[str, str]) -> None:
222  if len(key_rewrites) != 0:
223  for key in list(yaml.keys()):
224  val = yaml[key]
225  if key in key_rewrites:
226  new_key = key_rewrites[key]
227  yaml[new_key] = yaml[key]
228  del yaml[key]
229  if isinstance(val, dict):
230  self.substitute_keyssubstitute_keys(val, key_rewrites)
231 
232  def getYamlLeafKeys(self, yamlData: dict[str, YamlValue]) -> \
233  Generator[DictItemReference, None, None]:
234  if not isinstance(yamlData, dict):
235  return
236 
237  for key in yamlData.keys():
238  child = yamlData[key]
239 
240  if isinstance(child, dict):
241  # Recursively process nested dictionaries
242  yield from self.getYamlLeafKeysgetYamlLeafKeys(child)
243 
244  yield DictItemReference(yamlData, key)
245 
246  def pathify(
247  self, d: Union[dict[str, YamlValue], list[YamlValue], YamlValue],
248  p: Optional[str] = None,
249  paths: Optional[dict[str, YamlValue]] = None,
250  joinchar: str = '.') -> dict[str, YamlValue]:
251  if p is None:
252  paths = {}
253  self.pathifypathify(d, '', paths, joinchar=joinchar)
254  return paths
255 
256  assert paths is not None
257  pn = p
258  if p != '':
259  pn += joinchar
260  if isinstance(d, dict):
261  for k in d:
262  v = d[k]
263  self.pathifypathify(v, str(pn) + str(k), paths, joinchar=joinchar)
264  elif isinstance(d, list):
265  for idx, e in enumerate(d):
266  self.pathifypathify(e, pn + str(idx), paths, joinchar=joinchar)
267  else:
268  paths[p] = d
269  return paths
270 
271  def convert(self, text_value: str) -> YamlValue:
272  if self.__convert_types__convert_types:
273  # try converting to int or float
274  try:
275  return float(text_value) if '.' in text_value else int(text_value)
276  except ValueError:
277  pass
278 
279  # try converting to bool
280  if text_value.lower() == 'true':
281  return True
282  if text_value.lower() == 'false':
283  return False
284 
285  # nothing else worked so fall through and return text
286  return text_value
\ tuple[dict[str, str], dict[str, str], dict[str, str]] resolve_rewrites(self, launch.LaunchContext context)
\ Generator[DictItemReference, None, None] getYamlLeafKeys(self, dict[str, YamlValue] yamlData)
None add_params(self, dict[str, YamlValue] yaml, dict[str, str] param_rewrites)
None substitute_params(self, dict[str, YamlValue] yaml, dict[str, str] param_rewrites)
YamlValue convert(self, str text_value)
None substitute_keys(self, dict[str, YamlValue] yaml, dict[str, str] key_rewrites)
dict[str, YamlValue] pathify(self, Union[dict[str, YamlValue], list[YamlValue], YamlValue] d, Optional[str] p=None, Optional[dict[str, YamlValue]] paths=None, str joinchar='.')
dict[str, YamlValue] updateYamlPathVals(self, dict[str, YamlValue] yaml, list[str] yaml_key_list, YamlValue rewrite_val)
None substitute_values(self, dict[str, YamlValue] yaml, dict[str, str] value_rewrites)