# -*- coding: utf-8 -*-
"""Pibooth configuration.
"""
import io
import ast
import os
import os.path as osp
import inspect
from configparser import RawConfigParser
from pibooth.config.default import DEFAULT, add_default_option
from pibooth.utils import LOGGER, open_text_editor
[docs]
class PiboothConfigParser(RawConfigParser):
"""Class to parse and store the configuration values.
The following attributes are available for use in plugins (``cfg`` reprensents
the PiboothConfigParser instance):
- ``cfg.filename`` (str): absolute path to the laoded config file
- ``cfg.autostart_filename`` (str): file used to start pibooth at Raspberry Pi startup
"""
def __init__(self, filename, plugin_manager, load=True):
super().__init__()
self._pm = plugin_manager
# ---------------------------------------------------------------------
# Variables shared with plugins
# Change them may break plugins compatibility
self.filename = osp.abspath(osp.expanduser(filename))
self.autostart_filename = osp.expanduser('~/.config/autostart/pibooth.desktop')
# ---------------------------------------------------------------------
if osp.isfile(self.filename) and load:
self.load()
def _get_abs_path(self, path):
"""Return absolute path. In case of relative path given, the absolute
one is created using config file path as reference path.
"""
if not path: # Empty string, don't process it as it is not a path
return path
path = osp.expanduser(path)
if not osp.isabs(path):
path = osp.join(osp.relpath(osp.dirname(self.filename), '.'), path)
return osp.abspath(path)
def save(self, default=False):
"""Save the current or default values into the configuration file.
"""
LOGGER.info("Generate the configuration file in '%s'", self.filename)
dirname = osp.dirname(self.filename)
if not osp.isdir(dirname):
os.makedirs(dirname)
option_pattern = "# {comment}\n{name} = {value}\n\n"
with io.open(self.filename, 'w', encoding="utf-8") as fp:
for section, options in DEFAULT.items():
# 1. Write defined options
fp.write(f"[{section}]\n")
for name, value in options.items():
if default:
val = value[0]
else:
val = self.get(section, name)
fp.write(option_pattern.format(comment=value[1], name=name, value=val))
if not default and self.has_section(section):
# 2. Write options that are not in DEFAULT (maybe an option from a disabled plugin)
for name, value in self.items(section):
if name not in DEFAULT[section]:
fp.write(option_pattern.format(comment="Unknown option, maybe from a disabled plugin?",
name=name, value=value))
if not default:
# 3. Write sections that are not in DEFAULT (maybe a section from a disabled plugin)
for section in self.sections():
if section not in DEFAULT:
fp.write(f"[{section}]\n")
for name, value in self.items(section):
fp.write(option_pattern.format(comment="Unknown option, maybe from a disabled plugin?",
name=name, value=value))
self.handle_autostart()
def load(self, clean=False):
"""Load configuration from file.
"""
if clean:
for section in self.sections():
self.remove_section(section)
self.read(self.filename, encoding="utf-8")
self.handle_autostart()
def edit(self):
"""Open a text editor to edit the configuration.
"""
if open_text_editor(self.filename):
# Reload config to check if autostart has changed
self.load()
def handle_autostart(self):
"""Handle desktop file to start pibooth at the Raspberry Pi startup.
"""
dirname = osp.dirname(self.autostart_filename)
enable = self.getboolean('GENERAL', 'autostart')
delay = self.getint('GENERAL', 'autostart_delay')
if enable:
regenerate = True
if osp.isfile(self.autostart_filename):
with open(self.autostart_filename, 'r') as fp:
txt = fp.read()
if delay > 0 and f"sleep {delay}" in txt or delay <= 0 and "sleep" not in txt:
regenerate = False
if regenerate:
if not osp.isdir(dirname):
os.makedirs(dirname)
LOGGER.info("Generate the auto-startup file in '%s'", dirname)
with open(self.autostart_filename, 'w') as fp:
fp.write("[Desktop Entry]\n")
fp.write("Name=pibooth\n")
if delay > 0:
fp.write(f"Exec=bash -c \"sleep {delay} && pibooth\"\n")
else:
fp.write("Exec=pibooth\n")
fp.write("Type=application\n")
elif not enable and osp.isfile(self.autostart_filename):
LOGGER.info("Remove the auto-startup file in '%s'", dirname)
os.remove(self.autostart_filename)
[docs]
def join_path(self, *names):
"""Return the directory path of the configuration file
and join it the given names.
:param names: names to join to the directory path
:type names: str
"""
return osp.join(osp.dirname(self.filename), *names)
[docs]
def add_option(self, section, option, default, description, menu_name=None, menu_choices=None):
"""Add a new option to the configuration and defines its default value.
:param section: section in which the option is declared
:type section: str
:param option: option name
:type option: str
:param default: default value of the option
:type default: any
:param description: description to put in the configuration
:type description: str
:param menu_name: option label on graphical menu (hidden if None)
:type menu_name: str
:param menu_choices: option possible choices on graphical menu
:type menu_choices: any
"""
assert section, "Section name can not be empty string"
assert option, "Option name can not be empty string"
assert description, "Description can not be empty string"
# Find the caller plugin
stack = inspect.stack()
if len(stack) < 2:
plugin_name = "Unknown"
else:
plugin = inspect.getmodule(inspect.stack()[1][0])
plugin_name = self._pm.get_friendly_name(plugin, False)
# Check that the option is not already created
if section in DEFAULT and option in DEFAULT[section]:
raise ValueError(f"The plugin '{plugin_name}' try to define the option [{section}][{option}] "
"which is already defined.")
# Add the option to the default dictionary
description = f"{description}\n# Required by '{plugin_name}' plugin"
add_default_option(section, option, default, description, menu_name, menu_choices)
[docs]
def get(self, section, option, **kwargs):
"""Get a value from config. Return the default value if the section
or option is not defined.
:param section: config section name
:type section: str
:param option: option name
:type option: str
:return: value
:rtype: str
"""
if self.has_section(section) and self.has_option(section, option):
return super().get(section, option, **kwargs)
return str(DEFAULT[section][option][0])
[docs]
def set(self, section, option, value=None):
"""Set a value to config. Create the section if it is not defined.
:param section: config section name
:type section: str
:param option: option name
:type option: str
:param value: value to set
:type value: str
"""
if not self.has_section(section):
self.add_section(section)
super().set(section, option, value)
[docs]
def gettyped(self, section, option):
"""Get a value from config and try to convert it in a native Python
type (using the :py:mod:`ast` module).
:param section: config section name
:type section: str
:param option: option name
:type option: str
"""
value = self.get(section, option)
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
return value
[docs]
def getpath(self, section, option):
"""Get a path from config, evaluate the absolute path from configuration
file path.
:param section: config section name
:type section: str
:param option: option name
:type option: str
"""
return self._get_abs_path(self.get(section, option))
@staticmethod
def _get_authorized_types(types):
"""Get a tuple of authorized types and if the color and path are accepted
"""
if not isinstance(types, (tuple, list)):
types = [types]
else:
types = list(types)
color = False
if 'color' in types:
types.remove('color')
types.append(tuple)
types.append(list)
color = True # Option accept color tuples
path = False
if 'path' in types:
types.remove('path')
types.append(str)
path = True # Option accept file path
types = tuple(types)
return types, color, path
[docs]
def gettuple(self, section, option, types, extend=0):
"""Get a list of values from config. The values type shall be in the
list of authorized types. This method permits to get severals values
from the same configuration option.
If the option contains one value (with acceptable type), a tuple
with one element is created and returned.
:param section: config section name
:type section: str
:param option: option name
:type option: str
:param types: list of authorized types
:type types: list
:param extend: extend the tuple with the last value until length is reached
:type extend: int
"""
values = self.gettyped(section, option)
types, color, path = self._get_authorized_types(types)
if not isinstance(values, (tuple, list)):
if not isinstance(values, types):
raise ValueError(f"Invalid config value [{section}][{option}]={values}")
if values == '' and extend == 0:
# Empty config key and empty tuple accepted
values = ()
else:
values = (values,)
else:
# Check if one value is given or if it is a list of value
if color and len(values) == 3 and all(isinstance(elem, int) for elem in values):
values = (values,)
elif not all(isinstance(elem, types) for elem in values):
raise ValueError(f"Invalid config value [{section}][{option}]={values}")
if path:
new_values = []
for v in values:
if isinstance(v, str):
new_values.append(self._get_abs_path(v))
else:
new_values.append(v)
values = tuple(new_values)
while len(values) < extend:
values += (values[-1],)
return values