opencos-eda 0.3.5__py3-none-any.whl → 0.3.6__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.
opencos/tests/helpers.py CHANGED
@@ -164,38 +164,43 @@ def assert_gen_deps_yml_good(filepath:str, want_target:str='') -> dict:
164
164
 
165
165
  def assert_export_json_good(filepath:str) -> dict:
166
166
  '''Checks that an exported JSON (from eda export, or eda <command> --export) has known keys'''
167
- assert os.path.exists(filepath), f'{filepath=} does not exist'
167
+ assert os.path.isfile(filepath), f'{filepath=} does not exist'
168
168
  with open(filepath, encoding='utf-8') as f:
169
169
  data = json.load(f)
170
- assert 'name' in data
171
- assert 'eda' in data
172
- assert any(x in data for x in ['files', 'tb'])
170
+ assert 'tests' in data
171
+ assert len(data.get('tests', [])) >= 1
172
+ for test in data.get('tests', []):
173
+ check_test_runner_schema(test)
173
174
  return data
174
175
 
176
+ def check_test_runner_schema(test: dict) -> None:
177
+ '''Confirm that a single test's JSON/JSONL schema is OK.'''
178
+ assert 'correlationId' in test
179
+ assert 'jobType' in test
180
+ assert 'cmd' in test
181
+ assert 'filesList' in test # 0 files is OK.
182
+
175
183
 
176
184
  def assert_export_jsonl_good(filepath:str, jsonl:bool=True) -> list:
177
185
  '''Checks that an exported JSONL (from eda multi --export) has known keys'''
178
- assert os.path.exists(filepath), f'{filepath=} does not exist'
186
+ assert os.path.isfile(filepath), f'{filepath=} does not exist'
179
187
  ret = []
180
188
  with open(filepath, encoding='utf-8') as f:
181
189
  if jsonl:
190
+ print(f'Using JSONL for {filepath=}')
182
191
  for line in f.readlines():
183
192
  line = line.strip()
184
- data = json.loads(line)
185
- assert 'name' in data
186
- assert 'eda' in data
187
- assert any(x in data for x in ['files', 'tb'])
188
- ret.append(data)
193
+ test = json.loads(line)
194
+ check_test_runner_schema(test)
195
+ ret.append(test)
189
196
  else:
197
+ print(f'Using JSON for {filepath=}')
190
198
  data = json.load(f)
191
199
  assert 'tests' in data
192
- assert isinstance(data['tests'], list)
193
- for entry in data['tests']:
194
- assert 'name' in entry
195
- assert 'eda' in entry
196
- assert any(x in entry for x in ['files', 'tb'])
197
- ret.append(entry)
198
-
200
+ assert len(data.get('tests', [])) >= 1
201
+ for test in data.get('tests', []):
202
+ check_test_runner_schema(test)
203
+ ret.append(test)
199
204
 
200
205
  return ret
201
206
 
@@ -265,6 +270,7 @@ class Helpers:
265
270
  background=True,
266
271
  tee_fpath=logfile
267
272
  )
273
+ print(f'Wrote: {os.path.abspath(logfile)=}')
268
274
  else:
269
275
  with open(logfile, 'w', encoding='utf-8') as f:
270
276
  with redirect_stdout(f), redirect_stderr(f):
opencos/tools/cocotb.py CHANGED
@@ -10,7 +10,9 @@ import subprocess
10
10
  from opencos import util
11
11
  from opencos.eda_base import Tool
12
12
  from opencos.commands import CommandSim
13
+ from opencos.utils import status_constants
13
14
  from opencos.utils.str_helpers import sanitize_defines_for_sh
15
+ from opencos.tools import verilator # For default waivers.
14
16
 
15
17
 
16
18
  class ToolCocotb(Tool):
@@ -76,6 +78,8 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
76
78
  'tcl-file': None,
77
79
  'cocotb-test-module': None,
78
80
  'cocotb-test-runner': 'python',
81
+ 'cocotb-test-runner-file': '',
82
+ 'cocotb-test-run-dir': None, # If None, use self.args['work-dir']
79
83
  'cocotb-simulator': 'verilator',
80
84
  'cocotb-makefile': False,
81
85
  'cocotb-python-runner': True,
