Skip to content

Commit 0403245

Browse files
authored
Merge pull request #553 from boriel/bugfix/config_file_options
Bugfix/config file options
2 parents 9d9e67b + c807698 commit 0403245

File tree

8 files changed

+214
-118
lines changed

8 files changed

+214
-118
lines changed

poetry.lock

Lines changed: 122 additions & 83 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/config.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,32 @@
1212
import os
1313
import sys
1414
import configparser
15+
import enum
1516

16-
from src import api
17+
from enum import Enum
18+
from typing import Dict, Callable
19+
from src.api import errmsg
1720

1821
# The options container
19-
from . import options
20-
from . import global_
2122

22-
from .options import ANYTYPE, Action
23+
from src.api import options
24+
from src.api import global_
25+
26+
from src.api.options import ANYTYPE, Action
2327

2428

2529
# ------------------------------------------------------
2630
# Common setup and configuration for all tools
2731
# ------------------------------------------------------
28-
class ConfigSections:
32+
@enum.unique
33+
class ConfigSections(str, Enum):
2934
ZXBC = 'zxbc'
3035
ZXBASM = 'zxbasm'
3136
ZXBPP = 'zxbpp'
3237

3338

34-
class OPTION:
39+
@enum.unique
40+
class OPTION(str, Enum):
3541
OUTPUT_FILENAME = 'output_filename'
3642
INPUT_FILENAME = 'input_filename'
3743
STDERR_FILENAME = 'stderr_filename'
@@ -96,23 +102,23 @@ def load_config_from_file(filename: str, section: str, options_: options.Options
96102
cfg = configparser.ConfigParser()
97103
cfg.read(filename, encoding='utf-8')
98104
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError):
99-
api.errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields")
105+
errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields")
100106
if stop_on_error:
101107
sys.exit(1)
102108
return False
103109
except FileNotFoundError:
104-
api.errmsg.msg_output(f"Config file '{filename}' not found")
110+
errmsg.msg_output(f"Config file '{filename}' not found")
105111
if stop_on_error:
106112
sys.exit(1)
107113
return False
108114

109115
if section not in cfg.sections():
110-
api.errmsg.msg_output(f"Section '{section}' not found in config file '{filename}'")
116+
errmsg.msg_output(f"Section '{section}' not found in config file '{filename}'")
111117
if stop_on_error:
112118
sys.exit(1)
113119
return False
114120

