opencos-eda 0.2.47__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 (55) 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 +35 -18
  5. opencos/commands/sweep.py +9 -4
  6. opencos/commands/waves.py +1 -1
  7. opencos/deps/__init__.py +0 -0
  8. opencos/deps/defaults.py +69 -0
  9. opencos/deps/deps_commands.py +419 -0
  10. opencos/deps/deps_file.py +326 -0
  11. opencos/deps/deps_processor.py +670 -0
  12. opencos/deps_schema.py +7 -8
  13. opencos/eda.py +92 -67
  14. opencos/eda_base.py +625 -332
  15. opencos/eda_config.py +80 -14
  16. opencos/eda_extract_targets.py +22 -14
  17. opencos/eda_tool_helper.py +33 -7
  18. opencos/export_helper.py +166 -86
  19. opencos/export_json_convert.py +31 -23
  20. opencos/files.py +2 -1
  21. opencos/hw/__init__.py +0 -0
  22. opencos/{oc_cli.py → hw/oc_cli.py} +9 -4
  23. opencos/names.py +0 -4
  24. opencos/peakrdl_cleanup.py +13 -7
  25. opencos/seed.py +19 -11
  26. opencos/tests/helpers.py +27 -14
  27. opencos/tests/test_deps_helpers.py +35 -32
  28. opencos/tests/test_eda.py +47 -41
  29. opencos/tests/test_eda_elab.py +5 -3
  30. opencos/tests/test_eda_synth.py +1 -1
  31. opencos/tests/test_oc_cli.py +1 -1
  32. opencos/tests/test_tools.py +3 -2
  33. opencos/tools/iverilog.py +2 -2
  34. opencos/tools/modelsim_ase.py +2 -2
  35. opencos/tools/riviera.py +1 -1
  36. opencos/tools/slang.py +1 -1
  37. opencos/tools/surelog.py +1 -1
  38. opencos/tools/verilator.py +1 -1
  39. opencos/tools/vivado.py +1 -1
  40. opencos/tools/yosys.py +4 -3
  41. opencos/util.py +440 -483
  42. opencos/utils/__init__.py +0 -0
  43. opencos/utils/markup_helpers.py +98 -0
  44. opencos/utils/str_helpers.py +111 -0
  45. opencos/utils/subprocess_helpers.py +108 -0
  46. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/METADATA +1 -1
  47. opencos_eda-0.2.49.dist-info/RECORD +88 -0
  48. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
  49. opencos/deps_helpers.py +0 -1346
  50. opencos_eda-0.2.47.dist-info/RECORD +0 -79
  51. /opencos/{pcie.py → hw/pcie.py} +0 -0
  52. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
  53. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
  54. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
  55. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/top_level.txt +0 -0
opencos/util.py CHANGED
@@ -1,5 +1,4 @@
1
-
2
- # SPDX-License-Identifier: MPL-2.0
1
+ '''opencos.util -- support global logging, argparser for printing (colors)'''
3
2
 
4
3
  import sys
5
4
  import subprocess
@@ -10,22 +9,16 @@ import atexit
10
9
  import shutil
11
10
  import traceback
12
11
  import argparse
13
- import yaml
14
12
  import re
15
- import textwrap
16
-
17
- from opencos import eda_config
18
13
 
19
- global_exit_allowed = False
20
- progname = "UNKNOWN"
21
- progname_in_message = True
22
- logfile = None
23
- loglast = 0
24
- debug_level = 0
14
+ from importlib import import_module
25
15
 
26
- EDA_OUTPUT_CONFIG_FNAME = 'eda_output_config.yml'
16
+ global_exit_allowed = False # pylint: disable=invalid-name
17
+ progname = "UNKNOWN" # pylint: disable=invalid-name
18
+ progname_in_message = True # pylint: disable=invalid-name
19
+ debug_level = 0 # pylint: disable=invalid-name
27
20
 