@@ -86,17 +90,25 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
86
90
  'waves': 'Include waveforms by setting COCOTB_ENABLE_WAVES=1',
87
91
  'cocotb-test-module': 'Python test module name (e.g., test_my_design)',
88
92
  'cocotb-test-runner': 'Test runner to use: python (default) or pytest',
89
- 'cocotb-simulator': ('Simulator backend: verilator (default), icarus, etc.'
90
- ' Note that iverilog will convert to icarus here'),
93
+ 'cocotb-test-runner-file': 'Bring your own test_runner.py file',
94
+ 'cocotb-test-run-dir': (
95
+ 'Directory where cocotb-test-runner will run, if unset uses default eda.work/(name)'
96
+ ),
97
+ 'cocotb-simulator': (
98
+ 'Simulator backend: verilator (default), icarus, etc.'
99
+ ' Note that iverilog will convert to icarus here'
100
+ ),
91
101
  'cocotb-makefile': 'Use traditional Makefile system instead of Python runner',
92
102
  'cocotb-python-runner': 'Use Python-based runner system (default, cocotb 1.8+)',
93
- 'cocotb-standalone-makefile': ('Use provided Makefile as-is, '
94
- 'run make in source directory'),
103
+ 'cocotb-standalone-makefile': (
104
+ 'Use provided Makefile as-is, run make in source directory'
105
+ ),
95
106
  })
96
107
 
97
108
  self.cocotb_command_lists = []
98
109
  self.cocotb_test_files = []
99
110
 
111
+
100
112
  def set_tool_defines(self):
101
113
  ToolCocotb.set_tool_defines(self)
102
114
 
@@ -155,12 +167,13 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
155
167
  simulate_sh_fname='cocotb_test.sh'
156
168
  )
157
169
 
170
+
158
171
  def _find_cocotb_test_files(self):
159
172
  '''Find Python test files that contain cocotb tests'''
160
173
  self.cocotb_test_files = []
161
174
 
162
175
  # Look for test files in the current directory and deps
163
- for file_path in self.files_non_source:
176
+ for file_path in self.files_non_source + self.files_py:
164
177
  if (file_path.endswith('.py') and
165
178
  ('test' in file_path.lower() or 'tb' in file_path.lower())):
166
179
  # Check if it's a cocotb test file
@@ -193,8 +206,19 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
193
206
  def _prepare_python_runner_system(self):
194
207
  '''Prepare cocotb using the Python-based runner system (cocotb 1.8+)'''
195
208
 
196
- # Create a Python runner script
197
- runner_script = self._create_python_runner_script()
209
+ # Create a Python runner script, or use from --cocotb-test-runner-file
210
+ if self.args['cocotb-test-runner-file']:
211
+
212
+ runner_script = self.args['cocotb-test-runner-file']
213
+ if not os.path.isfile(self.args['cocotb-test-runner-file']):
214
+ self.error(
215
+ "File does not exist, for: --cocotb-test-runner-file=",
216
+ f"{self.args['cocotb-test-runner-file']}",
217
+ error_code=status_constants.EDA_GENERAL_FILE_NOT_FOUND
218
+ )
219
+ return
220
+ else:
221
+ runner_script = self._create_python_runner_script()
198
222
 
199
223
  if self.args['cocotb-test-runner'] == 'pytest':
200
224
  # Use pytest to run the tests
@@ -212,28 +236,41 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
212
236
  # Set environment variables
213
237
  env_vars = self._get_cocotb_env_vars()
214
238
 
215
- # Create command with environment variables
216
- command_list = self._create_env_command(env_vars) + cmd_list
217
- self.cocotb_command_lists = [util.ShellCommandList(command_list,
218
- tee_fpath='cocotb_test.log')]
239
+ # Create command with environment variables, needs to be list-of-(cmd_list)
240
+ self.cocotb_command_lists = [
241
+ util.ShellCommandList(
242
+ self._create_env_command(env_vars) + cmd_list,
243
+ tee_fpath='cocotb_test.log',
244
+ # If someone wanted to run this from a specific dir, then
245
+ # run it from there.
246
+ work_dir=self.args['cocotb-test-run-dir']
247
+ )
248
+ ]
219
249
 
220
250
  def _prepare_makefile_system(self):
