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.
- opencos/__init__.py +4 -2
- opencos/_version.py +10 -7
- opencos/commands/flist.py +8 -7
- opencos/commands/multi.py +35 -18
- opencos/commands/sweep.py +9 -4
- opencos/commands/waves.py +1 -1
- opencos/deps/__init__.py +0 -0
- opencos/deps/defaults.py +69 -0
- opencos/deps/deps_commands.py +419 -0
- opencos/deps/deps_file.py +326 -0
- opencos/deps/deps_processor.py +670 -0
- opencos/deps_schema.py +7 -8
- opencos/eda.py +92 -67
- opencos/eda_base.py +625 -332
- opencos/eda_config.py +80 -14
- opencos/eda_extract_targets.py +22 -14
- opencos/eda_tool_helper.py +33 -7
- opencos/export_helper.py +166 -86
- opencos/export_json_convert.py +31 -23
- opencos/files.py +2 -1
- opencos/hw/__init__.py +0 -0
- opencos/{oc_cli.py → hw/oc_cli.py} +9 -4
- opencos/names.py +0 -4
- opencos/peakrdl_cleanup.py +13 -7
- opencos/seed.py +19 -11
- opencos/tests/helpers.py +27 -14
- opencos/tests/test_deps_helpers.py +35 -32
- opencos/tests/test_eda.py +47 -41
- opencos/tests/test_eda_elab.py +5 -3
- opencos/tests/test_eda_synth.py +1 -1
- opencos/tests/test_oc_cli.py +1 -1
- opencos/tests/test_tools.py +3 -2
- opencos/tools/iverilog.py +2 -2
- opencos/tools/modelsim_ase.py +2 -2
- opencos/tools/riviera.py +1 -1
- opencos/tools/slang.py +1 -1
- opencos/tools/surelog.py +1 -1
- opencos/tools/verilator.py +1 -1
- opencos/tools/vivado.py +1 -1
- opencos/tools/yosys.py +4 -3
- opencos/util.py +440 -483
- opencos/utils/__init__.py +0 -0
- opencos/utils/markup_helpers.py +98 -0
- opencos/utils/str_helpers.py +111 -0
- opencos/utils/subprocess_helpers.py +108 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/METADATA +1 -1
- opencos_eda-0.2.49.dist-info/RECORD +88 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
- opencos/deps_helpers.py +0 -1346
- opencos_eda-0.2.47.dist-info/RECORD +0 -79
- /opencos/{pcie.py → hw/pcie.py} +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/top_level.txt +0 -0
opencos/deps_schema.py
CHANGED
|
@@ -133,7 +133,8 @@ import sys
|
|
|
133
133
|
|
|
134
134
|
from schema import Schema, Or, Optional, SchemaError
|
|
135
135
|
|
|
136
|
-
from opencos import util
|
|
136
|
+
from opencos import util
|
|
137
|
+
from opencos.deps import deps_file
|
|
137
138
|
|
|
138
139
|
# Because we deal with YAML, where a Table Key with dangling/empty value is allowed
|
|
139
140
|
# and we have things like SystemVerilog defines where there's a Table key with no Value,
|
|
@@ -312,21 +313,19 @@ FILE_SIMPLIFIED = Schema(
|
|
|
312
313
|
|
|
313
314
|
def check(data: dict, schema_obj=FILE) -> (bool, str):
|
|
314
315
|
'''Returns (bool, str) for checking dict against FILE schema'''
|
|
315
|
-
|
|
316
|
-
|
|
317
316
|
try:
|
|
318
317
|
schema_obj.validate(data)
|
|
319
318
|
return True, None
|
|
320
319
|
except SchemaError as e:
|
|
321
|
-
return False,
|
|
322
|
-
except Exception as e:
|
|
320
|
+
return False, f'SchemaError: {e}'
|
|
321
|
+
except Exception as e:
|
|
323
322
|
return False, str(e)
|
|
324
323
|
|
|
325
324
|
|
|
326
325
|
def deps_markup_safe_load(deps_filepath: str) -> (bool, dict):
|
|
327
326
|
'''Returns tuple (bool False if took errors, dict of markp data)'''
|
|
328
327
|
current_errors = util.args['errors']
|
|
329
|
-
data =
|
|
328
|
+
data = deps_file.deps_markup_safe_load(deps_filepath)
|
|
330
329
|
if util.args['errors'] > current_errors:
|
|
331
330
|
return False, data
|
|
332
331
|
return True, data
|
|
@@ -337,10 +336,10 @@ def check_file(filepath: str, schema_obj=FILE) -> (bool, str, str):
|
|
|
337
336
|
|
|
338
337
|
deps_filepath = filepath
|
|
339
338
|
if os.path.isdir(filepath):
|
|
340
|
-
deps_filepath =
|
|
339
|
+
deps_filepath = deps_file.get_deps_markup_file(base_path=filepath)
|
|
341
340
|
|
|
342
341
|
# get deps file
|
|
343
|
-
if not os.path.
|
|
342
|
+
if not os.path.isfile(deps_filepath):
|
|
344
343
|
print(f'ERROR: internal error(s) no DEPS.[yml|..] found in {filepath=}')
|
|
345
344
|
return False, '', deps_filepath
|
|
346
345
|
|
opencos/eda.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
'''opencos.eda is an executable script (as `eda <command> ...`)
|
|
4
|
+
|
|
5
|
+
This is the entrypoint for tool discovery, and running targets from command line or DEPS
|
|
6
|
+
markup files
|
|
7
|
+
'''
|
|
4
8
|
|
|
5
9
|
import subprocess
|
|
6
10
|
import os
|
|
@@ -13,14 +17,13 @@ import shlex
|
|
|
13
17
|
import importlib.util
|
|
14
18
|
|
|
15
19
|
import opencos
|
|
16
|
-
from opencos import util,
|
|
17
|
-
from opencos import eda_config
|
|
18
|
-
from opencos import eda_base
|
|
20
|
+
from opencos import util, eda_config, eda_base
|
|
19
21
|
from opencos.eda_base import Tool, which_tool
|
|
20
22
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
+
# Configure util:
|
|
23
24
|
util.progname = "EDA"
|
|
25
|
+
util.global_log.default_log_enabled = True
|
|
26
|
+
util.global_log.default_log_filepath = os.path.join('eda.work', 'eda.log')
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
# ******************************************************************************
|
|
@@ -45,7 +48,7 @@ def init_config(
|
|
|
45
48
|
else:
|
|
46
49
|
config['command_handler'][command] = cls
|
|
47
50
|
|
|
48
|
-
config['auto_tools_found'] =
|
|
51
|
+
config['auto_tools_found'] = {}
|
|
49
52
|
config['tools_loaded'] = set()
|
|
50
53
|
if run_auto_tool_setup:
|
|
51
54
|
config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
|
|
@@ -54,7 +57,7 @@ def init_config(
|
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def usage(tokens: list, config: dict, command=""):
|
|
60
|
+
def usage(tokens: list, config: dict, command="") -> int:
|
|
58
61
|
'''Returns an int shell return code, given remaining args (tokens list) and eda command.
|
|
59
62
|
|
|
60
63
|
config is the config dict. Used to check valid commands in config['command_handler']
|
|
@@ -100,41 +103,44 @@ And <files|targets, ...> is one or more source file or DEPS markup file target,
|
|
|
100
103
|
)
|
|
101
104
|
eda_base.print_base_help()
|
|
102
105
|
return 0
|
|
103
|
-
|
|
106
|
+
|
|
107
|
+
if command in config['command_handler'].keys():
|
|
104
108
|
sco = config['command_handler'][command](config=config) # sub command object
|
|
105
109
|
sco.help(tokens=tokens)
|
|
106
110
|
return util.exit(0)
|
|
107
|
-
else:
|
|
108
|
-
util.info(f"Valid commands are: ")
|
|
109
|
-
for k in sorted(config['command_handler'].keys()):
|
|
110
|
-
util.info(f" {k:20}")
|
|
111
|
-
return util.error(f"Cannot provide help, don't understand command: '{command}'")
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
util.info("Valid commands are:")
|
|
113
|
+
for k in sorted(config['command_handler'].keys()):
|
|
114
|
+
util.info(f" {k:20}")
|
|
115
|
+
return util.error(f"Cannot provide help, don't understand command: '{command}'")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def interactive(config: dict) -> int:
|
|
119
|
+
'''Returns bash/sh return code, entry point for running interactively in CLI if
|
|
120
|
+
|
|
121
|
+
args are not present to directly call a command handler, --help, etc
|
|
122
|
+
'''
|
|
123
|
+
rc = 0
|
|
115
124
|
while True:
|
|
116
|
-
|
|
117
|
-
line = f.readline()
|
|
118
|
-
if line:
|
|
119
|
-
print("%s->%s" % (fname, line), end="")
|
|
120
|
-
else:
|
|
121
|
-
read_file = False
|
|
122
|
-
f.close()
|
|
123
|
-
continue
|
|
124
|
-
else:
|
|
125
|
-
line = input('EDA->')
|
|
125
|
+
line = input('EDA->')
|
|
126
126
|
m = re.match(r'^([^\#]*)\#.*$', line)
|
|
127
|
-
if m:
|
|
127
|
+
if m:
|
|
128
|
+
line = m.group(1)
|
|
128
129
|
tokens = line.split()
|
|
129
130
|
original_args = tokens.copy()
|
|
130
131
|
# NOTE: interactive will not correctly handle --config-yml arg (from eda_config.py),
|
|
131
132
|
# but we should do a best effor to re-parse args from util.py, such as
|
|
132
133
|
# --quiet, --color, --fancy, --logfile, --debug or --debug-level, etc
|
|
133
134
|
_, tokens = util.process_tokens(tokens)
|
|
134
|
-
|
|
135
|
+
rc = process_tokens(
|
|
136
|
+
tokens=tokens, original_args=original_args, config=config, is_interactive=True
|
|
137
|
+
)
|
|
138
|
+
return rc
|
|
135
139
|
|
|
136
140
|
|
|
137
|
-
def auto_tool_setup(
|
|
141
|
+
def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
142
|
+
warnings: bool = True, config = None, quiet: bool = False, tool: str = ''
|
|
143
|
+
) -> dict:
|
|
138
144
|
'''Returns an updated config, uses config['auto_tools_order'][0] dict, calls tool_setup(..)
|
|
139
145
|
|
|
140
146
|
-- adds items to config['tools_loaded'] set
|
|
@@ -150,8 +156,8 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
|
|
|
150
156
|
)
|
|
151
157
|
|
|
152
158
|
assert 'auto_tools_order' in config
|
|
153
|
-
assert
|
|
154
|
-
assert
|
|
159
|
+
assert isinstance(config['auto_tools_order'], list)
|
|
160
|
+
assert isinstance(config['auto_tools_order'][0], dict)
|
|
155
161
|
|
|
156
162
|
for name, value in config['auto_tools_order'][0].items():
|
|
157
163
|
if tool and tool != name:
|
|
@@ -159,12 +165,13 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
|
|
|
159
165
|
|
|
160
166
|
util.debug(f"Checking for ability to run tool: {name}")
|
|
161
167
|
exe = value.get('exe', str())
|
|
162
|
-
if
|
|
168
|
+
if isinstance(exe, list):
|
|
163
169
|
exe_list = exe
|
|
164
|
-
elif
|
|
170
|
+
elif isinstance(exe, str):
|
|
165
171
|
exe_list = [exe] # make it a list
|
|
166
172
|
else:
|
|
167
|
-
util.error(f'eda.py: config["auto_tools_order"][0] for {name=} {value=} has bad type
|
|
173
|
+
util.error(f'eda.py: config["auto_tools_order"][0] for {name=} {value=} has bad type'
|
|
174
|
+
f'for {exe=}')
|
|
168
175
|
continue
|
|
169
176
|
|
|
170
177
|
has_all_py = True
|
|
@@ -200,14 +207,16 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
|
|
|
200
207
|
for cmd in requires_cmd_list:
|
|
201
208
|
cmd_list = shlex.split(cmd)
|
|
202
209
|
try:
|
|
203
|
-
proc = subprocess.run(cmd_list, capture_output=True,
|
|
210
|
+
proc = subprocess.run(cmd_list, capture_output=True, check=False,
|
|
211
|
+
input=b'exit\n\n')
|
|
204
212
|
if proc.returncode != 0:
|
|
205
213
|
if not quiet:
|
|
206
|
-
util.debug(f"For tool {name} missing required command
|
|
214
|
+
util.debug(f"For tool {name} missing required command",
|
|
215
|
+
f"({proc.returncode=}): {cmd_list=}")
|
|
207
216
|
has_all_exe = False
|
|
208
|
-
except:
|
|
217
|
+
except Exception as e:
|
|
209
218
|
has_all_exe = False
|
|
210
|
-
util.debug(f"... No, exception running {cmd_list}")
|
|
219
|
+
util.debug(f"... No, exception {e} running {cmd_list}")
|
|
211
220
|
|
|
212
221
|
|
|
213
222
|
if all([has_all_py, has_all_env, has_all_exe, has_all_in_exe_path]):
|
|
@@ -262,12 +271,11 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
|
|
|
262
271
|
if warnings:
|
|
263
272
|
util.warning(f"tool_setup: {auto_setup=} already setup for {tool}?")
|
|
264
273
|
|
|
265
|
-
entry = config['auto_tools_order'][0].get(tool,
|
|
266
|
-
tool_cmd_handler_dict = entry.get('handlers',
|
|
274
|
+
entry = config['auto_tools_order'][0].get(tool, {})
|
|
275
|
+
tool_cmd_handler_dict = entry.get('handlers', {})
|
|
267
276
|
|
|
268
277
|
for command, str_class_name in tool_cmd_handler_dict.items():
|
|
269
278
|
current_handler_cls = config['command_handler'].get(command, None)
|
|
270
|
-
ext_mod = None
|
|
271
279
|
|
|
272
280
|
if auto_setup and current_handler_cls is not None and issubclass(current_handler_cls, Tool):
|
|
273
281
|
# If we're not in auto_setup, then always override (aka arg --tool=<this tool>)
|
|
@@ -276,19 +284,28 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
|
|
|
276
284
|
|
|
277
285
|
cls = util.import_class_from_string(str_class_name)
|
|
278
286
|
|
|
279
|
-
assert issubclass(cls, Tool),
|
|
287
|
+
assert issubclass(cls, Tool), \
|
|
288
|
+
f'{str_class_name=} is does not have Tool class associated with it'
|
|
280
289
|
util.debug(f'Setting {cls=} for {command=} in config.command_handler')
|
|
281
290
|
config['command_handler'][command] = cls
|
|
282
291
|
|
|
283
292
|
config['tools_loaded'].add(tool)
|
|
284
293
|
|
|
285
294
|
|
|
286
|
-
def process_tokens(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
def process_tokens( # pylint: disable=too-many-branches,too-many-statements
|
|
296
|
+
tokens: list, original_args: list, config: dict, is_interactive=False
|
|
297
|
+
) -> int:
|
|
298
|
+
'''Returns bash/sh style return code int (0 pass, non-zero fail).
|
|
299
|
+
|
|
300
|
+
This is the top level token processing function, and entry point (after util and eda_config
|
|
301
|
+
have performed their argparsing). Tokens can come from command line, DEPS target markup file,
|
|
302
|
+
or interactively. We do one pass through all the tokens, triaging them into:
|
|
303
|
+
- those we can execute immediate (help, quit, and global opens like --debug, --color)
|
|
304
|
+
-- some of this has already been performed in main() by util for --color.
|
|
305
|
+
- a command (sim, synth, etc)
|
|
306
|
+
- command arguments (--seed, +define, +incdir, etc) which will be deferred and processed by
|
|
307
|
+
the command. Some are processed here (--tool)
|
|
308
|
+
'''
|
|
292
309
|
|
|
293
310
|
deferred_tokens = []
|
|
294
311
|
command = ""
|
|
@@ -328,7 +345,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
328
345
|
unparsed.remove(value) # remove command (flist, export, targets, etc)
|
|
329
346
|
break
|
|
330
347
|
|
|
331
|
-
if not
|
|
348
|
+
if not is_interactive:
|
|
332
349
|
# Run init_config() now, we deferred it in main(), but only run it
|
|
333
350
|
# for this tool (or tool=None to figure it out)
|
|
334
351
|
config = init_config(
|
|
@@ -336,7 +353,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
336
353
|
run_auto_tool_setup=run_auto_tool_setup
|
|
337
354
|
)
|
|
338
355
|
if not config:
|
|
339
|
-
util.error(f'eda.py main: problem loading config, {
|
|
356
|
+
util.error(f'eda.py main: problem loading config, {tokens=}')
|
|
340
357
|
return 3
|
|
341
358
|
|
|
342
359
|
# Deal with help, now that we have the command (if it was set).
|
|
@@ -354,9 +371,9 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
354
371
|
tool_setup(parsed.tool, config=config)
|
|
355
372
|
|
|
356
373
|
deferred_tokens = unparsed
|
|
357
|
-
if command
|
|
374
|
+
if not command:
|
|
358
375
|
util.error("Didn't get a command!")
|
|
359
|
-
return
|
|
376
|
+
return 2
|
|
360
377
|
|
|
361
378
|
sco = config['command_handler'][command](config=config) # sub command object
|
|
362
379
|
util.debug(f'{command=}')
|
|
@@ -377,16 +394,23 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
377
394
|
# Add the original, nothing-parsed args to the Command.config dict.
|
|
378
395
|
sco.config['eda_original_args'] = original_args
|
|
379
396
|
|
|
380
|
-
setattr(sco, 'command_name', command) # as a safeguard,
|
|
397
|
+
setattr(sco, 'command_name', command) # as a safeguard, 'command' is not always passed to 'sco'
|
|
381
398
|
unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
|
|
382
399
|
|
|
383
|
-
# query the status from the Command object (0 is pass, > 0 is fail
|
|
384
|
-
rc
|
|
400
|
+
# query the status from the Command object (0 is pass, > 0 is fail, but we'd prefer to avoid
|
|
401
|
+
# rc=1 because that's the python exception rc)
|
|
402
|
+
rc = getattr(sco, 'status', 2)
|
|
385
403
|
util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
|
|
386
404
|
return rc
|
|
387
405
|
|
|
388
406
|
|
|
389
407
|
def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> int:
|
|
408
|
+
'''Returns bash/sh return code, checks that a command handling class has all
|
|
409
|
+
|
|
410
|
+
internal CHECK_REQUIRES list items. For example, sim.py has CHECK_REQUIRES=[Tool],
|
|
411
|
+
so if a 'sim' command handler does not also inherit a Tool class, then reports this as an
|
|
412
|
+
error.
|
|
413
|
+
'''
|
|
390
414
|
sco = command_obj
|
|
391
415
|
for cls in getattr(sco, 'CHECK_REQUIRES', []):
|
|
392
416
|
if not isinstance(sco, cls):
|
|
@@ -404,7 +428,8 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
|
|
|
404
428
|
# **************************************************************
|
|
405
429
|
# **** Interrupt Handler
|
|
406
430
|
|
|
407
|
-
def signal_handler(sig, frame):
|
|
431
|
+
def signal_handler(sig, frame) -> None: # pylint: disable=unused-argument
|
|
432
|
+
'''Handles Ctrl-C, called by main_cli() if running from command line'''
|
|
408
433
|
util.fancy_stop()
|
|
409
434
|
util.info('Received Ctrl+C...', start='\nINFO: [EDA] ')
|
|
410
435
|
util.exit(-1)
|
|
@@ -436,6 +461,8 @@ def main(*args):
|
|
|
436
461
|
|
|
437
462
|
if not util.args['quiet']:
|
|
438
463
|
util.info(f'eda: version {opencos.__version__}')
|
|
464
|
+
# And show the command that was run (all args):
|
|
465
|
+
util.info(f"main: eda {' '.join(args)}; (run from {os.getcwd()})")
|
|
439
466
|
|
|
440
467
|
# Handle --config-yml= arg
|
|
441
468
|
config, unparsed = eda_config.get_eda_config(unparsed)
|
|
@@ -448,15 +475,20 @@ def main(*args):
|
|
|
448
475
|
if len(args) == 0 or (len(args) == 1 and '--debug' in args):
|
|
449
476
|
# special snowflake case if someone called with a singular arg --debug
|
|
450
477
|
# (without --help or exit)
|
|
451
|
-
util.debug(
|
|
478
|
+
util.debug("Starting automatic tool setup: init_config()")
|
|
452
479
|
config = init_config(config=config)
|
|
453
480
|
if not config:
|
|
454
481
|
util.error(f'eda.py main: problem loading config, {args=}')
|
|
455
482
|
return 3
|
|
456
|
-
|
|
483
|
+
main_ret = interactive(config=config)
|
|
457
484
|
else:
|
|
458
|
-
|
|
459
|
-
|
|
485
|
+
main_ret = process_tokens(
|
|
486
|
+
tokens=list(unparsed), original_args=original_args, config=config
|
|
487
|
+
)
|
|
488
|
+
# Stop the util log, needed for pytests that call eda.main directly that otherwise
|
|
489
|
+
# won't close the log file via util's atexist.register(stop_log)
|
|
490
|
+
util.global_log.stop()
|
|
491
|
+
return main_ret
|
|
460
492
|
|
|
461
493
|
|
|
462
494
|
def main_cli() -> None:
|
|
@@ -470,10 +502,3 @@ def main_cli() -> None:
|
|
|
470
502
|
|
|
471
503
|
if __name__ == '__main__':
|
|
472
504
|
main_cli()
|
|
473
|
-
|
|
474
|
-
# IDEAS:
|
|
475
|
-
# * options with no default (i.e. if user doesn't override, THEN we set it, like "seed" or "work-dir") can be given a
|
|
476
|
-
# special type (DefaultVar) versus saying "None" so that help can say more about it (it's a string, it's default val
|
|
477
|
-
# is X, etc) and it can be queried as to whether it's really a default val. This avoids having to avoid default vals
|
|
478
|
-
# that user can never set (-1, None, etc) which make it hard to infer the type. this same object can be given help
|
|
479
|
-
# info and simply "render" to the expected type (str, integer, etc) when used.
|