Skip to content

Commit b261606

Browse files
authored
Merge pull request #523 from boriel/feature/use-config-file
Refactorize Options container.
2 parents c7f9421 + c0b496b commit b261606

15 files changed

Lines changed: 186 additions & 88 deletions

src/api/config.py

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,25 @@
1818
# The options container
1919
from . import options
2020
from . import global_
21-
from .options import ANYTYPE
21+
22+
from .options import ANYTYPE, Actions
2223

2324

2425
# ------------------------------------------------------
2526
# Common setup and configuration for all tools
2627
# ------------------------------------------------------
28+
class ConfigSections:
29+
ZXBC = 'zxbc'
30+
ZXBASM = 'zxbasm'
31+
ZXBPP = 'zxbpp'
32+
33+
2734
class OPTION:
2835
OUTPUT_FILENAME = 'output_filename'
2936
INPUT_FILENAME = 'input_filename'
3037
STDERR_FILENAME = 'stderr_filename'
3138
DEBUG = 'debug_level'
39+
PROJECT_FILENAME = 'project_filename'
3240

3341
# File IO
3442
STDIN = 'stdin'
@@ -70,6 +78,10 @@ class OPTION:
7078

7179

7280
OPTIONS = options.Options()
81+
OPTIONS_NOT_SAVED = {
82+
OPTION.STDERR, OPTION.STDIN, OPTION.STDOUT, 'sinclair', OPTION.INPUT_FILENAME, OPTION.OUTPUT_FILENAME,
83+
OPTION.PROJECT_FILENAME, 'heap_start_label', 'heap_size_label'
84+
}
7385

7486

7587
def load_config_from_file(filename: str, section: str, options_: options.Options = None, stop_on_error=True) -> bool:
@@ -107,7 +119,7 @@ def load_config_from_file(filename: str, section: str, options_: options.Options
107119
}
108120

109121
for opt in cfg.options(section):
110-
options_[opt].value = parsing.get(options_[opt].type, cfg.get)(option=opt)
122+
options_[opt].value = parsing.get(options_[opt].type, cfg.get)(section=section, option=opt)
111123

112124
return True
113125