221
251
  '''Prepare cocotb using the traditional Makefile system'''
222
252
 
223
253
  makefile_path = os.path.join(self.args['work-dir'], 'Makefile')
254
+ self.files_makefile.append(makefile_path)
224
255
  with open(makefile_path, 'w', encoding='utf-8') as f:
225
256
  f.write(self._create_makefile_content())
226
257
 
227
258
  cmd_list = self._create_shell_command_with_success('make -f Makefile')
228
- self.cocotb_command_lists = [util.ShellCommandList(cmd_list,
229
- tee_fpath='cocotb_makefile.log')]
259
+ self.cocotb_command_lists = [
260
+ util.ShellCommandList(
261
+ cmd_list, tee_fpath='cocotb_makefile.log',
262
+ # If someone wanted to run this from a specific dir, then
263
+ # run it from there.
264
+ work_dir=self.args['cocotb-test-run-dir']
265
+ )
266
+ ]
230
267
 
231
268
  def _prepare_standalone_makefile_system(self):
232
269
  '''Use provided Makefile as-is, run make in source directory'''
233
270
 
234
271
  # Find the Makefile in our dependencies
235
272
  makefile_path = None
236
- for file_path in self.files_non_source:
273
+ for file_path in self.files_non_source + self.files_makefile:
237
274
  if os.path.basename(file_path).lower() == 'makefile':
238
275
  makefile_path = file_path
239
276
  break
@@ -241,10 +278,19 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
241
278
  if not makefile_path:
242
279
  self.error('No Makefile found in deps for --cocotb-standalone-makefile')
243
280
 
281
+ if any(value for key,value in self.args.items() if key.startswith('export')):
282
+ util.warning(f'Using an --export arg with Cocotb standalong Makefile {makefile_path}',
283
+ 'is not fully supported')
284
+
244
285
  makefile_dir = os.path.dirname(os.path.abspath(makefile_path))
245
- cmd_list = self._create_shell_command_with_success(f'cd {makefile_dir} && make')
246
- self.cocotb_command_lists = [util.ShellCommandList(
247
- cmd_list, tee_fpath='cocotb_standalone.log')]
286
+ cmd_list = self._create_shell_command_with_success('make')
287
+ self.cocotb_command_lists = [
288
+ util.ShellCommandList(
289
+ cmd_list,
290
+ tee_fpath='cocotb_standalone.log',
291
+ work_dir=makefile_dir
292
+ )
293
+ ]
248
294
 
249
295
  def _get_test_module_name(self) -> str:
250
296
  '''Get the test module name from args or detected files'''
@@ -280,8 +326,22 @@ class CommandSimCocotb(CommandSim, ToolCocotb):
280
326
 
281
327
  return script_path
282
328
 
329
+
283
330
  def _generate_runner_script_content(self, test_module: str, hdl_sources: list) -> str:
284
331
  '''Generate the content for the Python runner script'''