115-
parsing = {
121+
parsing: Dict[type, Callable] = {
116122
int: cfg.getint,
117123
float: cfg.getfloat,
118124
bool: cfg.getboolean
@@ -137,7 +143,7 @@ def save_config_into_file(filename: str, section: str, options_: options.Options
137143
try:
138144
cfg.read(filename, encoding='utf-8')
139145
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError):
140-
api.errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields")
146+
errmsg.msg_output(f"Invalid config file '{filename}': it has duplicated fields")
141147
if stop_on_error:
142148
sys.exit(1)
143149
return False
@@ -157,7 +163,7 @@ def save_config_into_file(filename: str, section: str, options_: options.Options
157163
with open(filename, 'wt', encoding='utf-8') as f:
158164
cfg.write(f)
159165
except IOError:
160-
api.errmsg.msg_output(f"Can't write config file '{filename}'")
166+
errmsg.msg_output(f"Can't write config file '{filename}'")
161167
if stop_on_error:
162168
sys.exit(1)
163169
return False
@@ -189,10 +195,10 @@ def init():
189195
OPTIONS(Action.ADD, name=OPTION.MEMORY_MAP, type=str, default=None, ignore_none=True)
190196
OPTIONS(Action.ADD, name=OPTION.FORCE_ASM_BRACKET, type=bool, default=False, ignore_none=True)
191197

192-
OPTIONS(Action.ADD, name=OPTION.USE_BASIC_LOADER, type=bool, default=False) # Whether to use a loader
198+
OPTIONS(Action.ADD, name=OPTION.USE_BASIC_LOADER, type=bool, default=False, ignore_none=True)
193199

194200
# Whether to add autostart code (needs basic loader = true)
195-
OPTIONS(Action.ADD, name=OPTION.AUTORUN, type=bool, default=False)
201+
OPTIONS(Action.ADD, name=OPTION.AUTORUN, type=bool, default=False, ignore_none=True)
196202
OPTIONS(Action.ADD, name=OPTION.OUTPUT_FILE_TYPE, type=str, default='bin') # bin, tap, tzx etc...
197203
OPTIONS(Action.ADD, name=OPTION.INCLUDE_PATH, type=str, default='') # Include path, like '/var/lib:/var/include'
198204

src/api/errmsg.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from typing import Optional
1717

1818
from src.api import global_
19+
from src.api import config
1920

2021
from src.api.constants import CLASS
21-
from src.api.config import OPTIONS
2222

2323

2424
# Exports only these functions. Others
@@ -39,14 +39,14 @@ def msg_output(msg: str) -> None:
3939
if msg in global_.error_msg_cache:
4040
return
4141

42-
OPTIONS.stderr.write("%s\n" % msg)
42+
config.OPTIONS.stderr.write("%s\n" % msg)
4343
global_.error_msg_cache.add(msg)
4444

4545

4646
def info(msg: str) -> None:
47-
if OPTIONS.debug_level < 1:
47+
if config.OPTIONS.debug_level < 1:
4848
return
49-
OPTIONS.stderr.write("info: %s\n" % msg)
49+
config.OPTIONS.stderr.write("info: %s\n" % msg)
5050

5151

5252
def error(lineno: int, msg: str, fname: Optional[str] = None) -> None:
@@ -55,13 +55,13 @@ def error(lineno: int, msg: str, fname: Optional[str] = None) -> None:
5555
if fname is None:
5656
fname = global_.FILENAME
5757

58-
if global_.has_errors > OPTIONS.max_syntax_errors:
58+
if global_.has_errors > config.OPTIONS.max_syntax_errors:
5959
msg = 'Too many errors. Giving up!'
6060

6161
msg = "%s:%i: error:%s %s" % (fname, lineno, ERROR_PREFIX, msg)
6262
msg_output(msg)
6363

64-
if global_.has_errors > OPTIONS.max_syntax_errors:
64+
if global_.has_errors > config.OPTIONS.max_syntax_errors:
6565
sys.exit(1)
6666

6767
global_.has_errors += 1
@@ -71,7 +71,7 @@ def warning(lineno: int, msg: str, fname: Optional[str] = None) -> None:
7171
""" Generic warning error routine
7272
"""
7373
global_.has_warnings += 1
74-
if global_.has_warnings <= OPTIONS.expected_warnings:
74+
if global_.has_warnings <= config.OPTIONS.expected_warnings:
7575
return
7676

7777
if fname is None:
@@ -107,7 +107,7 @@ def decorator(func: Callable) -> Callable:
107107
def wrapper(*args, **kwargs):
108108
global WARNING_PREFIX
109109
if global_.ENABLED_WARNINGS.get(code, True):
110-
if not OPTIONS.hide_warning_codes:
110+
if not config.OPTIONS.hide_warning_codes:
111111
WARNING_PREFIX = f'warning: [W{code}]'
112112
func(*args, **kwargs)
113113
WARNING_PREFIX = ''
@@ -122,7 +122,7 @@ def wrapper(*args, **kwargs):
122122
def warning_implicit_type(lineno: int, id_: str, type_: str = None):
123123
""" Warning: Using default implicit type 'x'
124124
"""
125-
if OPTIONS.strict:
125+
if config.OPTIONS.strict:
126126
syntax_error_undeclared_type(lineno, id_)
127127
return
128128

@@ -164,7 +164,7 @@ def warning_empty_if(lineno: int):
164164
def warning_not_used(lineno: int, id_: str, kind: str = 'Variable', fname: Optional[str] = None):
165165
""" Emits an optimization warning
166166
"""
167-
if OPTIONS.optimization_level > 0:
167+
if config.OPTIONS.optimization_level > 0:
168168
warning(lineno, "%s '%s' is never used" % (kind, id_), fname=fname)
169169

170170

src/api/global_.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@
188188

189189

190190
# ----------------------------------------------------------------------
191-
# Warning codes and whether they're enabled or not
191+
# Warning options
192192
# ----------------------------------------------------------------------
193+
194+
# Warning codes and whether they're enabled or not
193195
ENABLED_WARNINGS: Dict[str, bool] = {}
196+
197+
# Number of expected warnings (won't be issued)
198+
EXPECTED_WARNINGS: int = 0

src/api/options.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
# ----------------------------------------------------------------------
1111

1212
import json
13+
import enum
1314

1415
from typing import Dict
1516
from typing import List
1617
from typing import Any
1718

18-
from .errors import Error
19+
from src.api.errors import Error
1920

2021
__all__ = ['Option', 'Options', 'ANYTYPE', 'Action']
2122

@@ -161,13 +162,16 @@ def pop(self) -> Any:
161162
# ----------------------------------------------------------------------
162163
# Options commands
163164
# ----------------------------------------------------------------------
164-
class Action:
165+
@enum.unique
166+
class Action(str, enum.Enum):
165167
ADD = 'add'
166168
ADD_IF_NOT_DEFINED = 'add_if_not_defined'
167169
CLEAR = 'clear'
168170
LIST = 'list'
169171

170-
allowed = {ADD, CLEAR, LIST, ADD_IF_NOT_DEFINED}
172+
@classmethod
173+
def valid(cls, action: str) -> bool:
174+
return action in list(cls)
171175

172176

173177
# ----------------------------------------------------------------------
@@ -258,9 +262,9 @@ def check_allowed_args(action: str, kwargs_, allowed_args, required_args=None):
258262
if not args or args == (Action.LIST,):
259263
return {x: y for x, y in self._options.items()}
260264

261-
assert args, f"Missing one action of {', '.join(Action.allowed)}"
262-
assert len(args) == 1 and args[0] in Action.allowed, \
263-
f"Only one action of {', '.join(Action.allowed)} can be specified"
265+
assert args, f"Missing one action of {', '.join(Action)}"
266+
assert len(args) == 1 and Action.valid(args[0]), \
267+
f"Only one action of {', '.join(Action)} can be specified"
264268

265269
# clear
266270
if args[0] == Action.CLEAR:
@@ -282,6 +286,7 @@ def check_allowed_args(action: str, kwargs_, allowed_args, required_args=None):
282286
kwargs['type_'] = kwargs['type']
283287
del kwargs['type']
284288
self.__add_option(**kwargs)
289+
return
285290

286291
if args[0] == Action.ADD_IF_NOT_DEFINED:
287292
kwargs['type'] = kwargs.get('type')

src/zxbc/args_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def parser() -> argparse.ArgumentParser:
4242
output_file_type_group.add_argument('--parse-only', action='store_true',
4343
help='Only parses to check for syntax and semantic errors')
4444

45-
parser_.add_argument('-B', '--BASIC', action='store_true', dest='basic',
45+
parser_.add_argument('-B', '--BASIC', action='store_true', dest='basic', default=None,
4646
help="Creates a BASIC loader which loads the rest of the CODE. Requires -T ot -t")
47-
parser_.add_argument('-a', '--autorun', action='store_true',
47+
parser_.add_argument('-a', '--autorun', action='store_true', default=None,
4848
help="Sets the program to be run once loaded")
4949

5050
parser_.add_argument('-S', '--org', type=str,

tests/api/test_arg_parser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import unittest
2+
3+
from src.zxbc.args_parser import parser
4+
5+
6+
class TestArgParser(unittest.TestCase):
7+
""" Test argument options from the cmdline
8+
"""
9+
def setUp(self):
10+
self.parser = parser()
11+
12+
def test_autorun_defaults_to_none(self):
13+
""" Some boolean options, when not specified from the command line
14+
must return None (null) instead of False to preserve .INI saved
15+
value.
16+
"""
17+
options = self.parser.parse_args(["test.bas"])
18+
self.assertIsNone(options.autorun)
19+
20+
def test_loader_defaults_to_none(self):
21+
""" Some boolean options, when not specified from the command line
22+
must return None (null) instead of False to preserve .INI saved
23+
value.
24+
"""
25+
options = self.parser.parse_args(["test.bas"])
26+
self.assertIsNone(options.basic)

tests/api/test_config.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ class TestConfig(unittest.TestCase):
1212
"""
1313
def setUp(self):
1414
config.OPTIONS(config.Action.CLEAR)
15+
config.init()
1516

1617
def test_init(self):
17-
config.init()
1818
self.assertEqual(config.OPTIONS.debug_level, 0)
1919
self.assertEqual(config.OPTIONS.stdin, sys.stdin)
2020
self.assertEqual(config.OPTIONS.stdout, sys.stdout)
@@ -46,7 +46,6 @@ def test_init(self):
4646
self.assertEqual(config.OPTIONS.strict, False)
4747

4848
def test_initted_values(self):
49-
config.init()
5049
self.assertEqual(sorted(config.OPTIONS._options.keys()), [
5150
'__DEFINES',
5251
config.OPTION.ARCH,
@@ -82,3 +81,19 @@ def test_initted_values(self):
8281
config.OPTION.USE_BASIC_LOADER,
8382
config.OPTION.ASM_ZXNEXT
8483
])
84+
85+
def test_loader_ignore_none(self):
86+
""" Some settings must ignore "None" assignments, since
87+
this means the user didn't specify anything from the command line
88+
"""
89+
config.OPTIONS.use_basic_loader = True
90+
config.OPTIONS.use_basic_loader = None
91+
self.assertEqual(config.OPTIONS.use_basic_loader, True)
92+
93+
def test_autorun_ignore_none(self):
94+
""" Some settings must ignore "None" assignments, since
95+
this means the user didn't specify anything from the command line
96+
"""
97+
config.OPTIONS.autorun = True
98+
config.OPTIONS.autorun = None
99+
self.assertEqual(config.OPTIONS.autorun, True)

0 commit comments

Comments
 (0)