-
-
Notifications
You must be signed in to change notification settings - Fork 23
/
settings.py
240 lines (201 loc) · 7.76 KB
/
settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2021, Vanessa Sochat"
__license__ = "MPL 2.0"
from shpc.logger import logger
import shpc.defaults as defaults
import shpc.main.schemas
import shpc.utils
import shutil
try:
from ruamel_yaml import YAML
except:
from ruamel.yaml import YAML
from datetime import datetime
import jsonschema
import os
class SettingsBase:
def __init__(self):
"""
Create a new settings object not requiring a settings file.
"""
# Set an updated time, in case it's written back to file
self._settings = {"updated_at": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")}
self.settings_file = None
def __str__(self):
return "[shpc-settings]"
def __repr__(self):
return self.__str__()
def validate(self):
"""
Validate the loaded settings with jsonschema
"""
jsonschema.validate(instance=self._settings, schema=shpc.main.schemas.settings)
def inituser(self):
"""
Create a user specific config in user's home.
"""
user_home = os.path.dirname(defaults.user_settings_file)
if not os.path.exists(user_home):
os.makedirs(user_home)
if os.path.exists(defaults.user_settings_file):
logger.exit(
"%s already exists! Remove first before re-creating."
% defaults.user_settings_file
)
shutil.copyfile(self.settings_file, defaults.user_settings_file)
logger.info("Created user settings file %s" % defaults.user_settings_file)
def edit(self):
"""
Interactively edit a config file.
"""
if not self.settings_file or not os.path.exists(self.settings_file):
logger.exit("Settings file not found.")
shpc.utils.run_command([self.config_editor, self.settings_file], stream=True)
def get_settings_file(self, settings_file=None):
"""
Get the preferred used settings file.
"""
# Only consider user settings if the file exists!
user_settings = None
if os.path.exists(defaults.user_settings_file):
user_settings = defaults.user_settings_file
# First preference to command line, then user settings, then default
return settings_file or user_settings or defaults.default_settings_file
def load(self, settings_file=None):
"""
Load the settings file into the settings object
"""
# Get the preferred settings flie
self.settings_file = self.get_settings_file(settings_file)
# Exit quickly if the settings file does not exist
if not os.path.exists(self.settings_file):
logger.exit("%s does not exist." % self.settings_file)
# Default to round trip so we can save comments
yaml = YAML()
# Store the original settings for update as we go
with open(self.settings_file, "r") as fd:
self._settings = yaml.load(fd.read())
def get(self, key, default=None):
"""
Get a settings value, doing appropriate substitution and expansion.
"""
value = self._settings.get(key, default)
value = self._substitutions(value)
# If we allow environment substitution, do it
if key in defaults.allowed_envars and value:
if isinstance(value, list):
value = [os.path.expandvars(v) for v in value]
else:
value = os.path.expandvars(value)
return value
def __getattr__(self, key):
"""
A direct get of an attribute, but default to None if doesn't exist
"""
return self.get(key)
def add(self, key, value):
"""
Add a value to a list parameter
"""
# We can only add to lists
current = self._settings.get(key)
if current and not isinstance(current, list):
logger.exit("You cannot only add to a list variable.")
# Add to the beginning of the list
current = current + [value]
value = list(set(current))
self._settings[key] = value
self.change_validate(key, value)
def remove(self, key, value):
"""
Remove a value from a list parameter
"""
current = self._settings.get(key)
if current and not isinstance(current, list):
logger.exit("You cannot only remove from a list variable.")
if not current or value not in current:
logger.exit("%s is not in %s" % (value, key))
current.pop(current.index(value))
self._settings[key] = current
self.change_validate(key, current)
def set(self, key, value):
"""
Set a setting based on key and value. If the key has :, it's nested
"""
value = True if value == "true" else value
value = False if value == "false" else value
# List values not allowed for set
current = self._settings.get(key)
if current and isinstance(current, list):
logger.exit("You cannot use 'set' for a list. Use add/remove instead.")
# This is a reference to a dictionary (object) setting
if ":" in key:
key, subkey = key.split(":")
self._settings[key][subkey] = value
else:
self._settings[key] = value
# Validate and catch error message cleanly
self.change_validate(key, value)
def change_validate(self, key, value):
"""
A courtesy function to validate a new config addition.
"""
# Don't allow the user to add a setting not known
try:
self.validate()
except jsonschema.exceptions.ValidationError as error:
logger.exit(
"%s:%s cannot be added to config: %s" % (key, value, error.message)
)
def _substitutions(self, value):
"""
Given a value, make substitutions
"""
if isinstance(value, bool) or not value:
return value
# Currently dicts only support boolean or null so we return as is
elif isinstance(value, dict):
return value
for rep, repvalue in defaults.reps.items():
if isinstance(value, list):
value = [x.replace(rep, repvalue) for x in value]
else:
value = value.replace(rep, repvalue)
return value
def delete(self, key):
if key in self._settings:
del self._settings[key]
def save(self, filename=None):
"""
Save settings, but do not change order of anything.
"""
filename = filename or self.settings_file
if not filename:
logger.exit("A filename is required to save to.")
yaml = YAML()
# This requires Python 3.7 support
try:
with open(filename, "w") as fd:
yaml.dump(self._settings, fd, sort_keys=False)
except:
with open(filename, "w") as fd:
yaml.dump(self._settings, fd)
def __iter__(self):
for key, value in self.__dict__.items():
yield key, value
class Settings(SettingsBase):
"""
The settings class is a wrapper for easily parsing a settings.yml file.
We parse into a query-able class. It also gives us control to update settings,
meaning we change the values and then write them to file. It's basically
a dictionary-like class with extra functions.
"""
def __init__(self, settings_file, validate=True):
"""
Create a new settings object, which requires a settings file to load
"""
self.load(settings_file)
if validate:
self.validate()
# Set an updated time, in case it's written back to file
self._settings["updated_at"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")