332
+
333
+ if shutil.which('verilator'):
334
+ # TODO(drew): this shortcuts if verilator is truly usable,
335
+ # consider using eda_tool_helper to get "tools_loaded", which
336
+ # is not set in self.config['tools_loaded'] when --tool=cocotb.
337
+ # Would need minor refactor for eda.py methods auto_tools_order,
338
+ # tool_setup to go into eda_tool_helper.py, and those methods would
339
+ # need hooks to be non-destructive to config.
340
+ tmp_verilator_obj = verilator.VerilatorSim(config=self.config)
341
+ verilator_waivers = tmp_verilator_obj.get_verilator_tool_config_waivers()
342
+ else:
343
+ verilator_waivers = []
344
+
285
345
  return f'''#!/usr/bin/env python3
286
346
  """
287
347
  Cocotb test runner script generated by opencos
@@ -337,6 +397,8 @@ def run_cocotb_test():
337
397
 
338
398
  if simulator == "verilator":
339
399
  build_args.extend({list(self.args.get('verilate-args', []))!r})
400
+ build_args.extend({list(self.args.get('compile-args', []))!r})
401
+ build_args.extend({list(verilator_waivers)!r})
340
402
 
341
403
  # Build the design
342
404
  runner.build(
@@ -449,9 +511,20 @@ include $(shell cocotb-config --makefiles)/Makefile.sim
449
511
  '''Get environment variables for cocotb execution'''
450
512
  env_vars = {}
451
513
 
514
+ top = self.args.get('top', 'top')
515
+
452
516
  # Basic cocotb configuration
453
- env_vars['SIM'] = self.args['cocotb-simulator']
454
- env_vars['TOPLEVEL'] = self.args.get('top', 'top')
517
+ env_vars.update({
518
+ 'SIM': self.args['cocotb-simulator'],
519
+ 'TOPLEVEL': top,
520
+ 'COCOTB_TOPLEVEL': top,
521
+ })
522
+
523
+ if test_module := self.args['cocotb-test-module']:
524
+ env_vars.update({
525
+ 'MODULE': test_module,
526
+ 'COCOTB_TEST_MODULES': test_module,
527
+ })
455
528
 
456
529
  # Enable waves if requested
457
530
  if self.args.get('waves', False):
@@ -464,8 +537,8 @@ include $(shell cocotb-config --makefiles)/Makefile.sim
464
537
  env_vars['COCOTB_LOG_LEVEL'] = 'INFO'
465
538
 
466
539
  # Random seed
467
- if self.args.get('seed'):
468
- env_vars['COCOTB_RANDOM_SEED'] = str(self.args['seed'])
540
+ if seed := self.args.get('seed'):
541
+ env_vars['COCOTB_RANDOM_SEED'] = str(seed)
469
542
 
470
543
  return env_vars
471
544
 
@@ -228,7 +228,7 @@ class VerilatorSim(CommandSim, ToolVerilator):
228
228
  warnings=(not lint_only), add_uvm_pkg_if_found=True
229
229
  )
230
230
 
231
- verilate_command_list += self._get_verilator_tool_config_waivers()
231
+ verilate_command_list += self.get_verilator_tool_config_waivers()
232
232
 
233
233
  verilate_command_list += self._verilator_args_defaults_cflags_nproc()
234
234
 
@@ -408,7 +408,10 @@ class VerilatorSim(CommandSim, ToolVerilator):
408
408
 
409
409
  return verilate_command_list
410
410
 
411
- def _get_verilator_tool_config_waivers(self) -> list:
411
+ def get_verilator_tool_config_waivers(self) -> list:
412
+ '''Returns list of args to verilator for waviers, from --compile-waivers and
413
+
414
+ --config-yml for tool: (config)'''
412
415
 
413
416
  # Add compile waivers from self.config (tools.verilator.compile-waivers list):
414
417
  # list(set(mylist)) to get unique.
opencos/util.py CHANGED
@@ -32,6 +32,7 @@ env_files_loaded = set() # pylint: disable=invalid-name
32
32
 
33
33
  args = { # pylint: disable=invalid-name
34
34
  'color' : bool(supportsColor.stdout),
35
+ 'emoji' : bool(getattr(supportsColor.stdout, 'level', 0) >= 2),
35
36
  'quiet' : False,
36
37
  'verbose' : False,
37
38
  'debug' : False,
@@ -44,16 +45,21 @@ args = { # pylint: disable=invalid-name
44
45
  max_error_code = 0 # pylint: disable=invalid-name
45
46
 
46
47
  class Colors:
47
- '''Namespace class for color printing help'''
48
+ '''Namespace class for color printing help
49
+
50
+ Avoid calling these directly, other than perhapas calling info(*txt, color=Colors.red)
51
+ with 'color' set. It is preferred for outside callers to use one of the print_<color>(..)
52
+ functions, or one of info|warning|error|debug
53
+ '''
48
54
  red = "\x1B[31m"
49
55
  green = "\x1B[32m"
50
- orange = "\x1B[33m"
51
- yellow = "\x1B[39m"
56
+ yellow = "\x1B[33m" # This looks orange, but it's techincally yellow
57
+ foreground = "\x1B[39m"
52
58
  normal = "\x1B[0m"
53
59
 
54
60
  @staticmethod
55
61
  def color_text(text: str, color: str) -> str:
56
- '''Wraps 'text' (str) with color (one of red|green|orange|yellow) prefix and
62
+ '''Wraps 'text' (str) with color (one of red|green|yellow|foreground) prefix and
57
63
 
58
64
  color (normal) suffix. Disables color prefix/suffix wrapping if args['color']=False
59
65
  '''
