sier2 1.0.1__py2.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.
- sier2/__init__.py +6 -0
- sier2/__main__.py +111 -0
- sier2/_block.py +273 -0
- sier2/_config.py +251 -0
- sier2/_dag.py +648 -0
- sier2/_library.py +256 -0
- sier2/_logger.py +65 -0
- sier2/_util.py +127 -0
- sier2/_version.py +3 -0
- sier2/panel/__init__.py +1 -0
- sier2/panel/_chart.py +313 -0
- sier2/panel/_feedlogger.py +153 -0
- sier2/panel/_panel.py +505 -0
- sier2/panel/_panel_util.py +83 -0
- sier2-1.0.1.dist-info/METADATA +69 -0
- sier2-1.0.1.dist-info/RECORD +18 -0
- sier2-1.0.1.dist-info/WHEEL +4 -0
- sier2-1.0.1.dist-info/licenses/LICENSE +21 -0
sier2/__init__.py
ADDED
sier2/__main__.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
|
|
4
|
+
from sier2 import Library, Config
|
|
5
|
+
from ._library import _find_blocks, _find_dags, run_dag
|
|
6
|
+
from ._util import block_doc_text, dag_doc_text
|
|
7
|
+
|
|
8
|
+
BOLD = '' # '\x1b[1;37m'
|
|
9
|
+
NORM = '' # '\x1b[0m'
|
|
10
|
+
|
|
11
|
+
def _pkg(module):
|
|
12
|
+
return module.split('.')[0]
|
|
13
|
+
|
|
14
|
+
def blocks_cmd(args):
|
|
15
|
+
"""Display the blocks found via plugin entry points."""
|
|
16
|
+
|
|
17
|
+
seen = set()
|
|
18
|
+
curr_ep = None
|
|
19
|
+
for entry_point, gi in _find_blocks():
|
|
20
|
+
show = not args.block or gi.key.endswith(args.block)
|
|
21
|
+
if curr_ep is None or entry_point!=curr_ep:
|
|
22
|
+
if show:
|
|
23
|
+
pkg = _pkg(entry_point.module)
|
|
24
|
+
s = f'In {pkg} v{version(pkg)}'
|
|
25
|
+
u = '' # '\n' + '#' * len(s)
|
|
26
|
+
print(f'\n{BOLD}{s}{u}{NORM}')
|
|
27
|
+
# print(f'\x1b[1mIn {entry_point.module} v{version(entry_point.module)}:\x1b[0m')
|
|
28
|
+
curr_ep = entry_point
|
|
29
|
+
|
|
30
|
+
if show:
|
|
31
|
+
dup = f' (DUPLICATE)' if gi.key in seen else ''
|
|
32
|
+
print(f' {BOLD}{gi.key}: {gi.doc}{NORM}{dup}')
|
|
33
|
+
|
|
34
|
+
if args.verbose:
|
|
35
|
+
block = Library.get_block(gi.key)
|
|
36
|
+
print(block_doc_text(block))
|
|
37
|
+
print()
|
|
38
|
+
|
|
39
|
+
seen.add(gi.key)
|
|
40
|
+
|
|
41
|
+
def dags_cmd(args):
|
|
42
|
+
"""Display the dags found via plugin entry points."""
|
|
43
|
+
|
|
44
|
+
seen = set()
|
|
45
|
+
curr_ep = None
|
|
46
|
+
for entry_point, gi in _find_dags():
|
|
47
|
+
show = not args.dag or gi.key.endswith(args.dag)
|
|
48
|
+
if curr_ep is None or entry_point!=curr_ep:
|
|
49
|
+
if show:
|
|
50
|
+
pkg = _pkg(entry_point.module)
|
|
51
|
+
s = f'In {pkg} v{version(pkg)}'
|
|
52
|
+
u = '' # '\n' + '#' * len(s)
|
|
53
|
+
print(f'\n{BOLD}{s}{u}{NORM}')
|
|
54
|
+
curr_ep = entry_point
|
|
55
|
+
|
|
56
|
+
if show:
|
|
57
|
+
dup = f' (DUPLICATE)' if gi.key in seen else ''
|
|
58
|
+
print(f' {BOLD}{gi.key}: {gi.doc}{NORM}{dup}')
|
|
59
|
+
|
|
60
|
+
if args.verbose:
|
|
61
|
+
# We have to instantiate the dag to get the documentation.
|
|
62
|
+
#
|
|
63
|
+
dag = Library.get_dag(gi.key)
|
|
64
|
+
print(dag_doc_text(dag))
|
|
65
|
+
|
|
66
|
+
seen.add(gi.key)
|
|
67
|
+
|
|
68
|
+
def run_cmd(args):
|
|
69
|
+
if args.update_config:
|
|
70
|
+
args = args.update_config.split(',')
|
|
71
|
+
block_name = args[0]
|
|
72
|
+
update_arg = ','.join(args[1:])
|
|
73
|
+
write_to_file = True
|
|
74
|
+
else:
|
|
75
|
+
block_name = None
|
|
76
|
+
update_arg = None
|
|
77
|
+
write_to_file = False
|
|
78
|
+
|
|
79
|
+
if args.config or args.update_config:
|
|
80
|
+
Config.update(location=args.config, config_block=block_name, update_arg=update_arg, write_to_file=write_to_file)
|
|
81
|
+
|
|
82
|
+
run_dag(args.dag)
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
parser = argparse.ArgumentParser()
|
|
86
|
+
subparsers = parser.add_subparsers(help='sub-command help')
|
|
87
|
+
|
|
88
|
+
run = subparsers.add_parser('run', help='Run a dag')
|
|
89
|
+
run.add_argument('dag', type=str, help='A dag to run')
|
|
90
|
+
run.add_argument('-C', '--config', default=None, help='Use this config file (default to personal config file)')
|
|
91
|
+
run.add_argument('-U', '--update_config', default=None, help='A block that provides a config that updates the personal config file')
|
|
92
|
+
run.set_defaults(func=run_cmd)
|
|
93
|
+
|
|
94
|
+
blocks = subparsers.add_parser('blocks', help='Show available blocks')
|
|
95
|
+
blocks.add_argument('-v', '--verbose', action='store_true', help='Show help')
|
|
96
|
+
blocks.add_argument('block', nargs='?', help='Show all blocks ending with this string')
|
|
97
|
+
blocks.set_defaults(func=blocks_cmd)
|
|
98
|
+
|
|
99
|
+
dags = subparsers.add_parser('dags', help='Show available dags')
|
|
100
|
+
dags.add_argument('-v', '--verbose', action='store_true', help='Show help')
|
|
101
|
+
dags.add_argument('dag', nargs='?', help='Show all dags ending with this string')
|
|
102
|
+
dags.set_defaults(func=dags_cmd)
|
|
103
|
+
|
|
104
|
+
args = parser.parse_args()
|
|
105
|
+
if 'func' in args:
|
|
106
|
+
args.func(args)
|
|
107
|
+
else:
|
|
108
|
+
parser.print_help()
|
|
109
|
+
|
|
110
|
+
if __name__=='__main__':
|
|
111
|
+
main()
|
sier2/_block.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
import inspect
|
|
3
|
+
import param
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import _logger
|
|
7
|
+
from ._config import Config
|
|
8
|
+
|
|
9
|
+
class BlockError(Exception):
|
|
10
|
+
"""Raised if a Block configuration is invalid.
|
|
11
|
+
|
|
12
|
+
If this exception is raised, the executing dag sets its stop
|
|
13
|
+
flag (which must be manually reset), and displays a stacktrace.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class BlockState(StrEnum):
|
|
19
|
+
"""The current state of a block; also used for logging."""
|
|
20
|
+
|
|
21
|
+
DAG = 'DAG' # Dag logging.
|
|
22
|
+
BLOCK = 'BLOCK' # Block logging.
|
|
23
|
+
INPUT = 'INPUT'
|
|
24
|
+
READY = 'READY'
|
|
25
|
+
EXECUTING = 'EXECUTING'
|
|
26
|
+
WAITING = 'WAITING'
|
|
27
|
+
SUCCESSFUL = 'SUCCESSFUL'
|
|
28
|
+
INTERRUPTED = 'INTERRUPTED'
|
|
29
|
+
ERROR = 'ERROR'
|
|
30
|
+
|
|
31
|
+
_PAUSE_EXECUTION_DOC = '''If True, a block executes in two steps.
|
|
32
|
+
|
|
33
|
+
When the block is executed by a dag, the dag first sets the input
|
|
34
|
+
params, then calls ``prepare()``. Execution of the dag then stops.
|
|
35
|
+
|
|
36
|
+
The dag is then restarted using ``dag.execute_after_input(input_block)``.
|
|
37
|
+
(An input block must be specified because it is not required that the
|
|
38
|
+
same input block be used immediately.) This causes the block's
|
|
39
|
+
``execute()`` method to be called without resetting the input params.
|
|
40
|
+
|
|
41
|
+
Dag execution then continues as normal.
|
|
42
|
+
'''
|
|
43
|
+
|
|
44
|
+
_VISIBLE_DOC = '''If True, the block will be visible in a GUI.
|
|
45
|
+
|
|
46
|
+
A block may not need to be visible in a dag with a GUI. For example,
|
|
47
|
+
it may be applying a pre-defined filter, or running an algorithm that
|
|
48
|
+
takes an indeterminate amount of time. Setting this parameter to False
|
|
49
|
+
tells the GUI not display this block. Dag execution will otherwise
|
|
50
|
+
proceed as normal.
|
|
51
|
+
|
|
52
|
+
This is also useful if a GUI application only requires a single block.
|
|
53
|
+
A dag requires at least two blocks, because blocks can only be added
|
|
54
|
+
by connecting them to another block. By making one block a "dummy"
|
|
55
|
+
that is not visible, the GUI effectivly has a single block.
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
class Block(param.Parameterized):
|
|
59
|
+
"""The base class for blocks.
|
|
60
|
+
|
|
61
|
+
A block is implemented as:
|
|
62
|
+
|
|
63
|
+
.. code-block:: python
|
|
64
|
+
|
|
65
|
+
class MyBlock(Block):
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
The ``Block`` class inherits from ``param.Parameterized``, and uses parameters
|
|
69
|
+
as described at https://param.holoviz.org/user_guide/Parameters.html.
|
|
70
|
+
There are three kinds of parameters:
|
|
71
|
+
* Input parameters start with ``in_``. These parameters are set before a block is executed.
|
|
72
|
+
* Output parameters start with ``out_``. The block sets these in its ``execute()`` method.
|
|
73
|
+
* Block parameters start with ``block_``. These are reserved for use by blocks.
|
|
74
|
+
|
|
75
|
+
A typical block will have at least one input parameter, and an ``execute()``
|
|
76
|
+
method that is called when an input parameter value changes.
|
|
77
|
+
|
|
78
|
+
.. code-block:: python
|
|
79
|
+
|
|
80
|
+
class MyBlock(Block):
|
|
81
|
+
in_value = param.String(label='Input Value')
|
|
82
|
+
out_upper = param.String(label='Output value)
|
|
83
|
+
|
|
84
|
+
def execute(self):
|
|
85
|
+
self.out_value = self.in_value.upper()
|
|
86
|
+
print(f'New value is {self.out_value}')
|
|
87
|
+
|
|
88
|
+
The block parameter ``block_pause_execution`` allows a block to act as an "input" block,
|
|
89
|
+
particularly when the block hsa a GUI interface. When set to True and dag execution
|
|
90
|
+
reaches this block, the block's ``prepare()`` method is called, then the dag stops executing.
|
|
91
|
+
This allows the user to interact with a user interface.
|
|
92
|
+
|
|
93
|
+
The dag is then restarted using ``dag.execute_after_input(input_block)`` (typically by
|
|
94
|
+
a "Continue" button in the GUI.) When the dag is continued at this block,
|
|
95
|
+
the block's ``execute()`` method is called, and dag execution continues.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
block_pause_execution = param.Boolean(default=False, label='Pause execution', doc=_PAUSE_EXECUTION_DOC)
|
|
99
|
+
block_visible = param.Boolean(default=True, label='Display block', doc=_VISIBLE_DOC)
|
|
100
|
+
|
|
101
|
+
_block_state = param.String(default=BlockState.READY)
|
|
102
|
+
|
|
103
|
+
SIER2_KEY = '_sier2__key'
|
|
104
|
+
|
|
105
|
+
def __init__(self, *args, block_pause_execution: bool=False, block_visible: bool=True, block_doc: str|None=None, continue_label='Continue', **kwargs):
|
|
106
|
+
"""
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
block_pause_execution: bool
|
|
110
|
+
If True, ``prepare()`` is called and dag execution stops.
|
|
111
|
+
block_visible: bool
|
|
112
|
+
If True (the default), the block will be visible in a GUI.
|
|
113
|
+
block_doc: str|None
|
|
114
|
+
Markdown documentation that may displayed in the user interface.
|
|
115
|
+
"""
|
|
116
|
+
super().__init__(*args, **kwargs)
|
|
117
|
+
|
|
118
|
+
if not self.__doc__:
|
|
119
|
+
raise BlockError(f'Class {self.__class__} must have a docstring')
|
|
120
|
+
|
|
121
|
+
self.block_pause_execution = block_pause_execution
|
|
122
|
+
self.block_visible = block_visible
|
|
123
|
+
self.block_doc = block_doc
|
|
124
|
+
self.continue_label = continue_label
|
|
125
|
+
# self._block_state = BlockState.READY
|
|
126
|
+
self.logger = _logger.get_logger(self.name)
|
|
127
|
+
|
|
128
|
+
# Maintain a map of "block+output parameter being watched" -> "input parameter".
|
|
129
|
+
# This is used by _block_event() to set the correct input parameter.
|
|
130
|
+
#
|
|
131
|
+
self._block_name_map: dict[tuple[str, str], str] = {}
|
|
132
|
+
|
|
133
|
+
# Record this block's output parameters.
|
|
134
|
+
# If this is an input block, we need to trigger
|
|
135
|
+
# the output values before executing the next block,
|
|
136
|
+
# in case the user didn't change anything.
|
|
137
|
+
#
|
|
138
|
+
self._block_out_params = []
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def block_key(cls):
|
|
142
|
+
"""The unique key of this block class.
|
|
143
|
+
|
|
144
|
+
Blocks require a unique key so they can be identified in the block library.
|
|
145
|
+
The default implementation should be sufficient, but can be overridden
|
|
146
|
+
in case of refactoring or name clashes.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
im = inspect.getmodule(cls)
|
|
150
|
+
|
|
151
|
+
if hasattr(cls, Block.SIER2_KEY):
|
|
152
|
+
return getattr(cls, Block.SIER2_KEY)
|
|
153
|
+
|
|
154
|
+
return f'{im.__name__}.{cls.__qualname__}'
|
|
155
|
+
|
|
156
|
+
def get_config(self, *, block: 'Block'=None):
|
|
157
|
+
"""Return a dictionary containing keys and values from the section specified by
|
|
158
|
+
the block in the sier2 config file.
|
|
159
|
+
|
|
160
|
+
The config file has the format described by the Python ``configparser`` module,
|
|
161
|
+
with the added feature that values are evaluated using :func:`ast.literal_eval`,
|
|
162
|
+
and therefore must be syntactically correct Python literals.
|
|
163
|
+
|
|
164
|
+
They keys and values are read from the section ``[block.name]``, where ``name`` is
|
|
165
|
+
this block's unique key as specified by :func:`sier2.Block.block_key`.
|
|
166
|
+
If the ``block`` parameter is unspecified, the calling block is used by default.
|
|
167
|
+
|
|
168
|
+
If the section is not present in the config file, an empty dictionary is returned.
|
|
169
|
+
|
|
170
|
+
The default config file is looked for at
|
|
171
|
+
(the default user config directory) / 'sier2sier2.ini'.
|
|
172
|
+
On Windows, the config directory is ``$ENV:APPDATA``; on Linux, ``$XDG_CONFIG_HOME``
|
|
173
|
+
or ``$HOME/.config``.
|
|
174
|
+
|
|
175
|
+
An alternative config file can be specified by setting ``Config.location`` before
|
|
176
|
+
any dag or block is executed. THis can be done from a command line using
|
|
177
|
+
the ``--config`` option.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
block: Block
|
|
182
|
+
The specified block's config section will be returned. Defaults to ``self``.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
A dictionary containing the section's keys and values.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
b = block if block is not None else self
|
|
190
|
+
name = f'block.{b.block_key()}'
|
|
191
|
+
|
|
192
|
+
return Config[name]
|
|
193
|
+
|
|
194
|
+
def get_config_value(self, key: str, default: Any=None, *, block: 'Block'=None):
|
|
195
|
+
"""Return an individual value from the section specified by
|
|
196
|
+
the block in the sier2 config file.
|
|
197
|
+
|
|
198
|
+
See :func:`sier2.Block.get_config` for more details.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
key: str
|
|
203
|
+
The key of the value to be returned.
|
|
204
|
+
default: Any
|
|
205
|
+
The default value to return if the section or key are not present in the config file.
|
|
206
|
+
block: Block
|
|
207
|
+
The specified block's config section will be returned. Defaults to ``self``.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
b = block if block is not None else self
|
|
211
|
+
name = f'block.{b.block_key()}'
|
|
212
|
+
value = Config[name, key]
|
|
213
|
+
|
|
214
|
+
return value if value is not None else default
|
|
215
|
+
|
|
216
|
+
def prepare(self):
|
|
217
|
+
"""If blockpause_execution is True, called by a dag before calling ``execute()```.
|
|
218
|
+
|
|
219
|
+
This gives the block author an opportunity to validate the
|
|
220
|
+
input params and set up a user inteface.
|
|
221
|
+
|
|
222
|
+
After the dag restarts on this block, ``execute()`` will be called.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
def execute(self, *_, **__):
|
|
228
|
+
"""This method is called when one or more of the input parameters causes an event.
|
|
229
|
+
|
|
230
|
+
Override this method in a Block subclass.
|
|
231
|
+
|
|
232
|
+
The ``execute()`` method can have arguments. The arguments can be specified
|
|
233
|
+
in any order. It is not necessary to specify all, or any, arguments.
|
|
234
|
+
Arguments will not be passed via ``*args`` or ``**kwargs``.
|
|
235
|
+
|
|
236
|
+
* ``stopper`` - an indicator that the dag has been stopped. This may be
|
|
237
|
+
set while the block is executing, in which case the block should
|
|
238
|
+
stop executing as soon as possible.
|
|
239
|
+
* ``events`` - the param events that caused execute() to be called.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
# print(f'** EXECUTE {self.__class__=}')
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
def __call__(self, **kwargs) -> dict[str, Any]:
|
|
246
|
+
"""Allow a block to be called directly."""
|
|
247
|
+
|
|
248
|
+
in_names = [name for name in self.__class__.param if name.startswith('in_')]
|
|
249
|
+
if len(kwargs)!=len(in_names) or any(name not in in_names for name in kwargs):
|
|
250
|
+
names = ', '.join(in_names)
|
|
251
|
+
raise BlockError(f'All input params must be specified: {names}')
|
|
252
|
+
|
|
253
|
+
for name, value in kwargs.items():
|
|
254
|
+
setattr(self, name, value)
|
|
255
|
+
|
|
256
|
+
self.execute()
|
|
257
|
+
|
|
258
|
+
out_names = [name for name in self.__class__.param if name.startswith('out_')]
|
|
259
|
+
result = {name: getattr(self, name) for name in out_names}
|
|
260
|
+
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
class BlockValidateError(BlockError):
|
|
264
|
+
"""Raised if ``Block.prepare()`` or ``Block.execute()`` determines that input data is invalid.
|
|
265
|
+
|
|
266
|
+
If this exception is raised, it will be caught by the executing dag.
|
|
267
|
+
The dag will not set its stop flag, no stacktrace will be displayed,
|
|
268
|
+
and the error message will be displayed.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, *, block_name: str, error: str):
|
|
272
|
+
super().__init__(error)
|
|
273
|
+
self.block_name = block_name
|
sier2/_config.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# A configuration module.
|
|
2
|
+
#
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import configparser
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
# If this key exists in a section and has the value False,
|
|
11
|
+
# this section will not be updated.
|
|
12
|
+
#
|
|
13
|
+
CONFIG_UPDATE = 'config_update'
|
|
14
|
+
|
|
15
|
+
def _default_config_file():
|
|
16
|
+
"""Determine the location of the config file sier2.ini.
|
|
17
|
+
|
|
18
|
+
If the environment variable ``SIER2_INI`` is set,
|
|
19
|
+
it specifies the path of the config file.
|
|
20
|
+
|
|
21
|
+
Otherwise, if Windows, use ``$env:APPDATA/sier2/sier2.ini``.
|
|
22
|
+
|
|
23
|
+
Otherwise, if ``XDG_CONFIG_HOME`` is set, use ``$XDG_CONFIG_HOME/sier2/sier2.ini``.
|
|
24
|
+
|
|
25
|
+
Otherwise, use ``$HOME/.config/sier2/sier2.ini``.
|
|
26
|
+
|
|
27
|
+
If not using ``SIER2_INI``, the ``sier2`` directory will be created
|
|
28
|
+
if it does not exist.
|
|
29
|
+
|
|
30
|
+
TODO don't create the sier2 directory until the ini file is written.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# If a config file has been explicitly set in an environment variable,
|
|
34
|
+
# use it.
|
|
35
|
+
#
|
|
36
|
+
ini = os.environ.get('SIER2_INI', None)
|
|
37
|
+
if ini:
|
|
38
|
+
return Path(ini)
|
|
39
|
+
|
|
40
|
+
if os.name=='nt':
|
|
41
|
+
# Windows.
|
|
42
|
+
#
|
|
43
|
+
prdir = Path(os.environ['APPDATA']) / 'sier2'
|
|
44
|
+
else:
|
|
45
|
+
# Linux.
|
|
46
|
+
#
|
|
47
|
+
prdir = os.environ.get('XDG_CONFIG_HOME', None)
|
|
48
|
+
if prdir:
|
|
49
|
+
prdir = Path(prdir)
|
|
50
|
+
else:
|
|
51
|
+
prdir = Path.home() / '.config'
|
|
52
|
+
|
|
53
|
+
prdir = prdir / 'sier2'
|
|
54
|
+
|
|
55
|
+
if not prdir.exists():
|
|
56
|
+
prdir.mkdir(parents=True)
|
|
57
|
+
|
|
58
|
+
return prdir / 'sier2.ini'
|
|
59
|
+
|
|
60
|
+
class _Config:
|
|
61
|
+
"""This class is for internal use.
|
|
62
|
+
|
|
63
|
+
A single instance of this class is exposed publicly as ``Config``.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self):
|
|
67
|
+
self._clear()
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def update(*, location: Path|str|None=None, config_block: str|None=None, update_arg: str|None=None, write_to_file: bool=False):
|
|
71
|
+
"""Update the config.
|
|
72
|
+
|
|
73
|
+
If ``location`` has a value, that config file will be used instead of the default.
|
|
74
|
+
|
|
75
|
+
If config_block has a value, it must be the name of a block that has
|
|
76
|
+
an ``out_config`` param. The ``out_config`` param must contain a string that is
|
|
77
|
+
the content of a sier2 ini file. If ``update_arg`` is specified and the block has
|
|
78
|
+
an ``in_arg`` param, ``in_arg`` is set to ``update_arg``.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
location: Path|str|None
|
|
83
|
+
The location of a config file that will be loaded when a config is read.
|
|
84
|
+
config_block: str|None
|
|
85
|
+
A sier2 block that returns the contents of a config file in ``out_config``.
|
|
86
|
+
update_arg: str|None
|
|
87
|
+
A string that is passed to the ``config_block`` block is it has an ``in_arg`` param.
|
|
88
|
+
write_to_file: bool
|
|
89
|
+
If True, the config file at ``location`` is overwritten with the merged config.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if location:
|
|
93
|
+
Config.location = location
|
|
94
|
+
|
|
95
|
+
if config_block:
|
|
96
|
+
# Import here, otherwise there's a circular dependency Library -> Config -> Library.
|
|
97
|
+
# Config blocks better not have any config.
|
|
98
|
+
#
|
|
99
|
+
from sier2 import Library
|
|
100
|
+
|
|
101
|
+
block = Library.get_block(config_block)()
|
|
102
|
+
|
|
103
|
+
if not hasattr(block, 'out_config'):
|
|
104
|
+
raise ValueError('config block does not have out param "out_config"')
|
|
105
|
+
|
|
106
|
+
if hasattr(block, 'in_arg'):
|
|
107
|
+
block.in_arg = update_arg
|
|
108
|
+
|
|
109
|
+
block.execute()
|
|
110
|
+
Config._update(block.out_config, write_to_file=write_to_file)
|
|
111
|
+
|
|
112
|
+
def _clear(self):
|
|
113
|
+
self._location = _default_config_file()
|
|
114
|
+
self._config = {}
|
|
115
|
+
self._loaded = False
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def location(self) -> Path:
|
|
119
|
+
"""Get or set the config file location.
|
|
120
|
+
|
|
121
|
+
By default, this is `$ENV:APPDATA/sier2/sier2.ini` on Window,
|
|
122
|
+
and `$XDG_CONFIG_HOME/sier2/sier2.ini` on Linux.
|
|
123
|
+
|
|
124
|
+
The location cannot be set if the config file has already been loaded.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
return self._location
|
|
128
|
+
|
|
129
|
+
@location.setter
|
|
130
|
+
def location(self, config_file: Path|str):
|
|
131
|
+
if self._loaded:
|
|
132
|
+
raise ValueError('Config is already loaded')
|
|
133
|
+
|
|
134
|
+
if not isinstance(config_file, Path):
|
|
135
|
+
config_file = Path(config_file)
|
|
136
|
+
|
|
137
|
+
self._location = config_file
|
|
138
|
+
|
|
139
|
+
def _update(self, ini: str, write_to_file: bool=False):
|
|
140
|
+
"""Update the config file using ini, which contains the contents of another config file.
|
|
141
|
+
|
|
142
|
+
The current config is also updated.
|
|
143
|
+
|
|
144
|
+
The config file cannot be updated if it has already been loaded.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
if self._loaded:
|
|
148
|
+
raise ValueError('Config is already loaded')
|
|
149
|
+
|
|
150
|
+
new_config = configparser.ConfigParser()
|
|
151
|
+
new_config.read_string(ini)
|
|
152
|
+
|
|
153
|
+
config = configparser.ConfigParser()
|
|
154
|
+
if self._location.is_file():
|
|
155
|
+
config.read(self._location)
|
|
156
|
+
|
|
157
|
+
for section_name in new_config.sections():
|
|
158
|
+
if not config.has_section(section_name):
|
|
159
|
+
config.add_section(section_name)
|
|
160
|
+
config[section_name].update(new_config[section_name])
|
|
161
|
+
else:
|
|
162
|
+
update_section = True
|
|
163
|
+
if CONFIG_UPDATE in config[section_name]:
|
|
164
|
+
update_section = ast.literal_eval(config[section_name][CONFIG_UPDATE])
|
|
165
|
+
if not isinstance(update_section, bool):
|
|
166
|
+
raise ValueError(f'Value of [{section_name}].{CONFIG_UPDATE} is not a bool')
|
|
167
|
+
|
|
168
|
+
if update_section:
|
|
169
|
+
config[section_name].update(new_config[section_name])
|
|
170
|
+
# for k, v in new_config[section_name].items():
|
|
171
|
+
# config[section_name][k] = new_config[section_name][k]
|
|
172
|
+
|
|
173
|
+
if write_to_file:
|
|
174
|
+
with open(self._location, 'w', encoding='utf-8') as f:
|
|
175
|
+
config.write(f)
|
|
176
|
+
|
|
177
|
+
self._config = config
|
|
178
|
+
self._loaded = True
|
|
179
|
+
|
|
180
|
+
def _load(self):
|
|
181
|
+
"""Load the config.
|
|
182
|
+
|
|
183
|
+
Overwrites any previous config. If the location does not exist, the config will be empty.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
self._config = configparser.ConfigParser()
|
|
187
|
+
if self._location.is_file():
|
|
188
|
+
self._config.read(self._location)
|
|
189
|
+
|
|
190
|
+
# The config has been loaded, even if the file didn't exist.
|
|
191
|
+
#
|
|
192
|
+
self._loaded = True
|
|
193
|
+
|
|
194
|
+
def _load_string(self, sconfig):
|
|
195
|
+
"""For testing."""
|
|
196
|
+
|
|
197
|
+
self._config = configparser.ConfigParser()
|
|
198
|
+
self._config.read_string(sconfig)
|
|
199
|
+
self._loaded = True
|
|
200
|
+
|
|
201
|
+
def __getitem__(self, section_name: str|tuple[str, str]) -> Any|dict[str, Any]:
|
|
202
|
+
"""If section_name is a string, get the config values for the given section name.
|
|
203
|
+
|
|
204
|
+
The config file is lazily loaded. Non-existence of the file is normal.
|
|
205
|
+
The keys and values for the specified section are loaded into a new dictionary,
|
|
206
|
+
which is returned.
|
|
207
|
+
|
|
208
|
+
Since configparser always returns values as strings, the values are evaluated
|
|
209
|
+
using :func:`ast.literal_eval` to be correctly typed. This means that strings in the
|
|
210
|
+
.ini file must be surrounded by quotes.
|
|
211
|
+
|
|
212
|
+
A section name for a block is of the form 'block.block_key_name'.
|
|
213
|
+
|
|
214
|
+
The name need not to exist; if it doesn't, an empty dictionary is returned.
|
|
215
|
+
|
|
216
|
+
If section_name is a tuple, it is interpreted as (section_name, key). If the
|
|
217
|
+
section_name and key exist, the value of the key is returned, else None is returned.
|
|
218
|
+
Only that single value is evaluated.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
if not self._loaded:
|
|
222
|
+
self._load()
|
|
223
|
+
|
|
224
|
+
if isinstance(section_name, tuple):
|
|
225
|
+
section_name, key = section_name
|
|
226
|
+
if section_name not in self._config:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
section = self._config[section_name]
|
|
230
|
+
if key not in section:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
v = section[key]
|
|
235
|
+
return ast.literal_eval(v)
|
|
236
|
+
except ValueError:
|
|
237
|
+
raise ValueError(f'Cannot eval section [{section_name}], key {key}, value {v}')
|
|
238
|
+
|
|
239
|
+
if section_name not in self._config:
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
c = {}
|
|
243
|
+
for key, v in self._config[section_name].items():
|
|
244
|
+
try:
|
|
245
|
+
c[key] = ast.literal_eval(v)
|
|
246
|
+
except ValueError:
|
|
247
|
+
raise ValueError(f'Cannot eval section [{section_name}], key {key}, value {v}')
|
|
248
|
+
|
|
249
|
+
return c
|
|
250
|
+
|
|
251
|
+
Config = _Config()
|