opencos-eda 0.2.48__py3-none-any.whl → 0.2.49__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. opencos/__init__.py +4 -2
  2. opencos/_version.py +10 -7
  3. opencos/commands/flist.py +8 -7
  4. opencos/commands/multi.py +13 -14
  5. opencos/commands/sweep.py +3 -2
  6. opencos/deps/__init__.py +0 -0
  7. opencos/deps/defaults.py +69 -0
  8. opencos/deps/deps_commands.py +419 -0
  9. opencos/deps/deps_file.py +326 -0
  10. opencos/deps/deps_processor.py +670 -0
  11. opencos/deps_schema.py +7 -8
  12. opencos/eda.py +84 -64
  13. opencos/eda_base.py +572 -316
  14. opencos/eda_config.py +80 -14
  15. opencos/eda_extract_targets.py +22 -14
  16. opencos/eda_tool_helper.py +33 -7
  17. opencos/export_helper.py +166 -86
  18. opencos/export_json_convert.py +31 -23
  19. opencos/files.py +2 -1
  20. opencos/hw/__init__.py +0 -0
  21. opencos/{oc_cli.py → hw/oc_cli.py} +9 -4
  22. opencos/names.py +0 -4
  23. opencos/peakrdl_cleanup.py +13 -7
  24. opencos/seed.py +19 -11
  25. opencos/tests/helpers.py +3 -2
  26. opencos/tests/test_deps_helpers.py +35 -32
  27. opencos/tests/test_eda.py +36 -29
  28. opencos/tests/test_eda_elab.py +5 -3
  29. opencos/tests/test_eda_synth.py +1 -1
  30. opencos/tests/test_oc_cli.py +1 -1
  31. opencos/tests/test_tools.py +3 -2
  32. opencos/tools/iverilog.py +2 -2
  33. opencos/tools/modelsim_ase.py +2 -2
  34. opencos/tools/riviera.py +1 -1
  35. opencos/tools/slang.py +1 -1
  36. opencos/tools/surelog.py +1 -1
  37. opencos/tools/verilator.py +1 -1
  38. opencos/tools/vivado.py +1 -1
  39. opencos/tools/yosys.py +4 -3
  40. opencos/util.py +374 -468
  41. opencos/utils/__init__.py +0 -0
  42. opencos/utils/markup_helpers.py +98 -0
  43. opencos/utils/str_helpers.py +111 -0
  44. opencos/utils/subprocess_helpers.py +108 -0
  45. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/METADATA +1 -1
  46. opencos_eda-0.2.49.dist-info/RECORD +88 -0
  47. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
  48. opencos/deps_helpers.py +0 -1346
  49. opencos_eda-0.2.48.dist-info/RECORD +0 -79
  50. /opencos/{pcie.py → hw/pcie.py} +0 -0
  51. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
  52. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
  53. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
  54. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,326 @@