@@ -131,8 +143,8 @@ def save_config_into_file(filename: str, section: str, options_: options.Options
131143
return False
132144

133145
cfg[section] = {}
134-
for opt_name, opt in options.Options.get_options(options_):
135-
if opt_name.startswith('__') or opt.value is None or opt_name in ('stderr', 'stdin', 'stdout'):
146+
for opt_name, opt in options_().items():
147+
if opt_name.startswith('__') or opt.value is None or opt_name in OPTIONS_NOT_SAVED:
136148
continue
137149

138150
if opt.type == bool:
@@ -164,48 +176,53 @@ def init():
164176
param_byref --Default parameter passing. TRUE => By Reference
165177
"""
166178

167-
OPTIONS.reset()
179+
OPTIONS(Actions.CLEAR)
168180

169-
OPTIONS.add_option(OPTION.OUTPUT_FILENAME, str)
170-
OPTIONS.add_option(OPTION.INPUT_FILENAME, str)
171-
OPTIONS.add_option(OPTION.STDERR_FILENAME, str)
172-
OPTIONS.add_option(OPTION.DEBUG, int, 0)
181+
OPTIONS(Actions.ADD, name=OPTION.OUTPUT_FILENAME, type=str)
182+
OPTIONS(Actions.ADD, name=OPTION.INPUT_FILENAME, type=str)
183+
OPTIONS(Actions.ADD, name=OPTION.STDERR_FILENAME, type=str)
184+
OPTIONS(Actions.ADD, name=OPTION.DEBUG, type=int, default=0)
173185

174186
# Default console redirections
175-
OPTIONS.add_option(OPTION.STDIN, ANYTYPE, sys.stdin)
176-
OPTIONS.add_option(OPTION.STDOUT, ANYTYPE, sys.stdout)
177-
OPTIONS.add_option(OPTION.STDERR, ANYTYPE, sys.stderr)
178-
179-
OPTIONS.add_option(OPTION.O_LEVEL, int, global_.DEFAULT_OPTIMIZATION_LEVEL)
180-
OPTIONS.add_option(OPTION.CASE_INS, bool, False)
181-
OPTIONS.add_option(OPTION.ARRAY_BASE, int, 0)
182-
OPTIONS.add_option(OPTION.DEFAULT_BYREF, bool, False)
183-
OPTIONS.add_option(OPTION.MAX_SYN_ERRORS, int, global_.DEFAULT_MAX_SYNTAX_ERRORS)
184-
OPTIONS.add_option(OPTION.STR_BASE, int, 0)
185-
OPTIONS.add_option(OPTION.MEMORY_MAP, str, None)
186-
OPTIONS.add_option(OPTION.FORCE_ASM_BRACKET, bool, False)
187-
188-
OPTIONS.add_option(OPTION.USE_BASIC_LOADER, bool, False) # Whether to use a loader
189-
OPTIONS.add_option(OPTION.AUTORUN, bool, False) # Whether to add autostart code (needs basic loader = true)
190-
OPTIONS.add_option(OPTION.OUTPUT_FILE_TYPE, str, 'bin') # bin, tap, tzx etc...
191-
OPTIONS.add_option(OPTION.INCLUDE_PATH, str, '') # Include path, like '/var/lib:/var/include'
192-
193-
OPTIONS.add_option(OPTION.CHECK_MEMORY, bool, False)
194-
OPTIONS.add_option(OPTION.STRICT_BOOL, bool, False)
195-
OPTIONS.add_option(OPTION.CHECK_ARRAYS, bool, False)
196-
197-
OPTIONS.add_option(OPTION.ENABLE_BREAK, bool, False)
198-
OPTIONS.add_option(OPTION.EMIT_BACKEND, bool, False)
199-
OPTIONS.add_option('__DEFINES', dict, {})
200-
OPTIONS.add_option(OPTION.EXPLICIT, bool, False)
201-
OPTIONS.add_option('Sinclair', bool, False)
202-
OPTIONS.add_option(OPTION.STRICT, bool, False) # True to force type checking
203-
OPTIONS.add_option(OPTION.ASM_ZXNEXT, bool, False) # True to enable ZX Next ASM opcodes
204-
OPTIONS.add_option(OPTION.ARCH, str, None) # Architecture
205-
OPTIONS.add_option(OPTION.EXPECTED_WARNINGS, int, 0) # Expected Warnings that will be silenced
206-
OPTIONS.add_option(OPTION.HIDE_WARNING_CODES, bool, False) # Whether to show WXXX warning codes or not
207-
208-
save_config_into_file('project.ini', 'zxbc', stop_on_error=True)
187+
OPTIONS(Actions.ADD, name=OPTION.STDIN, type=ANYTYPE, default=sys.stdin)
188+
OPTIONS(Actions.ADD, name=OPTION.STDOUT, type=ANYTYPE, default=sys.stdout)
189+
OPTIONS(Actions.ADD, name=OPTION.STDERR, type=ANYTYPE, default=sys.stderr)
190+
191+
OPTIONS(Actions.ADD, name=OPTION.O_LEVEL, type=int, default=global_.DEFAULT_OPTIMIZATION_LEVEL)
192+
OPTIONS(Actions.ADD, name=OPTION.CASE_INS, type=bool, default=False)
193+
OPTIONS(Actions.ADD, name=OPTION.ARRAY_BASE, type=int, default=0)
194+
OPTIONS(Actions.ADD, name=OPTION.DEFAULT_BYREF, type=bool, default=False)
195+
OPTIONS(Actions.ADD, name=OPTION.MAX_SYN_ERRORS, type=int, default=global_.DEFAULT_MAX_SYNTAX_ERRORS)
196+
OPTIONS(Actions.ADD, name=OPTION.STR_BASE, type=int, default=0)
197+
OPTIONS(Actions.ADD, name=OPTION.MEMORY_MAP, type=str, default=None)
198+
OPTIONS(Actions.ADD, name=OPTION.FORCE_ASM_BRACKET, type=bool, default=False)
199+
200+
OPTIONS(Actions.ADD, name=OPTION.USE_BASIC_LOADER, type=bool, default=False) # Whether to use a loader
201+
202+
# Whether to add autostart code (needs basic loader = true)
203+
OPTIONS(Actions.ADD, name=OPTION.AUTORUN, type=bool, default=False)
204+
OPTIONS(Actions.ADD, name=OPTION.OUTPUT_FILE_TYPE, type=str, default='bin') # bin, tap, tzx etc...
205+
OPTIONS(Actions.ADD, name=OPTION.INCLUDE_PATH, type=str, default='') # Include path, like '/var/lib:/var/include'
206+
207+
OPTIONS(Actions.ADD, name=OPTION.CHECK_MEMORY, type=bool, default=False)
208+
OPTIONS(Actions.ADD, name=OPTION.STRICT_BOOL, type=bool, default=False)
209+
OPTIONS(Actions.ADD, name=OPTION.CHECK_ARRAYS, type=bool, default=False)
210+
211+
OPTIONS(Actions.ADD, name=OPTION.ENABLE_BREAK, type=bool, default=False)
212+
OPTIONS(Actions.ADD, name=OPTION.EMIT_BACKEND, type=bool, default=False)
213+
OPTIONS(Actions.ADD, name='__DEFINES', type=dict, default={})
214+
OPTIONS(Actions.ADD, name=OPTION.EXPLICIT, type=bool, default=False)
215+
OPTIONS(Actions.ADD, name='sinclair', type=bool, default=False)
216+
OPTIONS(Actions.ADD, name=OPTION.STRICT, type=bool, default=False) # True to force type checking
217+
OPTIONS(Actions.ADD, name=OPTION.ASM_ZXNEXT, type=bool, default=False) # True to enable ZX Next ASM opcodes
218+
OPTIONS(Actions.ADD, name=OPTION.ARCH, type=str, default=None) # Architecture
219+
OPTIONS(Actions.ADD, name=OPTION.EXPECTED_WARNINGS, type=int, default=0) # Expected Warnings that will be silenced
220+
221+
# Whether to show WXXX warning codes or not
222+
OPTIONS(Actions.ADD, name=OPTION.HIDE_WARNING_CODES, type=bool, default=False)
223+
224+
OPTIONS(Actions.ADD, name=OPTION.PROJECT_FILENAME, type=str, default=os.path.join(os.path.abspath(os.path.curdir),
225+
'project.ini'))
209226

210227

211228
init()

src/api/options.py

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
from typing import Dict
1515
from typing import List
1616
from typing import Any
17-
from typing import Tuple
1817

1918
from .errors import Error
2019

21-
__all__ = ['Option', 'Options', 'ANYTYPE']
20+
__all__ = ['Option', 'Options', 'ANYTYPE', 'Actions']
2221

2322

2423
class ANYTYPE:
@@ -31,7 +30,6 @@ class ANYTYPE:
3130
# Exception for duplicated Options
3231
# ----------------------------------------------------------------------
3332

34-
3533
class DuplicatedOptionError(Error):
3634
def __init__(self, option_name):
3735
self.option = option_name
@@ -75,6 +73,24 @@ def __str__(self):
7573
return "Invalid value for config initialization"
7674

7775

76+
class InvalidActionParameterError(Error):
77+
def __init__(self, action, invalid_parameter):
78+
self.invalid_parameter = invalid_parameter
79+
self.action = action
80+
81+
def __str__(self):
82+
return f"Action '{self.action}' does not accept parameter '{self.invalid_parameter}'"
83+
84+
85+
class InvalidActionMissingParameterError(Error):
86+
def __init__(self, action, missing_parameter):
87+
self.missing_parameter = missing_parameter
88+
self.action = action
89+
90+
def __str__(self):
91+
return f"Action '{self.action}' requires parameter '{self.missing_parameter}'"
92+
93+
7894
# ----------------------------------------------------------------------
7995
# This class interfaces an Options Container
8096
# ----------------------------------------------------------------------
@@ -137,6 +153,18 @@ def pop(self) -> Any:
137153
return result
138154

139155

156+
# ----------------------------------------------------------------------
157+
# Options commands
158+
# ----------------------------------------------------------------------
159+
class Actions:
160+
ADD = 'add'
161+
ADD_IF_NOT_DEFINED = 'add_if_not_defined'
162+
CLEAR = 'clear'
163+
LIST = 'list'
164+
165+
allowed = {ADD, CLEAR, LIST, ADD_IF_NOT_DEFINED}
166+
167+
140168
# ----------------------------------------------------------------------
141169
# This class interfaces an Options Container
142170
# ----------------------------------------------------------------------
@@ -154,24 +182,21 @@ def __init__(self, init_value=None):
154182
else:
155183
raise InvalidConfigInitialization(invalid_value=init_value)
156184

157-
def reset(self):
158-
self._options.clear()
159-
160-
def add_option(self, name, type_=None, default_value=None):
185+
def __add_option(self, name, type_=None, default=None):
161186
if name in self._options:
162187
raise DuplicatedOptionError(name)
163188

164-
if type_ is None and default_value is not None:
165-
type_ = type(default_value)
189+
if type_ is None and default is not None:
190+
type_ = type(default)
166191
elif type_ is ANYTYPE:
167192
type_ = None
168193

169-
self._options[name] = Option(name, type_, default_value)
194+
self._options[name] = Option(name, type_, default)
170195

171-
def add_option_if_not_defined(self, name, type_=None, default_value=None):
196+
def __add_option_if_not_defined(self, name, type_=None, default=None):
172197
if name in self._options:
173198
return
174-
self.add_option(name, type_, default_value)
199+
self.__add_option(name, type_, default)
175200

176201
def __delattr__(self, item: str):
177202
del self[item]
@@ -207,8 +232,51 @@ def __setitem__(self, key: str, value: Any):
207232
def __contains__(self, item: str):
208233
return item in self._options
209234

210-
@classmethod
211-
def get_options(cls, instance: 'Options') -> List[Tuple[str, Option]]:
212-
""" Iterate over all options of the given instance
235+
def __call__(self, *args, **kwargs):
236+
""" Multipurpose function.
237+
- With no parameters, returns a dictionary {'option' -> value}
238+
- With a command:
239+
'add', name='xxxx', type_=None, default_value=None <= Creates the option 'xxxx', if_not_defined=False
240+
'reset', clears the container
213241
"""
214-
return list(instance._options.items())
242+
def check_allowed_args(action: str, kwargs_, allowed_args, required_args=None):
243+
for option in kwargs_.keys():
244+
if option not in allowed_args:
245+
raise InvalidActionParameterError(action, option)
246+
247+
for required_option in required_args or []:
248+
if required_option not in kwargs_:
249+
raise InvalidActionMissingParameterError(action, required_option)
250+
251+
# With no parameters
252+
if not kwargs:
253+
if not args or args == (Actions.LIST, ):
254+
return {x: y for x, y in self._options.items()}
255+
256+
assert args, f"Missing one action of {', '.join(Actions.allowed)}"
257+
assert len(args) == 1 and args[0] in Actions.allowed, \
258+
f"Only one action of {', '.join(Actions.allowed)} can be specified"
259+
260+
# clear
261+
if args[0] == Actions.CLEAR:
262+
check_allowed_args(Actions.CLEAR, kwargs, {})
263+
self._options.clear()
264+
return
265+
266+
# list
267+
if args[0] == Actions.LIST:
268+
check_allowed_args(Actions.LIST, kwargs, {'options'})
269+
options = set(kwargs['options'])
270+
return {x: y for x, y in self._options.items() if x in options}
271+
272+
if args[0] == Actions.ADD:
273+
kwargs['type'] = kwargs.get('type')
274+
kwargs['default'] = kwargs.get('default')
275+
check_allowed_args(Actions.ADD, kwargs, {'name', 'type', 'default'}, ['name'])
276+
self.__add_option(kwargs['name'], kwargs['type'], kwargs['default'])
277+
278+
if args[0] == Actions.ADD_IF_NOT_DEFINED:
279+
kwargs['type'] = kwargs.get('type')
280+
kwargs['default'] = kwargs.get('default')
281+
check_allowed_args(Actions.ADD, kwargs, {'name', 'type', 'default'}, ['name'])
282+
self.__add_option_if_not_defined(kwargs['name'], kwargs['type'], kwargs['default'])

src/arch/zx48k/backend/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
# External functions
9696
from ..optimizer.helpers import HI16, LO16
9797
from src.arch.zx48k.optimizer.asm import Asm
98-
from src.api.config import OPTIONS
98+
from src.api.config import OPTIONS, Actions
9999
from src.arch.zx48k.peephole import engine
100100

101101
import src.api.fp
@@ -134,10 +134,10 @@
134134
FLAG_end_emitted = False
135135

136136
# Default code ORG
137-
OPTIONS.add_option_if_not_defined('org', int, 32768)
137+
OPTIONS(Actions.ADD_IF_NOT_DEFINED, name='org', type=int, default=32768)
138138

139139
# Default HEAP SIZE (Dynamic memory) in bytes
140-
OPTIONS.add_option_if_not_defined('heap_size', int, 4768) # A bit more than 4K
140+
OPTIONS(Actions.ADD_IF_NOT_DEFINED, name='heap_size', type=int, default=4768) # A bit more than 4K
141141

142142
# List of modules (in alphabetical order) that, if included, should call MEM_INIT
143143
MEMINITS = {
@@ -196,15 +196,15 @@ def init():
196196
FLAG_end_emitted = False
197197

198198
# Default code ORG
199-
OPTIONS.add_option('org', int, 32768)
199+
OPTIONS(Actions.ADD, name='org', type=int, default=32768)
200200
# Default HEAP SIZE (Dynamic memory) in bytes
201-
OPTIONS.add_option('heap_size', int, 4768) # A bit more than 4K
201+
OPTIONS(Actions.ADD, name='heap_size', type=int, default=4768) # A bit more than 4K
202202
# Labels for HEAP START (might not be used if not needed)
203-
OPTIONS.add_option('heap_start_label', str, f'{NAMESPACE}.ZXBASIC_MEM_HEAP')
203+
OPTIONS(Actions.ADD, name='heap_start_label', type=str, default=f'{NAMESPACE}.ZXBASIC_MEM_HEAP')
204204
# Labels for HEAP SIZE (might not be used if not needed)
205-
OPTIONS.add_option('heap_size_label', str, f'{NAMESPACE}.ZXBASIC_HEAP_SIZE')
205+
OPTIONS(Actions.ADD, name='heap_size_label', type=str, default=f'{NAMESPACE}.ZXBASIC_HEAP_SIZE')
206206
# Flag for headerless mode (No prologue / epilogue)
207-
OPTIONS.add_option('headerless', bool, False)
207+
OPTIONS(Actions.ADD, name='headerless', type=bool, default=False)
208208

209209
engine.main() # inits the optimizer
210210

src/zxbc/args_parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,7 @@ def parser() -> argparse.ArgumentParser:
9898
help='Disables warning WXXX (i.e. -W100 disables warning with code W100)')
9999
parser_.add_argument('--hide-warning-codes', action='store_true',
100100
help='Hides WXXX codes')
101+
parser_.add_argument('-F', '--config-file', type=str, default=OPTIONS.project_filename,
102+
help="Loads config from config file")
103+
parser_.add_argument('--save-config', type=str, help="Save options into a config file")
101104
return parser_

src/zxbc/zxbc.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def main(args=None, emitter=None):
8787
parser = args_parser.parser()
8888
options = parser.parse_args(args=args)
8989

90+
if os.path.isfile(options.config_file):
91+
if src.api.config.load_config_from_file(options.config_file, src.api.config.ConfigSections.ZXBC):
92+
src.api.errmsg.info(f"Config file {options.config_file} loaded")
93+
9094
# ------------------------------------------------------------
9195
# Setting of internal parameters according to command line
9296
# ------------------------------------------------------------
@@ -96,10 +100,10 @@ def main(args=None, emitter=None):
96100
OPTIONS.stderr_filename = options.stderr
97101
OPTIONS.array_base = options.array_base
98102
OPTIONS.string_base = options.string_base
99-
OPTIONS.Sinclair = options.sinclair
103+
OPTIONS.sinclair = options.sinclair
100104
OPTIONS.heap_size = options.heap_size
101105
OPTIONS.memory_check = options.debug_memory
102-
OPTIONS.strict_bool = options.strict_bool or OPTIONS.Sinclair
106+
OPTIONS.strict_bool = options.strict_bool or OPTIONS.sinclair
103107
OPTIONS.array_check = options.debug_array
104108
OPTIONS.emit_backend = options.emit_backend
105109
OPTIONS.enable_break = options.enable_break
@@ -149,7 +153,7 @@ def main(args=None, emitter=None):
149153
OPTIONS.__DEFINES[name] = val
150154
zxbpp.ID_TABLE.define(name, value=val, lineno=0)
151155

152-
if OPTIONS.Sinclair:
156+
if OPTIONS.sinclair:
153157
OPTIONS.array_base = 1
154158
OPTIONS.string_base = 1
155159
OPTIONS.strictBool = True
@@ -334,6 +338,9 @@ def main(args=None, emitter=None):
334338
with open_file(OPTIONS.memory_map, 'wt', 'utf-8') as f:
335339
f.write(asmparse.MEMORY.memory_map)
336340

341+
if not gl.has_errors and options.save_config:
342+
src.api.config.save_config_into_file(options.save_config, src.api.config.ConfigSections.ZXBC)
343+
337344
return gl.has_errors # Exit success
338345

339346

0 commit comments

Comments
 (0)