@@ -61,29 +67,13 @@ class Colors:
61
67
  return color + text + "\x1B[0m" # (normal)
62
68
  return text
63
69
 
64
- def red_text(text: str) -> str:
65
- '''Wraps text for printing as red, disabled if global args['color']=False'''
66
- if args['color']:
67
- return Colors.red + text + Colors.normal
68
- return text
69
-
70
- def green_text(text: str) -> str:
71
- '''Wraps text for printing as green, disabled if global args['color']=False'''
72
- if args['color']:
73
- return Colors.green + text + Colors.normal
74
- return text
75
-
76
- def orange_text(text: str) -> str:
77
- '''Wraps text for printing as orange, disabled if global args['color']=False'''
78
- if args['color']:
79
- return Colors.orange + text + Colors.normal
80
- return text
81
-
82
- def yellow_text(text: str) -> str:
83
- '''Wraps text for printing as yellow, disabled if global args['color']=False'''
84
- if args['color']:
85
- return Colors.yellow + text + Colors.normal
86
- return text
70
+
71
+ def safe_emoji(emoji: str, default: str = '') -> str:
72
+ '''Returns emoji character if args['emoji'] is True'''
73
+ if args['emoji']:
74
+ return emoji
75
+ return default
76
+
87
77
 
88
78
  class ArtifactTypes(Enum):
