Skip to content

Commit 7c8fd58

Browse files
Exireldgw
andcommitted
config: small refactoring and unit tests for types
Co-authored-by: dgw <dgw@technobabbl.es>
1 parent 005a534 commit 7c8fd58

File tree

3 files changed

+444
-72
lines changed

3 files changed

+444
-72
lines changed

sopel/config/types.py

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,20 @@ def configure_setting(self, name, prompt, default=NO_DEFAULT):
9595
passed, the current value of the setting will be ignored, even if it is
9696
not the attribute's default.
9797
"""
98-
clazz = getattr(self.__class__, name)
99-
if default is NO_DEFAULT and not clazz.is_secret:
98+
attribute = getattr(self.__class__, name)
99+
if default is NO_DEFAULT and not attribute.is_secret:
100100
try:
101101
default = getattr(self, name)
102102
except AttributeError:
103103
pass
104104
except ValueError:
105105
print('The configured value for this option was invalid.')
106-
if clazz.default is not NO_DEFAULT:
107-
default = clazz.default
106+
if attribute.default is not NO_DEFAULT:
107+
default = attribute.default
108108
while True:
109109
try:
110-
value = clazz.configure(prompt, default, self._parent, self._section_name)
110+
value = attribute.configure(
111+
prompt, default, self._parent, self._section_name)
111112
except ValueError as exc:
112113
print(exc)
113114
else:
@@ -194,7 +195,9 @@ def configure(self, prompt, default, parent, section_name):
194195
raise ValueError("You must provide a value for this option.")
195196

196197
value = value or default
197-
return self.parse(value)
198+
section = getattr(parent, section_name)
199+
200+
return self._parse(value, parent, section)
198201

199202
def serialize(self, value, *args, **kwargs):
200203
"""Take some object, and return the string to be saved to the file.
@@ -212,35 +215,48 @@ def parse(self, value, *args, **kwargs):
212215
def __get__(self, instance, owner=None):
213216
if instance is None:
214217
# If instance is None, we're getting from a section class, not an
215-
# instance of a session class. It makes the wizard code simpler
218+
# instance of a section class. It makes the wizard code simpler
216219
# (and is really just more intuitive) to return the descriptor
217220
# instance here.
218221
return self
219222