28
- args = {
21
+ args = { # pylint: disable=invalid-name
29
22
  'color' : False,
30
23
  'quiet' : False,
31
24
  'verbose' : False,
@@ -35,149 +28,147 @@ args = {
35
28
  'errors' : 0,
36
29
  }
37
30
 
38
- def strip_all_quotes(s: str) -> str:
39
- return s.replace("'", '').replace('"', '')
40
-
41
- def strip_outer_quotes(s: str) -> str:
42
- ret = str(s)
43
- while (ret.startswith("'") and ret.endswith("'")) or \
44
- (ret.startswith('"') and ret.endswith('"')):
45
- ret = ret[1:-1]
46
- return ret
47
-
48
-
49
- def yaml_load_only_root_line_numbers(filepath:str):
50
- '''Returns a dict of {key: int line number}, very crude'''
51
- # Other solutions aren't as attractive, require a lot of mappers to get
52
- # line numbers on returned values that aren't dict
53
- data = None
54
- with open(filepath) as f:
55
- try:
56
- # Try to do a very lazy parse of root level keys only, returns dict{key:lineno}
57
- data = dict()
58
- for lineno,line in enumerate(f.readlines()):
59
- m = re.match(r'^(\w+):', line)
60
- if m:
61
- key = m.group(1)
62
- data[key] = lineno + 1
63
- except Exception as e:
64
- error(f"Error loading YAML {filepath=}:", e)
65
- return data
66
-
31
+ class Colors:
32
+ '''Namespace class for color printing help'''
33
+ red = "\x1B[31m"
34
+ green = "\x1B[32m"
35
+ orange = "\x1B[33m"
36
+ yellow = "\x1B[39m"
37
+ normal = "\x1B[0m"
38
+
39
+ @staticmethod
40
+ def color_text(text: str, color: str) -> str:
41
+ '''Wraps 'text' (str) with color (one of red|green|orange|yellow) prefix and
42
+
43
+ color (normal) suffix. Disables color prefix/suffix wrapping if args['color']=False
44
+ '''
45
+ if args['color']:
46
+ return color + text + "\x1B[0m" # (normal)
47
+ return text
67
48
 
68
- def toml_load_only_root_line_numbers(filepath:str):
69
- '''Returns a dict of {key: int line number}, very crude'''
70
- data = None
71
- with open(filepath) as f:
49
+ def red_text(text: str) -> str:
50
+ '''Wraps text for printing as red, disabled if global args['color']=False'''
51
+ if args['color']:
52
+ return Colors.red + text + Colors.normal
53
+ return text
54
+
55
+ def green_text(text: str) -> str:
56
+ '''Wraps text for printing as green, disabled if global args['color']=False'''
57
+ if args['color']:
58
+ return Colors.green + text + Colors.normal
59
+ return text
60
+
61
+ def orange_text(text: str) -> str:
62
+ '''Wraps text for printing as orange, disabled if global args['color']=False'''
63
+ if args['color']:
64
+ return Colors.orange + text + Colors.normal
65
+ return text
66
+
67
+ def yellow_text(text: str) -> str:
68
+ '''Wraps text for printing as yellow, disabled if global args['color']=False'''
69
+ if args['color']:
70
+ return Colors.yellow + text + Colors.normal
71
+ return text
72
+
73
+
74
+ class UtilLogger:
75
+ '''Class for the util.global_log'''
76
+ file = None
77
+ filepath = ''
78
+ time_last = 0 #timestamp via time.time()
79
+
80
+ # disabled by default, eda.py enables it. Can also be disabled via
81
+ # util's argparser: --no-default-log, --logfile=<name>, or --force-logfile=<name>
82
+ default_log_enabled = False
83
+ default_log_filepath = os.path.join('eda.work', 'eda.log')
84
+
85
+ def clear(self) -> None:
86
+ '''Resets internals'''
87
+ self.file = None
88
+ self.filepath = ''
89
+ self.time_last = 0
90
+
91
+ def stop(self) -> None:
92
+ '''Closes open log, resets internals'''
93
+ if self.file:
94
+ self.write_timestamp(f'stop - {self.filepath}')
95
+ info(f"Closing logfile: {self.filepath}")
96
+ self.file.close()
97
+ self.clear()
98
+
99
+ def start(self, filename: str, force: bool = False) -> None:
100
+ '''Starts (opens) log'''
101
+ if not filename:
102
+ error('Trying to start a logfile, but filename is missing')
103
+ return
104
+ if os.path.exists(filename):
105
+ if force:
106
+ debug(f"Overwriting logfile '{filename}', which exists, due to --force-logfile.")
107
+ else:
108
+ error(f"The --logfile path '{filename}' exists. Use --force-logfile",
109
+ "(vs --logfile) to override.")
110
+ return
111
+ else:
112
+ safe_mkdir_for_file(filename)
72
113
  try:
73
- data = dict()
74
- for lineno, line in enumerate(f.readlines()):
75
- m = re.match(r'^\[(\w+)\]', line)
76
- if m:
77
- key = m.group(1)
78
- data[key] = lineno + 1
114
+ self.file = open(filename, 'w', encoding='utf-8') # pylint: disable=consider-using-with
115
+ debug(f"Opened logfile '{filename}' for writing")
116
+ self.filepath = filename
117
+ self.write_timestamp(f'start - {self.filepath}')
79
118
  except Exception as e:
80
- error(f'Error loading TOML {filepath=}', e)
81
- return data
82
-
83
-
84
- def yaml_safe_load(filepath:str, only_root_line_numbers=False):
85
- '''Returns dict or None from filepath (str), errors if return type not in assert_return_types.
119
+ error(f"Error opening '{filename}' for writing, {e}")
120
+ self.clear()
86
121
 
87
- (assert_return_types can be empty list to avoid check.)
88
-
89
- only_root_line_numbers -- if True, will return a dict of {key: line number (int)} for
90
- all the root level keys. Used for debugging DEPS.yml in
91
- eda.CommandDesign.resolve_target_core
92
- '''
93
-
94
- data = None
122
+ def write_timestamp(self, text: str = "") -> None:
123
+ '''Writes timestamp to opened log'''
124
+ if not self.file:
125
+ return
126
+ dt = datetime.datetime.now().ctime()
127
+ print(f"INFO: [{progname}] Time: {dt} {text}", file=self.file)
128
+ self.time_last = time.time()
95
129
 
96
- if only_root_line_numbers:
97
- return yaml_load_only_root_line_numbers(filepath)
130
+ def write(self, text: str, end: str = '\n') -> None:
131
+ '''Writes text to opened log'''
132
+ if not self.file:
133
+ return
98
134
 
99
- with open(filepath) as f:
100
- debug(f'Opening {filepath=}')
101
- try:
102
- data = yaml.safe_load(f)
103
- except yaml.YAMLError as e:
104
-
105
- # if yamllint is installed, then use it to get all errors in the .yml|.yaml
106
- # file, instead of the single exception.
107
- if shutil.which('yamllint'):
108
- try:
109
- sp_out = subprocess.run(
110
- f'yamllint -d relaxed --no-warnings {filepath}'.split(),
111
- capture_output=True, text=True )
112
- for x in sp_out.stdout.split('\n'):
113
- if x:
114
- info('yamllint: ' + x)
115
- except:
116
- pass
117
-
118
- if hasattr(e, 'problem_mark'):
119
- mark = e.problem_mark
120
- error(f"Error parsing {filepath=}: line {mark.line + 1},",
121
- f"column {mark.column +1}: {e.problem}")
122
- else:
123
- error(f"Error loading YAML {filepath=}:", e)
124
- except Exception as e:
125
- error(f"Error loading YAML {filepath=}:", e)
135
+ if ((time.time() - self.time_last) > 10) and \
136
+ any(text.startswith(x) for x in [
137
+ f"DEBUG: [{progname}]",
138
+ f"INFO: [{progname}]",
139
+ f"WARNING: [{progname}]",
140
+ f"ERROR: [{progname}]"]):
141
+ self.write_timestamp()
142
+ print(text, end=end, file=self.file)
143
+ self.file.flush()
144
+ os.fsync(self.file)
126
145
 
127
- return data
128
146
 
147
+ global_log = UtilLogger()
129
148
 
130
- def yaml_safe_writer(data:dict, filepath:str) -> None:
131
149
 
132
- if filepath.endswith('.yml') or filepath.endswith('.yaml'):
133
- with open(filepath, 'w', encoding='utf-8') as f:
134
- yaml.dump(data, f, allow_unicode=True,
135
- default_flow_style=False, sort_keys=False, encoding=('utf-8'))
136
- else:
137
- warning(f'{filepath=} to be written for this extension not implemented.')
150
+ def start_log(filename: str, force: bool = False) -> None:
151
+ '''Starts the global_log, if not already started'''
152
+ global_log.start(filename=filename, force=force)
138
153
 
154
+ def write_log(text: str, end: str = '\n') -> None:
155
+ '''Writes to the global_log, if started'''
156
+ global_log.write(text=text, end=end)
139
157
 
158
+ def stop_log() -> None:
159
+ '''Stops/closed the global_log'''
160
+ global_log.stop()
140
161
 
141
- def start_log(filename, force=False):
142
- global logfile, loglast
143
- if os.path.exists(filename):
144
- if force:
145
- info(f"Overwriting '{filename}', which exists, due to --force-logfile.")
146
- else:
147
- error(f"The --logfile path '{filename}' exists. Use --force-logfile (vs --logfile) to override.")
148
- try:
149
- logfile = open( filename, 'w')
150
- debug(f"Opened logfile '{filename}' for writing")
151
- except Exception as e:
152
- error(f"Error opening '{filename}' for writing!")
153
-
154
- def write_log(text, end):
155
- global logfile, loglast
156
- sw = text.startswith(f"INFO: [{progname}]")
157
- if (((time.time() - loglast) > 10) and
158
- (text.startswith(f"DEBUG: [{progname}]") or
159
- text.startswith(f"INFO: [{progname}]") or
160
- text.startswith(f"WARNING: [{progname}]") or
161
- text.startswith(f"ERROR: [{progname}]"))):
162
- dt = datetime.datetime.now().ctime()
163
- print(f"INFO: [{progname}] Time: {dt}", file=logfile)
164
- loglast = time.time()
165
- print(text, end=end, file=logfile)
166
- logfile.flush()
167
- os.fsync(logfile)
168
-
169
- def stop_log():
170
- global logfile, loglast
171
- if logfile:
172
- debug(f"Closing logfile")
173
- logfile.close()
174
- logfile = None
175
- loglast = 0
176
162
 
177
163
  atexit.register(stop_log)
178
164
 
165
+
179
166
  def get_argparse_bool_action_kwargs() -> dict:
180
- bool_kwargs = dict()
167
+ '''Returns args for BooleanOptionalAction kwargs for an ArgumentParser.add_argument
168
+
169
+ This is mostly for python compatibility to support --some-enable and --no-some-enable
170
+ '''
171
+ bool_kwargs = {}
181
172
  x = getattr(argparse, 'BooleanOptionalAction', None)
182
173
  if x is not None:
183
174
  bool_kwargs['action'] = x
@@ -185,8 +176,12 @@ def get_argparse_bool_action_kwargs() -> dict:
185
176
  bool_kwargs['action'] = 'store_true'
186
177
  return bool_kwargs
187
178
 
179
+
188
180
  def get_argparser() -> argparse.ArgumentParser:
189
- parser = argparse.ArgumentParser(prog='opencos common options', add_help=False, allow_abbrev=False)
181
+ '''Returns the opencos.util ArgumentParser'''
182
+ parser = argparse.ArgumentParser(
183
+ prog='opencos common options', add_help=False, allow_abbrev=False
184
+ )
190
185
  # We set allow_abbrev=False so --force-logfile won't try to attempt parsing shorter similarly
191
186
  # named args like --force, we want those to go to unparsed list.
192
187
  # For bools, support --color and --no-color with this action=argparse.BooleanOptionalAction
@@ -205,15 +200,24 @@ def get_argparser() -> argparse.ArgumentParser:
205
200
  help='Display additional debug messaging level 1 or higher')
206
201
  parser.add_argument('--debug-level', type=int, default=0,
207
202
  help='Set debug level messaging (default: 0)')
208
- parser.add_argument('--logfile', type=str, default='',
209
- help='Write eda messaging to logfile (default disabled)')
210
- parser.add_argument('--force-logfile', type=str, default='',
203
+ parser.add_argument('--logfile', type=str, default=None,
204
+ help=('Write eda messaging to safe logfile that will not be overwritten'
205
+ ' (default disabled)'))
206
+ parser.add_argument('--force-logfile', type=str, default=None,
211
207
  help='Set to force overwrite the logfile')
208
+ parser.add_argument('--default-log', **bool_action_kwargs,
209
+ default=global_log.default_log_enabled,
210
+ help=('Enable/Disable default logging to'
211
+ f' {global_log.default_log_filepath}. Default logging is disabled'
212
+ ' if --logfile or --force-logfile is set'))
212
213
  parser.add_argument('--no-respawn', action='store_true',
213
- help='Legacy mode (default respawn disabled) for respawning eda.py using $OC_ROOT/bin')
214
+ help=('Legacy mode (default respawn disabled) for respawning eda.py'
215
+ ' using $OC_ROOT/bin'))
214
216
  return parser
215
217
 
216
- def get_argparser_short_help(parser=None) -> str:
218
+
219
+ def get_argparser_short_help(parser: object = None) -> str:
220
+ '''Returns short help for our ArgumentParser'''
217
221
  if not parser:
218
222
  parser = get_argparser()
219
223
  full_lines = parser.format_help().split('\n')
@@ -225,18 +229,25 @@ def get_argparser_short_help(parser=None) -> str:
225
229
  return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
226
230
 
227
231
 
228
- def process_token(arg:str) -> bool:
229
- # This is legacy holdover for oc_cli.py, that would process one token at a time.
230
- # Simply run through our full argparser.
231
- parsed, unparsed = process_tokens(tokens[arg])
232
- if len(unparsed) == 0:
233
- debug(f"Processed command: {arg}")
232
+ def process_token(arg: list) -> bool:
233
+ '''Returns true if we parsed arg (list, because we may pass --arg value)
234
+
235
+ This is legacy holdover for oc_cli.py, that would process one token at a time.
236
+ Simply run through our full argparser.
237
+ '''
238
+ _, unparsed = process_tokens(arg)
239
+ if not unparsed: # empty list
240
+ debug(f"Processed token or pair: {arg}")
234
241
  return True
235
242
  return False
236
243
 
237
244
 
238
245
  def process_tokens(tokens:list) -> (argparse.Namespace, list):
239
- global debug_level
246
+ '''Processes tokens (unparsed args list) on util's ArgumentParser
247
+
248
+ Returns tuple of (parsed Namespace, unparsed args list)
249
+ '''
250
+ global debug_level # pylint: disable=global-statement
240
251
 
241
252
  parser = get_argparser()
242
253
  try:
@@ -245,251 +256,316 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
245
256
  except argparse.ArgumentError:
246
257
  error(f'problem attempting to parse_known_args for {tokens=}')
247
258
 
248
- if parsed.debug_level: set_debug_level(parsed.debug_level)
249
- elif parsed.debug: set_debug_level(1)
250
- else: debug_level = 0
259
+ if parsed.debug_level:
260
+ set_debug_level(parsed.debug_level)
261
+ elif parsed.debug:
262
+ set_debug_level(1)
263
+ else:
264
+ debug_level = 0
251
265
 
252
266
  debug(f'util.process_tokens: {parsed=} {unparsed=} from {tokens=}')
253
267
 
254
- if parsed.force_logfile != '':
268
+ if parsed.force_logfile:
255
269
  start_log(parsed.force_logfile, force=True)
256
- elif parsed.logfile != '':
270
+ elif parsed.logfile:
257
271
  start_log(parsed.logfile, force=False)
272
+ elif parsed.default_log and \
273
+ (parsed.force_logfile is None and parsed.logfile is None):
274
+ # Use a forced logfile in the eda.work/eda.log:
275
+ start_log(global_log.default_log_filepath, force=True)
276
+
258
277
 
259
278
  parsed_as_dict = vars(parsed)
260
279
  for key,value in parsed_as_dict.items():
261
280
  if value is not None:
262
281
  args[key] = value # only update with non-None values to our global 'args' dict
263
- return parsed, unparsed
264
282
 
265
- def indent_wrap_long_text(text, width=80, initial_indent=0, indent=4):
266
- """Returns str, wraps text to a specified width and indents subsequent lines."""
267
- wrapped_lines = textwrap.wrap(text, width=width,
268
- initial_indent=' ' * initial_indent,
269
- subsequent_indent=' ' * indent)
270
- return '\n'.join(wrapped_lines)
283
+ return parsed, unparsed
271
284
 
272
285
  # ********************
273
286
  # fancy support
274
- # In fancy mode, we take the bottom fancy_lines_ lines of the screen to be written using fancy_print,
275
- # while the lines above that show regular scrolling content (via info, debug, warning, error above).
276
- # User should not use print() when in fancy mode
277
-
278
- fancy_lines_ = []
279
-
280
- def fancy_start(fancy_lines = 4, min_vanilla_lines = 4):
281
- global fancy_lines_
282
- (columns,lines) = shutil.get_terminal_size()
283
- if (fancy_lines < 2):
284
- error(f"Fancy mode requires at least 2 fancy lines")
285
- if (fancy_lines > (lines-min_vanilla_lines)):
286
- error(f"Fancy mode supports at most {(lines-min_vanilla_lines)} fancy lines, given {min_vanilla_lines} non-fancy lines")
287
- if len(fancy_lines_): error(f"We are already in fancy line mode??")
288
- for _ in range(fancy_lines-1):
289
- print("") # create the requisite number of blank lines
290
- fancy_lines_.append("")
287
+ # In fancy mode, we take the bottom _FANCY_LINES lines of the screen to be written using
288
+ # fancy_print, while the lines above that show regular scrolling content (via info, debug, warning,
289
+ # error above). User should not use print() when in fancy mode
290
+
291
+ _FANCY_LINES = []
292
+
293
+ def fancy_start(fancy_lines: int = 4, min_vanilla_lines: int = 4) -> None:
294
+ '''Starts fancy line support. This is not called by util internally
295
+
296
+ It is called by an opencos.eda Command handling class, it can check if util.args['fancy']
297
+ is set
298
+ '''
299
+ _, lines = shutil.get_terminal_size()
300
+ if fancy_lines < 2:
301
+ error("Fancy mode requires at least 2 fancy lines")
302
+ if fancy_lines > (lines - min_vanilla_lines):
303
+ error(f"Fancy mode supports at most {(lines - min_vanilla_lines)} fancy lines, given",
304
+ f"{min_vanilla_lines} non-fancy lines")
305
+ if _FANCY_LINES:
306
+ error("We are already in fancy line mode, cannot call fancy_start() again")
307
+ for _ in range(fancy_lines - 1):
308
+ print() # create the requisite number of blank lines
309
+ _FANCY_LINES.append("")
291
310
  print("", end="") # the last line has no "\n" because we don't want ANOTHER blank line below
292
- fancy_lines_.append("")
311
+ _FANCY_LINES.append("")
293
312
  # the cursor remains at the leftmost character of the bottom line of the screen
294
313
 
295
- def fancy_stop():
296
- global fancy_lines_
297
- if len(fancy_lines_): # don't do anything if we aren't in fancy mode
298
- # user is expected to have painted something into the fancy lines, we can't "pull down" the regular
299
- # lines above, and we don't want fancy_lines_ blank or garbage lines either, that's not pretty
300
- fancy_lines_ = []
301
- # since cursor is always left at the leftmost character of the bottom line of the screen, which was
302
- # one of the fancy lines which now has the above-mentioned "something", we want to move one lower
303
- print("")
304
-
305
- def fancy_print(text, line):
306
- global fancy_lines_
314
+ def fancy_stop() -> None:
315
+ '''Stops fancy mode. Intended to be called by an opencos.eda Command handling class'''
316
+ if not _FANCY_LINES:
317
+ # don't do anything if we aren't in fancy mode.
318
+ return
319
+
320
+ # user is expected to have painted something into the fancy lines, we can't "pull down"
321
+ # the regular lines above, and we don't want _FANCY_LINES blank or garbage lines either,
322
+ # that's not pretty
323
+ _FANCY_LINES.clear()
324
+ # since cursor is always left at the leftmost character of the bottom line of the screen,
325
+ # which was one of the fancy lines which now has the above-mentioned "something", we want
326
+ # to move one lower
327
+ print()
328
+
329
+
330
+ def fancy_print(text: str, line: int) -> None:
331
+ '''Fancy print, intended to be called by an opencos.eda Command handling class'''
332
+
307
333
  # strip any newline, we don't want to print that
308
- if text.endswith("\n"): text.rstrip()
309
- lines_above = len(fancy_lines_) - line - 1
334
+ text = text.rstrip("\n")
335
+ lines_above = len(_FANCY_LINES) - line - 1
310
336
  if lines_above:
311
- print(f"\033[{lines_above}A"+ # move cursor up
312
- text+f"\033[1G"+ # desired text, then move cursor to the first character of the line
313
- f"\033[{lines_above}B", # move the cursor down
314
- end="", flush=True)
337
+ print(
338
+ (f"\033[{lines_above}A" # move cursor up
339
+ f"{text}\033[1G" # desired text, then move cursor to the first character of the line
340
+ f"\033[{lines_above}B" # move the cursor down
341
+ ),
342
+ end="", flush=True
343
+ )
315
344
  else:
316
- print(text+f"\033[1G", # desired text, then move cursor to the first character of the line
317
- end="", flush=True)
318
- fancy_lines_[line] = text
319
-
320
- def print_pre():
345
+ print(
346
+ f"{text}\033[1G", # desired text, then move cursor to the first character of the line
347
+ end="", flush=True
348
+ )
349
+ _FANCY_LINES[line] = text
350
+
351
+ def print_pre() -> None:
352
+ '''called by all util info/warning/debug/error. Handles fancy mode'''
321
353
  # stuff we do before printing any line
322
- if len(fancy_lines_):
323
- # Also, note that in fancy mode we don't allow the "above lines" to be partially written, they
324
- # are assumed to be full lines ending in "\n"
325
- # As always, we expect the cursor was left in the leftmost character of bottom line of screen
326
- print(f"\033[{len(fancy_lines_)-1}A"+ # move the cursor up to where the first fancy line is drawn
327
- f"\033[0K", # clear the old fancy line 0
328
- end="",flush=True)
329
-
330
- def print_post(text, end):
354
+ if not _FANCY_LINES:
355
+ return
356
+
357
+ # Also, note that in fancy mode we don't allow the "above lines" to be partially written, they
358
+ # are assumed to be full lines ending in "\n"
359
+ # As always, we expect the cursor was left in the leftmost character of bottom line of screen
360
+ print(
361
+ (f"\033[{len(_FANCY_LINES)-1}A" # move the cursor up to where the first fancy line is drawn
362
+ f"\033[0K" # clear the old fancy line 0
363
+ ),
364
+ end="", flush=True
365
+ )
366
+
367
+ def print_post(text: str, end: str) -> None:
368
+ '''called by all util info/warning/debug/error. Handles fancy mode'''
331
369
  # stuff we do after printing any line
332
- if len(fancy_lines_):
333
- #time.sleep(1)
334
- # we just printed a line, including a new line, on top of where fancy line 0 used to be, so cursor
335
- # is now at the start of fancy line 1.
336
- # move cursor down to the beginning of the final fancy line (i.e. standard fancy cursor resting place)
337
- for x in range(len(fancy_lines_)):
338
- print("\033[0K",end="") # erase the line to the right
339
- print(fancy_lines_[x],flush=True,end=('' if x==(len(fancy_lines_)-1) else '\n'))
340
- #time.sleep(1)
370
+ if _FANCY_LINES:
371
+ # we just printed a line, including a new line, on top of where fancy line 0 used to be,
372
+ # so cursor is now at the start of fancy line 1. move cursor down to the beginning of the
373
+ # final fancy line (i.e. standard fancy cursor resting place)
374
+ for x, line in enumerate(_FANCY_LINES):
375
+ print("\033[0K", end="") # erase the line to the right
376
+ print(line, flush=True,
377
+ end=('' if x == (len(_FANCY_LINES) - 1) else '\n'))
378
+
341
379
  print("\033[1G", end="", flush=True)
342
- if logfile: write_log(text, end=end)
380
+ if global_log.file:
381
+ write_log(text, end=end)
343
382
 
344
- string_red = f"\x1B[31m"
345
- string_green = f"\x1B[32m"
346
- string_orange = f"\x1B[33m"
347
- string_yellow = f"\x1B[39m"
348
- string_normal = f"\x1B[0m"
349
383
 
350
- def print_red(text, end='\n'):
384
+ def print_color(text: str, color: str, end: str = '\n') -> None:
385
+ '''Note that color(str) must be one of Colors.[red|green|orange|yellow|normal]'''
351
386
  print_pre()
352
- print(f"{string_red}{text}{string_normal}" if args['color'] else f"{text}", end=end, flush=True)
387
+ print(Colors.color_text(text, color), end=end, flush=True)
353
388
  print_post(text, end)
354
389
 
355
- def print_green(text, end='\n'):
390
+ def print_red(text: str, end: str = '\n') -> None:
391
+ '''Print text as red, goes back to normal color'''
356
392
  print_pre()
357
- print(f"{string_green}{text}{string_normal}" if args['color'] else f"{text}", end=end, flush=True)
393
+ print(red_text(text), end=end, flush=True)
358
394
  print_post(text, end)
359
395
 
360
- def print_orange(text, end='\n'):
396
+ def print_green(text: str, end: str = '\n') -> None:
397
+ '''Print text as green, goes back to normal color'''
361
398
  print_pre()
362
- print(f"{string_orange}{text}{string_normal}" if args['color'] else f"{text}", end=end, flush=True)
399
+ print(green_text(text), end=end, flush=True)
363
400
  print_post(text, end)
364
401
 
365
- def print_yellow(text, end='\n'):
402
+ def print_orange(text: str, end: str = '\n') -> None:
403
+ '''Print text as orange, goes back to normal color'''
366
404
  print_pre()
367
- print(f"{string_yellow}{text}{string_normal}" if args['color'] else f"{text}", end=end, flush=True)
405
+ print(orange_text(text), end=end, flush=True)
368
406
  print_post(text, end)
369
407
 
370
- def set_debug_level(level):
371
- global debug_level
408
+ def print_yellow(text: str, end: str = '\n') -> None:
409
+ '''Print text as yellow, goes back to normal color'''
410
+ print_pre()
411
+ print(yellow_text(text), end=end, flush=True)
412
+ print_post(text, end)
413
+
414
+
415
+ def set_debug_level(level) -> None:
416
+ '''Sets global debug level, sets args['debug'] and args['verbose']'''
417
+ global debug_level # pylint: disable=global-statement
372
418
  debug_level = level
373
- args['debug'] = (level > 0)
374
- args['verbose'] = (level > 1)
419
+ args['debug'] = level > 0
420
+ args['verbose'] = level > 1
375
421
  info(f"Set debug level to {debug_level}")
376
422
 
377
- # the <<d>> stuff is because we change progname after this is read in. if we instead infer progname or
378
- # get it passed somehow, we can avoid this ugliness / performance impact (lots of calls to debug happen)
379
- def debug(*text, level=1, start='<<d>>', end='\n'):
380
- if start=='<<d>>': start = f"DEBUG: " + (f"[{progname}] " if progname_in_message else "")
381
- if args['debug'] and (((level==1) and args['verbose']) or (debug_level >= level)):
423
+
424
+ def debug(*text, level: int = 1, start: object = None, end: str = '\n') -> None:
425
+ '''Print debug messaging (in yellow if possible). If args['debug'] is false, prints nothing.
426
+
427
+ *text: (positional str args) to be printed
428
+ level: (int) debug level to decide if printed or not.
429
+ start: (optional str) prefix to message; if None: chooses default start str
430
+ end: (optional str) suffix to print
431
+
432
+ Note these messages append to global logging (but require args['debug'] to be set)
433
+ '''
434
+ if start is None:
435
+ start = "DEBUG: " + (f"[{progname}] " if progname_in_message else "")
436
+ if args['debug'] and \
437
+ (((level==1) and args['verbose']) or (debug_level >= level)):
382
438
  print_yellow(f"{start}{' '.join(list(text))}", end=end)
383
439
 
384
- def info(*text, start='<<d>>', end='\n'):
385
- if start=='<<d>>': start = f"INFO: " + (f"[{progname}] " if progname_in_message else "")
440
+
441
+ def info(*text, start: object = None, end='\n') -> None:
442
+ '''Print information messaging (in green if possible). If args['quiet'], prints nothing.
443
+
444
+ *text: (positional str args) to be printed
445
+ start: (optional str) prefix to message; if None: chooses default start str
446
+ end: (optional str) suffix to print
447
+
448
+ Note these messages append to global logging even if args['quiet'] is set
449
+ '''
450
+ if start is None:
451
+ start = "INFO: " + (f"[{progname}] " if progname_in_message else "")
386
452
  if not args['quiet']:
387
453
  print_green(f"{start}{' '.join(list(text))}", end=end)
388
454
 
389
- def warning(*text, start='<<d>>', end='\n'):
390
- if start=='<<d>>': start = f"WARNING: " + (f"[{progname}] " if progname_in_message else "")
455
+ def warning(*text, start: object = None, end: str = '\n') -> None:
456
+ '''Print warning messaging (in orange if possible).
457
+
458
+ *text: (positional str args) to be printed
459
+ start: (optional str) prefix to message; if None: chooses default start str
460
+ end: (optional str) suffix to print
461
+
462
+ Note these messages append to global logging. Increments global args['warnings'] int.
463
+ '''
464
+ if start is None:
465
+ start = "WARNING: " + (f"[{progname}] " if progname_in_message else "")
391
466
  args['warnings'] += 1
392
467
  print_orange(f"{start}{' '.join(list(text))}", end=end)
393
468
 
394
- def error(*text, error_code=-1, do_exit=True, start='<<d>>', end='\n') -> int:
395
- if start=='<<d>>': start = f"ERROR: " + (f"[{progname}] " if progname_in_message else "")
469
+
470
+ def error(
471
+ *text, error_code: int = 255, do_exit: bool = True, start: object = None, end: str = '\n'
472
+ ) -> int:
473
+ '''Print error messaging (in red if possible).
474
+
475
+ *text: (positional str args) to be printed
476
+ error_code: (int) shell style return code (non-zero is error, but prefer > 1 b/c those are
477
+ python exceptions)
478
+ do_exit: (bool) if True will call exit based on global_exit_allowed.
479
+ start: (optional str) prefix to message; if None: chooses default start str
480
+ end: (optional str) suffix to print
481
+
482
+ Note these messages append to global logging. Increments global args['errors'] int.
483
+ '''
484
+ if start is None:
485
+ start = "ERROR: " + (f"[{progname}] " if progname_in_message else "")
396
486
  args['errors'] += 1
397
487
  print_red(f"{start}{' '.join(list(text))}", end=end)
398
488
  if do_exit:
399
- if args['debug']: print(traceback.print_stack())
400
- return exit(error_code)
401
- else:
402
- if error_code is None:
403
- return 0
404
- else:
405
- return abs(int(error_code))
489
+ if args['debug']:
490
+ print(traceback.print_stack())
491
+ # Call our overriden-builtin function for 'exit':
492
+ return exit(error_code) # pylint: disable=consider-using-sys-exit
406
493
 
407
- def exit(error_code=0, quiet=False):
494
+ if error_code is None:
495
+ return 0
496
+ return abs(int(error_code))
497
+
498
+
499
+ def exit( # pylint: disable=redefined-builtin
500
+ error_code: int = 0, quiet: bool = False
501
+ ) -> int:
502
+ '''sys.exit(int) wrapper, returns the error_code if global_exit_allowed=False'''
408
503
  if global_exit_allowed:
409
- if not quiet: info(f"Exiting with {args['warnings']} warnings, {args['errors']} errors")
504
+ if not quiet:
505
+ info(f"Exiting with {args['warnings']} warnings, {args['errors']} errors")
410
506
  sys.exit(error_code)
411
507
 
412
508
  if error_code is None:
413
509
  return 0
414
- else:
415
- return abs(int(error_code))
510
+
511
+ return abs(int(error_code))
512
+
416
513
 
417
514
  def getcwd():
515
+ '''Wrapper for os.getcwd() for current working dir'''
418
516
  try:
419
- cc = os.getcwd()
517
+ return os.getcwd()
420
518
  except Exception as e:
421
- error("Unable to getcwd(), did it get deleted from under us?")
422
- return cc
423
-
424
- _oc_root=None
425
- _oc_root_set=False
426
- def get_oc_root(error_on_fail:bool=False):
427
- global _oc_root
428
- global _oc_root_set
519
+ error(f"Unable to getcwd(), did it get deleted from under us? Exception: {e}")
520
+ return None
521
+
522
+ _OC_ROOT = None
523
+ _OC_ROOT_SET = False
524
+
525
+ def get_oc_root(error_on_fail: bool = False) -> None:
429
526
  '''Returns a str or False for the root directory of *this* repo.
430
527
 
431
528
  If environment variable OC_ROOT is set, that is used instead, otherwise attempts to use
432
529
  `git rev-parse`
433
530
  '''
531
+
532
+ global _OC_ROOT # pylint: disable=global-statement
533
+ global _OC_ROOT_SET # pylint: disable=global-statement
434
534
  # if we've already run through here once, just return the memorized result
435
- if _oc_root_set: return _oc_root
535
+ if _OC_ROOT_SET:
536
+ return _OC_ROOT
436
537
 
437
538
  # try looking for an env var
438
539
  s = os.environ.get('OC_ROOT')
439
540
  if s:
440
541
  debug(f'get_oc_root() -- returning from env: {s=}')
441
- _oc_root = s.strip()
542
+ _OC_ROOT = s.strip()
442
543
  else:
443
544
  # try asking GIT
444
- cp = subprocess.run('git rev-parse --show-toplevel', stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
445
- shell=True, universal_newlines=True)
545
+ cp = subprocess.run(
546
+ 'git rev-parse --show-toplevel', stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
547
+ shell=True, check=False, universal_newlines=True
548
+ )
446
549
  if cp.returncode != 0:
447
- # TODO(drew): at some point, address the fact that not all repos are oc_root. Is this function asking for
448
- # the repo we are in? or a pointer to the oc_root which maybe elsewhere on the system?
550
+ # TODO(drew): at some point, address the fact that not all repos are oc_root.
551
+ # Is this function asking for the repo we are in? or a pointer to the oc_root which
552
+ # maybe elsewhere on the system?
449
553
  print_didnt_find_it = debug
450
554
  if error_on_fail:
451
555
  print_didnt_find_it = error
452
- print_didnt_find_it(f'Unable to get a OC_ROOT directory using git rev-parse')
556
+ print_didnt_find_it('Unable to get a OC_ROOT directory using git rev-parse')
453
557
  else:
454
- _oc_root = cp.stdout.strip()
455
- if sys.platform == 'win32':
456
- _oc_root = _oc_root.replace('/', '\\') # git gives us /, but we need \
558
+ _OC_ROOT = cp.stdout.strip()
559
+ if sys.platform.startswith('win'):
560
+ _OC_ROOT = _OC_ROOT.replace('/', '\\') # git gives us /, but we need \
457
561
 
458
562
  # there is no sense running through this code more than once
459
- _oc_root_set = True
460
- return _oc_root
563
+ _OC_ROOT_SET = True
564
+ return _OC_ROOT
461
565
 
462
- def string_or_space(text, whitespace=False):
463
- if whitespace:
464
- return " " * len(text)
465
- else:
466
- return text
467
566
 
468
- def sprint_time(s):
469
- s = int(s)
470
- txt = ""
471
- do_all = False
472
- # days
473
- if (s >= (24*60*60)): # greater than 24h, we show days
474
- d = int(s/(24*60*60))
475
- txt += f"{d}d:"
476
- s -= (d*24*60*60)
477
- do_all = True
478
- # hours
479
- if do_all or (s >= (60*60)):
480
- d = int(s/(60*60))
481
- txt += f"{d:2}:"
482
- s -= (d*60*60)
483
- do_all = True
484
- # minutes
485
- d = int(s/(60))
486
- txt += f"{d:02}:"
487
- s -= (d*60)
488
- # seconds
489
- txt += f"{s:02}"
490
- return txt
491
-
492
- def safe_cp(source:str, destination:str, create_dirs:bool=False):
567
+ def safe_cp(source: str, destination: str, create_dirs: bool = False) -> None:
568
+ '''shutil.copy2 wrapper to optionally make the destination directories'''
493
569
  try:
494
570
  # Infer if destination is a directory
495
571
  if destination.endswith('/') or os.path.isdir(destination):
@@ -503,41 +579,45 @@ def safe_cp(source:str, destination:str, create_dirs:bool=False):
503
579
  os.makedirs(parent_dir, exist_ok=True)
504
580
  # actually copy the file
505
581
  shutil.copy2(source, destination)
582
+ info(f"Copied {source} to {destination}")
506
583
  except Exception as e:
507
584
  print(f"Error copying file from '{source}' to '{destination}': {e}")
508
- info(f"Copied {source} to {destination}")
509
585
 
510
- def safe_rmdir(path):
511
- """Safely and reliably remove a non-empty directory."""
512
- try:
513
- # Ensure the path exists
514
- if os.path.exists(path):
515
- shutil.rmtree(path)
516
- info(f"Directory '{path}' has been removed successfully.")
517
- else:
518
- debug(f"Directory '{path}' does not exist.")
519
- except Exception as e:
520
- error(f"An error occurred while removing the directory '{path}': {e}")
521
586
 
522
- def safe_mkdir(path : str):
587
+ def safe_mkdir(path : str) -> None:
588
+ '''Attempt to make dir at path, and make all subdirs up to that path'''
589
+ if os.path.exists(path):
590
+ return
591
+ left, _ = os.path.split(os.path.relpath(path))
592
+ if left and left not in ['.', '..', os.path.sep]:
593
+ safe_mkdir(left)
523
594
  try:
524
595
  os.mkdir(path)
525
596
  except FileExistsError:
526
597
  pass
527
- except:
598
+ except Exception as e1:
528
599
  try:
529
600
  os.system(f'mkdir -p {path}')
530
- except Exception as e:
531
- error(f'unable to mkdir {path=}, exception {e=}')
601
+ except Exception as e2:
602
+ error(f'unable to mkdir {path=}, exceptions {e1}, {e2=}')
532
603
 
533
- def safe_mkdirs(base : str, new_dirs : list):
604
+
605
+ def safe_mkdirs(base : str, new_dirs : list) -> None:
606
+ '''Create new_dirs at base'''
534
607
  for p in new_dirs:
535
608
  safe_mkdir( os.path.join(base, p) )
536
609
 
537
610
 
538
- def import_class_from_string(full_class_name):
611
+ def safe_mkdir_for_file(filepath: str) -> None:
612
+ '''Given a new filepath, create dir for that filepath'''
613
+ left, _ = os.path.split(filepath)
614
+ if left:
615
+ safe_mkdir(left)
616
+
617
+
618
+ def import_class_from_string(full_class_name: str) -> None:
539
619
  """
540
- Imports a class given its full name as a string.
620
+ Imports a class given its full name as a str.
541
621
 
542
622
  Args:
543
623
  full_class_name: The full name of the class,
@@ -546,18 +626,21 @@ def import_class_from_string(full_class_name):
546
626
  Returns:
547
627
  The imported class, or None if an error occurs.
548
628
  """
549
- from importlib import import_module
550
629
  try:
551
630
  module_path, class_name = full_class_name.rsplit(".", 1)
552
631
  module = import_module(module_path)
553
632
  return getattr(module, class_name)
554
- except (ImportError, AttributeError) as e:
555
- print(f"Error importing class {full_class_name=}: {e=}")
633
+ except Exception as e:
634
+ print(f"Error importing class {full_class_name=}: {e}")
556
635
  return None
557
636
 
558
637
 
559
638
  class ShellCommandList(list):
560
- def __init__(self, obj=None, tee_fpath=None):
639
+ '''Wrapper around a list, of str that we'll run as a subprocess command
640
+
641
+ included member var for tee_path, to save a log from this subprocess commands list
642
+ '''
643
+ def __init__(self, obj: object = None, tee_fpath: str = ''):
561
644
  super().__init__(obj)
562
645
  for k in ['tee_fpath']:
563
646
  setattr(self, k, getattr(obj, k, None))
@@ -565,29 +648,35 @@ class ShellCommandList(list):
565
648
  self.tee_fpath = tee_fpath
566
649
 
567
650
 
568
- def write_shell_command_file(dirpath : str, filename : str, command_lists : list, line_breaks : bool = False):
569
- ''' Writes new file at {dirpath}/{filename} as a bash shell command, using command_lists (list of lists)
651
+ def write_shell_command_file(
652
+ dirpath : str, filename : str, command_lists : list, line_breaks : bool = False
653
+ ) -> None:
654
+ ''' Writes new file at {dirpath}/{filename} as a bash shell command, using command_lists
655
+ (list of lists)
570
656
 
571
657
  -- dirpath (str) -- directory where file is written (usually eda.work/{target}_sim
572
658
  -- filename (str) -- filename, for example compile_only.sh
573
- -- command_lists (list) -- list of (list or ShellCommandList), each item in the list is a list of commands (aka, how
574
- subprocess.run(args) uses a list of commands.
575
- -- line_breaks (bool) -- Set to True to have 1 word per line in the file followed by a line break.
576
- Default False has an entry in command_lists all on a single line.
659
+ -- command_lists (list) -- list of (list or ShellCommandList), each item in the list is a
660
+ list of commands (aka, how subprocess.run(args) uses a list of
661
+ commands.
662
+ -- line_breaks (bool) -- Set to True to have 1 word per line in the file followed by a line
663
+ break. Default False has an entry in command_lists all on a single
664
+ line.
577
665
 
578
666
  Returns None, writes the file and chmod's it to 0x755.
579
-
580
667
  '''
668
+
581
669
  # command_lists should be a list-of-lists.
582
670
  bash_path = shutil.which('bash')
583
- assert type(command_lists) is list, f'{command_lists=}'
671
+ assert isinstance(command_lists, list), f'{command_lists=}'
584
672
  fullpath = os.path.join(dirpath, filename)
585
- with open(fullpath, 'w') as f:
673
+ with open(fullpath, 'w', encoding='utf-8') as f:
586
674
  if not bash_path:
587
675
  bash_path = "/bin/bash" # we may not get far, but we'll try
588
676
  f.write('#!' + bash_path + '\n\n')
589
677
  for obj in command_lists:
590
- assert isinstance(obj, list), f'{obj=} (obj must be list/ShellCommandList) {command_lists=}'
678
+ assert isinstance(obj, list), \
679
+ f'{obj=} (obj must be list/ShellCommandList) {command_lists=}'
591
680
  clist = list(obj).copy()
592
681
  tee_fpath = getattr(obj, 'tee_fpath', None)
593
682
  if tee_fpath:
@@ -599,7 +688,7 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
599
688
  else:
600
689
  clist.append(f'2>&1 | tee {tee_fpath}')
601
690
 
602
- if len(clist) > 0:
691
+ if clist:
603
692
  if line_breaks:
604
693
  # line_breaks=True - have 1 word per line, followed by \ and \n
605
694
  sep = " \\" + "\n"
@@ -616,26 +705,6 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
616
705
  os.chmod(fullpath, 0o755)
617
706
 
618
707
 
619
- def write_eda_config_and_args(dirpath : str, filename=EDA_OUTPUT_CONFIG_FNAME, command_obj_ref=None):
620
- import copy
621
- if command_obj_ref is None:
622
- return
623
- fullpath = os.path.join(dirpath, filename)
624
- data = dict()
625
- for x in ['command_name', 'config', 'target', 'args', 'modified_args', 'defines',
626
- 'incdirs', 'files_v', 'files_sv', 'files_vhd']:
627
- # Use deep copy b/c otherwise these are references to opencos.eda.
628
- data[x] = copy.deepcopy(getattr(command_obj_ref, x, ''))
629
-
630
- # fix some burried class references in command_obj_ref.config,
631
- # otherwise we won't be able to safe load this yaml, so cast as str repr.
632
- for k, v in command_obj_ref.config.items():
633
- if k == 'command_handler':
634
- data['config'][k] = str(v)
635
-
636
- yaml_safe_writer(data=data, filepath=fullpath)
637
-
638
-
639
708
  def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
640
709
  '''Returns the best guess as the 'top' module name name, given a fpath where
641
710
 
@@ -660,121 +729,9 @@ def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
660
729
  if bool(re.fullmatch(r'^\w+$', module_name)):
661
730
  if module_name == module_guess:
662
731
  return module_guess
663
- elif module_name:
732
+ if module_name:
664
733
  best_guess = module_name
665
734
  if best_guess:
666
735
  return best_guess
667
- else:
668
- return ''
669
-
670
-
671
- def subprocess_run(work_dir, command_list, fake:bool=False, shell=False) -> int:
672
- ''' Run command_list in the foreground, with preference to use bash if shell=True.'''
673
-
674
- if work_dir is not None:
675
- os.chdir(work_dir)
676
-
677
- is_windows = sys.platform.startswith('win')
678
-
679
- proc_kwargs = {'shell': shell}
680
- bash_exec = shutil.which('bash')
681
- if shell and bash_exec and not is_windows:
682
- proc_kwargs.update({'executable': bash_exec})
683
-
684
- if not is_windows and shell:
685
- c = ' '.join(command_list)
686
- else:
687
- c = command_list
688
-
689
- if fake:
690
- info(f"util.subprocess_run FAKE: would have called subprocess.run({c}, **{proc_kwargs}")
691
- return 0
692
- else:
693
- debug(f"util.subprocess_run: About to call subprocess.run({c}, **{proc_kwargs}")
694
- proc = subprocess.run(c, **proc_kwargs)
695
- return proc.returncode
696
-
697
-
698
- def subprocess_run_background(work_dir, command_list, background=True, fake:bool=False,
699
- shell=False, tee_fpath=None) -> (str, str, int):
700
- ''' Run command_list in the background, with preference to use bash if shell=True
701
-
702
- tee_fpath is relative to work_dir.
703
- '''
704
-
705
-
706
- is_windows = sys.platform.startswith('win')
707
-
708
- debug(f'util.subprocess_run_background: {background=} {tee_fpath=} {shell=}')
709
-
710
- if fake or (not background and not tee_fpath):
711
- # If tee_fpath is set, we're going to "background" but with it also
712
- # printed out (stdout and tee_fpath).
713
- rc = subprocess_run(work_dir, command_list, fake=fake, shell=shell)
714
- return '', '', rc
715
-
716
- if work_dir is not None:
717
- os.chdir(work_dir)
718
-
719
- proc_kwargs = {'shell': shell,
720
- 'stdout': subprocess.PIPE,
721
- 'stderr': subprocess.STDOUT,
722
- }
723
-
724
- bash_exec = shutil.which('bash')
725
- if shell and bash_exec and not is_windows:
726
- # Note - windows powershell will end up calling: /bin/bash /c, which won't work
727
- proc_kwargs.update({'executable': bash_exec})
728
-
729
- if not is_windows and shell:
730
- c = ' '.join(command_list)
731
- else:
732
- c = command_list # leave as list.
733
-
734
- debug(f"util.subprocess_run_background: about to call subprocess.Popen({c}, **{proc_kwargs})")
735
- proc = subprocess.Popen(c, **proc_kwargs)
736
-
737
- if tee_fpath:
738
- stdout = ''
739
- stderr = ''
740
- with open(tee_fpath, 'w') as f:
741
- for line in iter(proc.stdout.readline, b''):
742
- line = line.rstrip().decode("utf-8", errors="replace")
743
- if not background:
744
- print(line)
745
- f.write(line + '\n')
746
- stdout += line + '\n'
747
-
748
- proc.communicate()
749
- rc = proc.returncode
750
- info('util.subprocess_run_background: wrote: ' + os.path.abspath(tee_fpath))
751
-
752
-
753
- else:
754
736
 
755
- debug(f"util.subprocess_run_background: about to call proc.communicate()")
756
- stdout, stderr = proc.communicate()
757
- rc = proc.returncode
758
-
759
- stdout = stdout.decode('utf-8', errors="replace") if stdout else ""
760
- stderr = stderr.decode('utf-8', errors="replace") if stderr else ""
761
- debug(f"shell_run_background: {rc=}")
762
- if stdout:
763
- for lineno, line in enumerate(stdout.strip().split('\n')):
764
- debug(f"stdout:{lineno+1}: {line}")
765
- if stderr:
766
- for lineno, line in enumerate(stdout.strip().split('\n')):
767
- debug(f"stderr:{lineno+1}: {line}")
768
-
769
- return stdout, stderr, rc
770
-
771
-
772
- def sanitize_defines_for_sh(value):
773
- # Need to sanitize this for shell in case someone sends a +define+foo+1'b0,
774
- # which needs to be escaped as +define+foo+1\'b0, otherwise bash or sh will
775
- # think this is an unterminated string.
776
- # TODO(drew): decide if we should instead us shlex.quote('+define+key=value')
777
- # instead of this function.
778
- if type(value) is str:
779
- value = value.replace("'", "\\" + "'")
780
- return value
737
+ return ''