89
79
  '''Types that are allow-listed for artifacts.add* methods. If you don't use one of
@@ -343,7 +333,7 @@ def get_argparse_bool_action_kwargs() -> dict:
343
333
  def get_argparser() -> argparse.ArgumentParser:
344
334
  '''Returns the opencos.util ArgumentParser'''
345
335
  parser = argparse.ArgumentParser(
346
- prog='opencos common options', add_help=False, allow_abbrev=False
336
+ prog=f'{safe_emoji("🔎 ")}opencos common options', add_help=False, allow_abbrev=False
347
337
  )
348
338
  # We set allow_abbrev=False so --force-logfile won't try to attempt parsing shorter similarly
349
339
  # named args like --force, we want those to go to unparsed list.
@@ -355,6 +345,8 @@ def get_argparser() -> argparse.ArgumentParser:
355
345
  parser.add_argument('--version', default=False, action='store_true')
356
346
  parser.add_argument('--color', **bool_action_kwargs, default=bool(supportsColor.stdout),
357
347
  help='Use shell colors for info/warning/error messaging')
348
+ parser.add_argument('--emoji', **bool_action_kwargs, default=args['emoji'],
349
+ help=f'Support emojis in terminal{safe_emoji(" 💪")}')
358
350
  parser.add_argument('--quiet', **bool_action_kwargs, default=args['quiet'],
359
351
  help='Do not display info messaging')
360
352
  parser.add_argument('--verbose', **bool_action_kwargs, default=args['verbose'],
@@ -410,12 +402,25 @@ def get_argparser_short_help(parser: object = None) -> str:
410
402
  '''Returns short help for our ArgumentParser'''
411
403
  if not parser:
412
404
  parser = get_argparser()
413
- full_lines = strip_ansi_color(parser.format_help()).split('\n')
405
+
406
+ if not args['color']:
407
+ # Since python3.14 doesn't care about our custom color settings,
408
+ # need to remove any ANSI colors from argparse help formatter:
409
+ full_lines = strip_ansi_color(parser.format_help()).split('\n')
410
+ else:
411
+ full_lines = parser.format_help().split('\n')
412
+
414
413
  lineno = 0
415
414
  for lineno, line in enumerate(full_lines):
415
+ # Again, strip any ANSI colors when searching for starting text:
416
+ # - options:
417
+ # - optional arguments:
418
+ if args['color']:
419
+ line = strip_ansi_color(line)
416
420
  if any(line.startswith(x) for x in ('options:', 'optional arguments:')):
417
421
  break
418
- # skip the line that says 'options:', repalce with the progname:
422
+
423
+ # skip the line that says 'options:', replace with the progname:
419
424
  return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
420
425
 
421
426
 
@@ -724,7 +729,7 @@ def print_post(text: str, end: str) -> None:
724
729
 
725
730
 
726
731
  def print_color(text: str, color: str, end: str = '\n') -> None:
727
- '''Note that color(str) must be one of Colors.[red|green|orange|yellow|normal]'''
732
+ '''Note that color(str) must be one of Colors.[red|green|yellow|normal]'''
728
733
  print_pre()
729
734
  print(Colors.color_text(text, color), end=end, flush=True)
730
735
  print_post(text, end)
@@ -732,25 +737,25 @@ def print_color(text: str, color: str, end: str = '\n') -> None:
732
737
  def print_red(text: str, end: str = '\n') -> None:
733
738
  '''Print text as red, goes back to normal color'''
734
739
  print_pre()
735
- print(red_text(text), end=end, flush=True)
740
+ print(Colors.color_text(text, color=Colors.red), end=end, flush=True)
736
741
  print_post(text, end)
737
742
 
738
743
  def print_green(text: str, end: str = '\n') -> None:
739
744
  '''Print text as green, goes back to normal color'''
740
745
  print_pre()
741
- print(green_text(text), end=end, flush=True)
746
+ print(Colors.color_text(text, color=Colors.green), end=end, flush=True)
742
747
  print_post(text, end)
743
748
 
744
- def print_orange(text: str, end: str = '\n') -> None:
745
- '''Print text as orange, goes back to normal color'''
749
+ def print_yellow(text: str, end: str = '\n') -> None:
750
+ '''Print text as yellow, goes back to normal color'''
746
751
  print_pre()
747
- print(orange_text(text), end=end, flush=True)
752
+ print(Colors.color_text(text, color=Colors.yellow), end=end, flush=True)
748
753
  print_post(text, end)
749
754
 
750
- def print_yellow(text: str, end: str = '\n') -> None:
751
- '''Print text as yellow, goes back to normal color'''
755
+ def print_foreground_color(text: str, end: str = '\n') -> None:
756
+ '''Print text as foreground color, goes back to normal color'''
752
757
  print_pre()
753
- print(yellow_text(text), end=end, flush=True)
758
+ print(Colors.color_text(text, color=Colors.foreground), end=end, flush=True)
754
759
  print_post(text, end)
755
760
 
756
761
 
@@ -763,8 +768,11 @@ def set_debug_level(level) -> None:
763
768
  info(f"Set debug level to {debug_level}")
764
769
 
765
770
 
766
- def debug(*text, level: int = 1, start: object = None, end: str = '\n') -> None:
767
- '''Print debug messaging (in yellow if possible). If args['debug'] is false, prints nothing.
771
+ def debug(
772
+ *text, level: int = 1, start: object = None, end: str = '\n', color=Colors.foreground
773
+ ) -> None:
774
+ '''Print debug messaging (in foreground color if possible). If args['debug'] is false,
775
+ prints nothing.
768
776
 
769
777
  *text: (positional str args) to be printed
770
778
  level: (int) debug level to decide if printed or not.
@@ -777,10 +785,10 @@ def debug(*text, level: int = 1, start: object = None, end: str = '\n') -> None:
777
785
  start = "DEBUG: " + (f"[{progname}] " if progname_in_message else "")
778
786
  if args['debug'] and \
779
787
  (((level==1) and args['verbose']) or (debug_level >= level)):
780
- print_yellow(f"{start}{' '.join(list(text))}", end=end)
788
+ print_color(f"{start}{' '.join(list(text))}", color=color, end=end)
781
789
 
782
790
 
783
- def info(*text, start: object = None, end='\n') -> None:
791
+ def info(*text, start: object = None, end='\n', color=Colors.green) -> None:
784
792
  '''Print information messaging (in green if possible). If args['quiet'], prints nothing.
785
793
 
786
794
  *text: (positional str args) to be printed
@@ -792,10 +800,10 @@ def info(*text, start: object = None, end='\n') -> None:
792
800
  if start is None:
793
801
  start = "INFO: " + (f"[{progname}] " if progname_in_message else "")
794
802
  if not args['quiet']:
795
- print_green(f"{start}{' '.join(list(text))}", end=end)
803
+ print_color(f"{start}{' '.join(list(text))}", color=color, end=end)
796
804
 