223+
value = None
220224
env_name = 'SOPEL_%s_%s' % (instance._section_name.upper(), self.name.upper())
221225
if env_name in os.environ:
222226
value = os.environ.get(env_name)
223227
elif instance._parser.has_option(instance._section_name, self.name):
224228
value = instance._parser.get(instance._section_name, self.name)
225-
else:
226-
if self.default is not NO_DEFAULT:
227-
return self.default
228-
raise AttributeError(
229-
"Missing required value for {}.{}".format(
230-
instance._section_name, self.name
231-
)
229+
230+
settings = instance._parent
231+
section = getattr(settings, instance._section_name)
232+
return self._parse(value, settings, section)
233+
234+
def _parse(self, value, settings, section):
235+
if value is not None:
236+
return self.parse(value)
237+
if self.default is not NO_DEFAULT:
238+
return self.default
239+
raise AttributeError(
240+
"Missing required value for {}.{}".format(
241+
section._section_name, self.name
232242
)
233-
return self.parse(value)
243+
)
234244

235245
def __set__(self, instance, value):
236246
if value is None:
237247
if self.default == NO_DEFAULT:
238248
raise ValueError('Cannot unset an option with a required value.')
239249
instance._parser.remove_option(instance._section_name, self.name)
240250
return
241-
value = self.serialize(value)
251+
252+
settings = instance._parent
253+
section = getattr(settings, instance._section_name)
254+
value = self._serialize(value, settings, section)
242255
instance._parser.set(instance._section_name, self.name, value)
243256

257+
def _serialize(self, value, settings, section):
258+
return self.serialize(value)
259+
244260
def __delete__(self, instance):
245261
instance._parser.remove_option(instance._section_name, self.name)
246262

@@ -503,7 +519,10 @@ def configure(self, prompt, default, parent, section_name):
503519
else:
504520
values.append(value)
505521
value = get_input(each_prompt + ' ')
506-
return self.parse(self.serialize(values))
522+
523+
section = getattr(parent, section_name)
524+
values = self._serialize(values, parent, section)
525+
return self._parse(values, parent, section)
507526

508527

509528
class ChoiceAttribute(BaseValidated):
@@ -569,62 +588,55 @@ def __init__(self, name, relative=True, directory=False, default=None):
569588
self.relative = relative
570589
self.directory = directory
571590

572-
def __get__(self, instance, owner=None):
573-
if instance is None:
574-
return self
575-
env_name = 'SOPEL_%s_%s' % (instance._section_name.upper(), self.name.upper())
576-
if env_name in os.environ:
577-
value = os.environ.get(env_name)
578-
elif instance._parser.has_option(instance._section_name, self.name):
579-
value = instance._parser.get(instance._section_name, self.name)
580-
else:
581-
if self.default is not NO_DEFAULT:
582-
value = self.default
583-
else:
591+
def _parse(self, value, settings, section):
592+
if value is None:
593+
if self.default == NO_DEFAULT:
584594
raise AttributeError(
585595
"Missing required value for {}.{}".format(
586-
instance._section_name, self.name
596+
section._section_name, self.name
587597
)
588598
)
589-
main_config = instance._parent
590-
this_section = getattr(main_config, instance._section_name)
591-
return self.parse(value, main_config, this_section)
599+
value = self.default
592600

593-
def __set__(self, instance, value):
594-
main_config = instance._parent
595-
this_section = getattr(main_config, instance._section_name)
596-
value = self.serialize(value, main_config, this_section)
597-
instance._parser.set(instance._section_name, self.name, value)
601+
if not value:
602+
return self.parse(value)
598603

599-
def configure(self, prompt, default, parent, section_name):
600-
if default is not NO_DEFAULT and default is not None:
601-
prompt = '{} [{}]'.format(prompt, default)
602-
value = get_input(prompt + ' ')
603-
if not value and default is NO_DEFAULT:
604-
raise ValueError("You must provide a value for this option.")
605-
value = value or default
606-
return self.parse(value, parent, section_name)
604+
result = os.path.expanduser(value)
605+
if not os.path.isabs(result):
606+
if not self.relative:
607+
raise ValueError("Value must be an absolute path.")
608+
result = os.path.join(settings.homedir, result)
609+
610+
return self.parse(result)
607611

608-
def parse(self, value, main_config, this_section):
609-
"""Used to validate ``value`` when loading the config.
612+
def _serialize(self, value, settings, section):
613+
"""Used to validate ``value`` when it is changed at runtime.
610614
611-
:param main_config: the config object which contains this attribute
612-
:type main_config: :class:`~sopel.config.Config`
613-
:param this_section: the config section which contains this attribute
614-
:type this_section: :class:`~StaticSection`
615+
:param settings: the config object which contains this attribute
616+
:type settings: :class:`~sopel.config.Config`
617+
:param section: the config section which contains this attribute
618+
:type section: :class:`~StaticSection`
615619
:return: the ``value``, if it is valid
616620
:rtype: str
617621
:raise ValueError: if the ``value`` is not valid
618622
"""
619-
if value is None:
620-
return
623+
self._parse(value, settings, section)
624+
return self.serialize(value)
621625

622-
value = os.path.expanduser(value)
626+
def parse(self, value):
627+
"""Parse ``value`` as a path on the filesystem to check.
623628
624-
if not os.path.isabs(value):
625-
if not self.relative:
626-
raise ValueError("Value must be an absolute path.")
627-
value = os.path.join(main_config.homedir, value)
629+
:param str value: the path to check
630+
:rtype: str
631+
:raise ValueError: if the directory or file doesn't exist and cannot
632+
be created
633+
634+
If there is no ``value``, then this returns ``None``. Otherwise, it'll
635+
check if the directory or file exists. If it doesn't, it'll try to
636+
create it.
637+
"""
638+
if not value:
639+
return None
628640

629641
if self.directory and not os.path.isdir(value):
630642
try:
@@ -639,16 +651,15 @@ def parse(self, value, main_config, this_section):
639651
raise ValueError("Value must be an existing or creatable file.")
640652
return value
641653

642-
def serialize(self, value, main_config, this_section):
643-
"""Used to validate ``value`` when it is changed at runtime.
654+
def serialize(self, value):
655+
"""Directly return the ``value`` without modification.
644656
645-
:param main_config: the config object which contains this attribute
646-
:type main_config: :class:`~sopel.config.Config`
647-
:param this_section: the config section which contains this attribute
648-
:type this_section: :class:`~StaticSection`
649-
:return: the ``value``, if it is valid
657+
:param str value: the value needing to be saved
658+
:return: the unaltered ``value``, if it is valid
650659
:rtype: str
651-
:raise ValueError: if the ``value`` is not valid
660+
661+
Managing the filename is done by other methods (:meth:`parse`), and
662+
this method is a no-op: this way it ensures that relative paths won't
663+
be replaced by absolute ones.
652664
"""
653-
self.parse(value, main_config, this_section)
654665
return value # So that it's still relative

sopel/modules/admin.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,7 @@ def set_config(bot, trigger):
352352
# Otherwise, set the value to one given
353353
if descriptor is not None:
354354
try:
355-
if isinstance(descriptor, types.FilenameAttribute):
356-
value = descriptor.parse(value, bot.config, descriptor)
357-
else:
358-
value = descriptor.parse(value)
355+
value = descriptor._parse(value, bot.config, section)
359356
except ValueError as exc:
360357
bot.say("Can't set attribute: " + str(exc))
361358
return

0 commit comments

Comments
 (0)