1
+ '''deps_file -- functions and DepsFile class (for holding info and getting data from a
2
+ DEPS markup file.)
3
+
4
+ Performs no procesing.
5
+ '''
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+
11
+ import toml
12
+
13
+ from opencos.deps.defaults import DEPS_FILE_EXTS, ROOT_TABLE_KEYS_NOT_TARGETS
14
+ from opencos.util import debug, error
15
+ from opencos.utils.markup_helpers import yaml_safe_load, toml_load_only_root_line_numbers
16
+ from opencos.utils.str_helpers import fnmatch_or_re, dep_str2list
17
+ from opencos.utils.subprocess_helpers import subprocess_run_background
18
+
19
+
20
+ def deps_data_get_all_targets(data: dict) -> list:
21
+ '''Given extracted DEPS data (dict) get all the root level keys that aren't defaults'''
22
+ return [x for x in data.keys() if x not in ROOT_TABLE_KEYS_NOT_TARGETS]
23
+
24
+
25
+ def get_deps_markup_file(base_path: str) -> str:
26
+ '''Returns one of DEPS.yml, DEPS.yaml, DEPS.toml, DEPS.json'''
27
+ for suffix in DEPS_FILE_EXTS:
28
+ deps_file = os.path.join(base_path, 'DEPS' + suffix)
29
+ if os.path.isfile(deps_file):
30
+ return deps_file
31
+ return ''
32
+
33
+
34
+ def deps_markup_safe_load(
35
+ filepath: str, assert_return_types: tuple = (type(None), dict),
36
+ only_root_line_numbers: bool = False
37
+ ) -> dict:
38
+ '''Returns dict (may return {}) from filepath (str), errors if return type not in
39
+ assert_return_types.
40
+
41
+ (assert_return_types can be empty tuple, or any type to avoid check.)
42
+
43
+ only_root_line_numbers -- if True, will return a dict of {key: line number (int)} for
44
+ all the root level keys. Used for debugging DEPS.yml in
45
+ eda.CommandDesign.resolve_target_core
46
+ '''
47
+ data = {}
48
+ _, file_ext = os.path.splitext(filepath)
49
+ if file_ext in ['', '.yml', 'yaml']:
50
+ # treat DEPS as YAML.
51
+ data = yaml_safe_load(filepath=filepath, only_root_line_numbers=only_root_line_numbers)
52
+ elif file_ext == '.toml':
53
+ if only_root_line_numbers:
54
+ data = toml_load_only_root_line_numbers(filepath)
55
+ else:
56
+ data = toml.load(filepath)
57
+ elif file_ext == '.json':
58
+ if only_root_line_numbers:
59
+ data = {}
60
+ else:
61
+ with open(filepath, encoding='utf=8') as f:
62
+ data = json.load(f)
63
+
64
+ if assert_return_types and not isinstance(data, assert_return_types):
65
+ error(f'deps_markeup_safe_load: {filepath=} loaded type {type(data)=} is not in',
66
+ f'{assert_return_types=}')
67
+
68
+ return data
69
+
70
+
71
+ def get_all_targets( # pylint: disable=dangerous-default-value,too-many-locals,too-many-branches
72
+ dirs: list = [os.getcwd()],
73
+ base_path: str = os.getcwd(),
74
+ filter_str: str = '',
75
+ filter_using_multi: str = '',
76
+ error_on_empty_return: bool = True,
77
+ lstrip_path: bool = True
78
+ ) -> list:
79
+ '''Returns a list of [dir/target, ... ] using relpath from base_path
80
+
81
+ If using filter_using_multi (str), dirs (list) is not required. Example:
82
+ filter_using_multi='sim --tool vivado path/to/*test'
83
+ and filter_str is applied to all resulting targets.
84
+
85
+ If not using filter_using_multi, dirs is required, and filter_str is applied
86
+ To all targets from dirs.
87
+ '''
88
+
89
+ _path_lprefix = str(Path('.')) + os.path.sep
90
+
91
+ if filter_using_multi:
92
+ targets = []
93
+ orig_dir = os.path.abspath(os.getcwd())
94
+ os.chdir(base_path)
95
+ cmd_str = 'eda multi --quiet --print-targets ' + filter_using_multi
96
+ stdout, _, rc = subprocess_run_background(
97
+ work_dir='.', command_list=cmd_str.split()
98
+ )
99
+ os.chdir(orig_dir)
100
+ if rc != 0:
101
+ error(f'get_all_targets: {base_path=} {filter_using_multi=} {cmd_str=} returned:',
102
+ f'{rc=}, {stdout=}')
103
+
104
+ multi_filtered_targets = stdout.split()
105
+ if not filter_str:
106
+ targets = multi_filtered_targets
107
+ else:
108
+ targets = set()
109
+ for target in multi_filtered_targets:
110
+ this_dir, leaf_target = os.path.split(target)
111
+ if fnmatch_or_re(pattern=filter_str,
112
+ string=leaf_target):
113
+ t = os.path.join(os.path.relpath(this_dir, start=base_path), leaf_target)
114
+ if lstrip_path:
115
+ t = t.removeprefix(_path_lprefix)
116
+ targets.add(t)
117
+ targets = list(targets)
118
+ if not targets and error_on_empty_return:
119
+ error(f'get_all_targets: {base_path=} {filter_using_multi=} returned no targets')
120
+ return targets
121
+
122
+ targets = set()
123
+ for this_dir in dirs:
124
+ this_dir = os.path.join(base_path, this_dir)
125
+ deps_file = get_deps_markup_file(this_dir)
126
+ if not deps_file:
127
+ continue
128
+ data = deps_markup_safe_load(filepath=deps_file)
129
+
130
+ for leaf_target in deps_data_get_all_targets(data):
131
+ if not filter_str or fnmatch_or_re(pattern=filter_str,
132
+ string=leaf_target):
133
+ t = os.path.join(os.path.relpath(this_dir, start=base_path), leaf_target)
134
+ if lstrip_path:
135
+ t = t.removeprefix(_path_lprefix)
136
+ targets.add(t)
137
+
138
+ if not targets and error_on_empty_return:
139
+ error(f'get_all_targets: {base_path=} {dirs=} {filter_str=} returned no targets')
140
+ return list(targets)
141
+
142
+
143
+ def deps_target_get_deps_list(
144
+ entry, default_key: str = 'deps', target_node: str = '',
145
+ deps_file: str = '', entry_must_have_default_key: bool = False
146
+ ) -> list:
147
+ '''Given a DEPS table entry (str, list, dict) return the 'deps:' list'''
148
+
149
+ # For convenience, if key 'deps' in not in an entry, and entry is a list or string, then
150
+ # assume it's a list of deps
151
+ debug(f'{deps_file=} {target_node=}: {entry=} {default_key=}')
152
+ deps = []
153
+ if isinstance(entry, str):
154
+ deps = dep_str2list(entry)
155
+ elif isinstance(entry, list):
156
+ deps = entry # already a list
157
+ elif isinstance(entry, dict):
158
+
159
+ if entry_must_have_default_key:
160
+ assert default_key in entry, \
161
+ f'{target_node=} in {deps_file=} does not have a key for {default_key=} in {entry=}'
162
+ deps = entry.get(default_key, [])
163
+ deps = dep_str2list(deps)
164
+
165
+ # Strip commented out list entries, strip blank strings, preserve non-strings
166
+ ret = []
167
+ for dep in deps:
168
+ if isinstance(dep, str):
169
+ if dep.startswith('#') or dep == '':
170
+ continue
171
+ ret.append(dep)
172
+ return ret
173
+
174
+
175
+ def deps_list_target_sanitize(
176
+ entry, default_key: str = 'deps', target_node: str = '', deps_file: str = ''
177
+ ) -> dict:
178
+ '''Returns a sanitized DEPS markup table entry (dict --> dict)
179
+
180
+ Since we support target entries that can be dict, list, or str(), sanitize
181
+ them so they are a dict, with a key named 'deps' that has a list of deps.
182
+ '''
183
+ if isinstance(entry, dict):
184
+ return entry
185
+
186
+ if isinstance(entry, str):
187
+ mylist = dep_str2list(entry) # convert str to list
188
+ return {default_key: mylist}
189
+
190
+ if isinstance(entry, list):
191
+ # it's already a list
192
+ return {default_key: entry}
193
+
194
+ assert False, f"Can't convert to list {entry=} {default_key=} {target_node=} {deps_file=}"
195
+
196
+
197
+ class DepsFile:
198
+ '''A Container for a DEPS.yml or other Markup file
199
+
200
+ References the original CommandDesign object and its cache
201
+
202
+ Used for looking up a target, getting its line number in the original file, and
203
+ merging contents with a DEFAULTS key if present.
204
+ '''
205
+
206
+ def __init__( # pylint: disable=dangerous-default-value
207
+ self, command_design_ref: object, target_path: str, cache: dict = {}
208
+ ):
209
+ self.target_path = target_path
210
+ self.deps_file = get_deps_markup_file(target_path)
211
+ self.rel_deps_file = self.deps_file
212
+
213
+ if not self.deps_file:
214
+ # didn't find it, file doesn't exist.
215
+ self.data = {}
216
+ self.line_numbers = {}
217
+ elif self.deps_file in cache:
218
+ self.data = cache[self.deps_file].get('data', {})
219
+ self.line_numbers = cache[self.deps_file].get('line_numbers', {})
220
+ else:
221
+ self.data = deps_markup_safe_load(self.deps_file)
222
+ self.line_numbers = deps_markup_safe_load(self.deps_file, only_root_line_numbers=True)
223
+ cache[self.deps_file] = {
224
+ 'data': self.data,
225
+ 'line_numbers': self.line_numbers,
226
+ }
227
+
228
+ if self.deps_file:
229
+ deps_path, deps_leaf = os.path.split(self.deps_file)
230
+ if deps_path and os.path.exists(deps_path):
231
+ self.rel_deps_file = os.path.join(os.path.relpath(deps_path), deps_leaf)
232
+
233
+ self.error = command_design_ref.error # method.
234
+
235
+ def found(self) -> bool:
236
+ '''Returns true if this DEPS file exists and extracted non-empty data'''
237
+ return bool(self.deps_file) and bool(self.data)
238
+
239
+ def get_approx_line_number_str(self, target) -> str:
240
+ '''Given a full target name, get the approximate line numbers in the DEPS file if
241
+ available in self.line_numbers'''
242
+ _, target_node = os.path.split(target)
243
+ if not self.line_numbers:
244
+ return ''
245
+
246
+ return f'line={self.line_numbers.get(target_node, "")}'
247
+
248
+ def gen_caller_info(self, target: str) -> str:
249
+ '''Given a full target name (path/to/my_target) return caller_info str for debug'''
250
+ return '::'.join([
251
+ self.rel_deps_file,
252
+ target,
253
+ self.get_approx_line_number_str(target)
254
+ ])
255
+
256
+ def lookup(self, target_node: str, caller_info: str) -> bool:
257
+ '''Returns True if the target_node is in the DEPS markup file. If not, error with
258
+
259
+ some caller_info(str). This is more useful for YAML or TOML markup where we have
260
+ caller_info.
261
+ '''
262
+ if target_node not in self.data:
263
+ found_target = False
264
+ # For error printing, prefer relative paths:
265
+ t_path = os.path.relpath(self.target_path) + os.path.sep
266
+ t_node = target_node
267
+ t_full = os.path.join(t_path, t_node)
268
+ if not caller_info:
269
+ # If we don't have caller_info, likely came from command line (or DEPS JSON data):
270
+ if '.' in target_node:
271
+ # Likely a filename (target_node does not include path)
272
+ self.error(f'Trying to resolve command-line target={t_full} (file?):',
273
+ f'File={t_node} not found in directory={t_path}')
274
+ elif not self.rel_deps_file:
275
+ # target, but there's no DEPS file
276
+ self.error(f'Trying to resolve command-line target={t_full}:',
277
+ f'but path {t_path} has no DEPS markup file (DEPS.yml)')
278
+ else:
279
+ self.error(f'Trying to resolve command-line target={t_full}:',
280
+ f'was not found in deps_file={self.rel_deps_file},',
281
+ f'possible targets in deps file = {list(self.data.keys())}')
282
+ else:
283
+ # If we have caller_info, then this was a recursive call from another
284
+ # DEPS file. It should already have the useful error messaging:
285
+
286
+ if '.' in target_node:
287
+ # Likely a filename (target_node does not include path)
288
+ self.error(f'Trying to resolve target={t_full} (file?):',
289
+ f'called from {caller_info},',
290
+ f'File={t_node} not found in directory={t_path}')
291
+ elif not self.rel_deps_file:
292
+ # target, but there's no DEPS file
293
+ self.error(f'Trying to resolve target={t_full}:',
294
+ f'called from {caller_info},',
295
+ f'but {t_path} has no DEPS markup file (DEPS.yml)')
296
+ else:
297
+ self.error(f'Trying to resolve target={t_full}:',
298
+ f'called from {caller_info},',
299
+ f'Target not found in deps_file={self.rel_deps_file}')
300
+ else:
301
+ debug(f'Found {target_node=} in deps_file={self.rel_deps_file}')
302
+ found_target = True
303
+
304
+ return found_target
305
+
306
+
307
+ def get_entry(self, target_node) -> dict:
308
+ '''Returns the DEPS markup table "entry" (dict) for a target
309
+
310
+ This has DEFAULTS applied, and is converted to a dict if it wasn't already
311
+ '''
312
+
313
+ # Start with the defaults
314
+ entry = self.data.get('DEFAULTS', {}).copy()
315
+
316
+ # Lookup the entry from the DEPS dict:
317
+ entry_raw = self.data[target_node]
318
+
319
+ entry_sanitized = deps_list_target_sanitize(
320
+ entry_raw, target_node=target_node, deps_file=self.deps_file
321
+ )
322
+
323
+ # Finally update entry (defaults) with what we looked up:
324
+ entry.update(entry_sanitized)
325
+
326
+ return entry