797
805
  def warning(*text, start: object = None, end: str = '\n') -> None:
798
- '''Print warning messaging (in orange if possible).
806
+ '''Print warning messaging (in yellow if possible).
799
807
 
800
808
  *text: (positional str args) to be printed
801
809
  start: (optional str) prefix to message; if None: chooses default start str
@@ -806,7 +814,7 @@ def warning(*text, start: object = None, end: str = '\n') -> None:
806
814
  if start is None:
807
815
  start = "WARNING: " + (f"[{progname}] " if progname_in_message else "")
808
816
  args['warnings'] += 1
809
- print_orange(f"{start}{' '.join(list(text))}", end=end)
817
+ print_yellow(f"{start}{' '.join(list(text))}", end=end)
810
818
 
811
819
 
812
820
  def error(
@@ -829,6 +837,7 @@ def error(
829
837
 
830
838
  if start is None:
831
839
  start = "ERROR: " + (f"[{progname}] " if progname_in_message else "")
840
+ start += safe_emoji("❌ ")
832
841
  args['errors'] += 1
833
842
  max_error_code = max(max_error_code, error_code)
834
843
  print_red(f"{start}{' '.join(list(text))}", end=end)
@@ -862,7 +871,17 @@ def exit( # pylint: disable=redefined-builtin
862
871
 
863
872
  if global_exit_allowed:
864
873
  if not quiet:
865
- info(f"Exiting with {args['warnings']} warnings, {args['errors']} errors")
874
+ info_color = Colors.green
875
+ start = safe_emoji('🔚 ')
876
+ if args['errors']:
877
+ info_color = Colors.red
878
+ start = safe_emoji('❗ ')
879
+ elif args['warnings']:
880
+ info_color = Colors.yellow
881
+ info(
882
+ f"{start}Exiting with {args['warnings']} warnings, {args['errors']} errors",
883
+ color=info_color
884
+ )
866
885
  sys.exit(error_code)
867
886
 
868
887
  if error_code is None:
@@ -998,14 +1017,18 @@ def import_class_from_string(full_class_name: str) -> None:
998
1017
  class ShellCommandList(list):
999
1018
  '''Wrapper around a list, of str that we'll run as a subprocess command
1000
1019
 
1001
- included member var for tee_path, to save a log from this subprocess commands list
1020
+ included member vars for:
1021
+ - tee_path, to save a log from this subprocess commands list
1022
+ - work_dir - in case we want to run this from non-default location.
1002
1023
  '''
1003
- def __init__(self, obj: object = None, tee_fpath: str = ''):
1024
+ def __init__(self, obj: object = None, tee_fpath: str = '', work_dir: str = ''):
1004
1025
  super().__init__(obj)
1005
- for k in ['tee_fpath']:
1026
+ for k in ('tee_fpath', 'work_dir'):
1006
1027
  setattr(self, k, getattr(obj, k, None))
1007
1028
  if tee_fpath:
1008
1029
  self.tee_fpath = tee_fpath
1030
+ if work_dir:
1031
+ self.work_dir = work_dir
1009
1032
 
1010
1033
 
1011
1034
  def write_shell_command_file(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: A simple Python package for wrapping RTL simuliatons and synthesis
5
5
  Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
6
6
  Project-URL: Homepage, https://github.com/cognichip/opencos
@@ -16,11 +16,12 @@ Requires-Dist: schema>=0.7.7
16
16
  Requires-Dist: toml>=0.10.2
17
17
  Requires-Dist: yamllint>=1.35.1
18
18
  Requires-Dist: PySerial>=3.5
19
- Requires-Dist: cocotb>=2.0
20
19
  Requires-Dist: supports_color>=0.2.0
20
+ Requires-Dist: cocotb>=2.0
21
+ Requires-Dist: pytest>=8.3.5
22
+ Requires-Dist: coverage>=7.6.1
21
23
  Provides-Extra: dev
22
24
  Requires-Dist: pylint>=3.0.0; extra == "dev"
23
- Requires-Dist: pytest>=8.3.5; extra == "dev"
24
25
  Provides-Extra: docs
25
26
  Requires-Dist: mkdocs; extra == "docs"
26
27
  Requires-Dist: mkdocs-material; extra == "docs"