#!/usr/bin/env python3
# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
#===============================================================================
# Copyright Notice
# ----------------
# Copyright 2021 National Technology & Engineering Solutions of Sandia,
# LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS,
# the U.S. Government retains certain rights in this software.
#
# License (3-Clause BSD)
# ----------------------
# Copyright 2021 National Technology & Engineering Solutions of Sandia,
# LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS,
# the U.S. Government retains certain rights in this software.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#===============================================================================
"""
The :class:`~configparserenhanced.ConfigParserEnhanced` provides extended functionality for the :class:`~configparser.ConfigParser`
module. This class enables configurable parsing of ``.ini`` files by splitting
the **key** portion of an option in a configuration section into an **operation**
and a **parameter**. For example, given this ``.ini`` file:
.. code-block:: ini
:linenos:
[SECTION_NAME]
key: value
operation parameter: optional value
operation parameter uniquestr: optional value
The built-in parser will process each OPTION (``key: value`` pairs)
in-order within a SECTION. During the parsing process we attempt to
split the *key* field into two pieces consisting of an *operation*
and a *parameter*.
If there is a **handler method** associated with the **operation**
field that is extracted, then the parser will call that handler.
Handlers are added as methods named like ``_handler_<operation>()``
(with or without the leading underscore, which denotes a method not intended
to be overriden by subclasses)
and will be called if they exist. For example, if the **operation**
field resolves to ``use``, then the parser looks for a method called
``_handler_use()``.
If the handler exists, then it is invoked. Otherwise the parser treats
the OPTION field as a generic ``key: value`` pair as normal.
In this way, we can customize our processing by subclassing
:class:`~configparserenhanced.ConfigParserEnhanced` and defining our own handler methods.
Todo:
Determine if we can use the @final decorators (requires Python 3.8).
If it doesn't hurt older python versions we should use them to indicate
what methods should not be overridden, not that Python will enforce this
but it's better than nothing.
:Authors:
- William C. McLendon III <wcmclen@sandia.gov>
"""
from __future__ import print_function
import configparser
import inspect
import io
import os
from pathlib import Path
from pprint import pprint
import re
import shlex
import sys
try:
# @final decorator, requires Python 3.8.x
from typing import final # pragma: no cover
except ImportError: # pragma: no cover
pass # pragma: no cover
from .Debuggable import Debuggable
from .ExceptionControl import ExceptionControl
from .HandlerParameters import HandlerParameters
from .TypedProperty import typed_property
# Check for minimum required Python version
MIN_PYTHON = (3, 6)
if sys.version_info < MIN_PYTHON: # pragma: no cover
raise RuntimeError("Python %s.%s or later is required.\n" % (MIN_PYTHON))
# ============================================================
# S U P P O R T F U N C T I O N S A N D C L A S S E S
# ============================================================
class AmbiguousHandlerError(Exception):
"""Raised when the parser encounters ambiguity in Handler methods.
Attributes:
previous -- state at beginning of transition
next -- attempted new state
message -- explanation of why the specific transition is not allowed
"""
pass
# ===============================
# M A I N C L A S S
# ===============================
[docs]class ConfigParserEnhanced(Debuggable, ExceptionControl):
"""An enhanced ``.ini`` file parser built using :class:`ConfigParser`.
Provides an enhanced version of the :class:`ConfigParser` module, which enables some
extended processing of the information provided in a ``.ini`` file.
See Also:
- `ConfigParser reference <https://docs.python.org/3/library/configparser.html>`
"""
[docs] def __init__(self, filename=None):
"""Constructor
Args:
filename (str,Path,list): The ``.ini`` file or files to be loaded. If a ``str``
or ``Path`` is provided then we load only the one file. A ``list`` of strings
or ``Path`` s can also be provided, which will be loaded by :class:`ConfigParser`'s
`read() <https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.read>`_
method.
"""
if filename is not None:
self.inifilepath = filename
# -----------------------
# P R O P E R T I E S
# -----------------------
parse_section_last_result = typed_property(
"parse_section_last_result", (dict), default=None, internal_type=dict
)
default_section_name = typed_property("default_section_name", str, default="DEFAULT")
_internal_default_section_name = typed_property(
"_internal_default_section_name", str, default="CONFIGPARSERENHANCED_COMMON"
)
@property
def inifilepath(self) -> list:
"""Provides access to the path to the ``.ini`` file (or files).
``inifilepath`` can be set to one of these things:
1. A ``str`` contining a path to a ``.ini`` file.
2. A ``pathlib.Path`` object pointing to a ``.ini`` file.
3. A ``list`` of one or more of (1) or (2).
Entries in the list will be converted to ``pathlib.Path`` objects.
Returns:
list: A ``list`` containing the ``.ini`` files that will be processed.
Note:
Subclasses should not override this.
"""
if not hasattr(self, '_inifilepath'):
raise ValueError("ERROR: The filename has not been specified yet.")
return self._inifilepath
@inifilepath.setter
def inifilepath(self, value) -> list:
self._validate_parameter(value, (str, list, Path))
# If we have already loaded a .ini file, we should reset the data
# structure. Delete any lazy-created properties, etc.
if hasattr(self, '_inifilepath'):
if hasattr(self, '_configparserdata'):
delattr(self, '_configparserdata')
self._reset_lazy_attr("_loginfo")
# Internally we represent the inifile as a `list of Path` objects.
# Do the necessary conversions to make that so.
if not isinstance(value, list):
value = [value]
self._inifilepath = []
for entry in value:
try:
self._inifilepath.append(Path(entry))
except TypeError as ex:
self.debug_message(0, "ERROR: invalid entry in `inifilepath` list.")
raise ex
return self._inifilepath
@property
def configparserdata(self) -> configparser.ConfigParser:
"""The raw results to a vanilla :class:`ConfigParser`-processed ``.ini`` file.
This property is lazy-evaluated and will be processed the first time it is
accessed.
Returns:
ConfigParser: The object containing the contents of the loaded
``.ini`` file.
Raises:
ValueError: If the length of ``self.inifilepath`` is zero.
IOError: If any of the files in ``self.inifilepath`` don't
exist or are not files.
Note:
Subclasses should not override this.
See Also:
- Python library: `configparser <https://docs.python.org/3/library/configparser.html>`_
reference and user-guide.
"""
if not hasattr(self, '_configparserdata'):
self._configparserdata = configparser.ConfigParser(
allow_no_value=True,
delimiters=self.configparser_delimiters,
default_section=self._internal_default_section_name
)
# Prevent ConfigParser from lowercasing the keys.
self._configparserdata.optionxform = str
# configparser.ConfigParser.read() will not fail if it doesn't read the
# .ini file(s) in the list, it'll just happily continue on and return
# whatever it does get... or an empty configuration if no files were found.
# We want to fail if we provide a bad file name so we need to check here.
if len(self.inifilepath) == 0:
raise ValueError("ERROR: No .ini filename(s) were provided.")
for inifilepath_i in self.inifilepath:
# Sanity type check here -- we'd throw on the .exists() and .is_file()
# methods below if the entry isn't a Path object, but the error might
# be cryptic. This will throw a more explicit error.
# This should never happen if the users set things up through the
# property interface.
if isinstance(inifilepath_i, Path) is not True:
raise TypeError("INTERNAL ERROR: .ini file paths should be Path objects!")
if (inifilepath_i.exists() and inifilepath_i.is_file()) is not True:
msg = f"\n" + \
f"+" + "="*78 + "+\n" + \
f"| ERROR: Unable to load configuration .ini file\n" + \
f"| - Requested file: `{inifilepath_i}`\n" + \
f"| - CWD: `{os.getcwd()}`\n" + \
f"+" + "="*78 + "+\n"
raise IOError(msg)
try:
self._configparserdata.read(self.inifilepath, encoding='utf-8')
except configparser.DuplicateOptionError as ex:
delattr(self, '_configparserdata')
message = "ERROR: Configparser found a section with "
message += "two options with identical keys."
self.debug_message(0, message)
raise ex
return self._configparserdata
@property
def configparser_delimiters(self) -> tuple:
"""Delimiters for the configparser
Changing the value of this will trigger a **reset** of
the cached data in the class since this fundamentally
changes how `ConfigParser` will parse the .ini file.
This defaults to ``('=', ':')`` which is the default
in ConfigParser (https://docs.python.org/3/library/configparser.html)
Returns:
tuple: Returns a tuple containing the delimiters.
Raises:
TypeError: If assignment of something other than a ``tuple``,
a ``list``, or a ``str`` is attempted.
"""
if not hasattr(self, '_configparser_delimiters'):
self._configparser_delimiters = ('=', ':')
return self._configparser_delimiters
@configparser_delimiters.setter
def configparser_delimiters(self, value) -> tuple:
self._validate_parameter(value, (tuple, list, str))
self._reset_configparserdata()
self._configparser_delimiters = tuple(value)
return self._configparser_delimiters
@property
def configparserenhanceddata(self):
"""Enhanced ``configparserdata`` ``.ini`` file information data.
This *property* returns a *parsed* representation of the ``configparserdata``
that would be loaded from our ``.ini`` file. The data in this will return the
contents of a section plus its parsed results. For example, if we have this
in our ``.ini`` file:
.. code-block:: ini
[SEC A]
key A1: value A1
[SEC B]
use 'SEC A':
key B1: value B1
Extracting the data from 'SEC B' would result the contents of 'SEC B' + 'SEC A':
>>> ConfigParserEnhancedObj.configparserenhanceddata["SEC B"]
{
'key A1': 'value A1',
'key B1': 'value B1'
}
Options that resolve to a format matching an operation
(``operation parameter : value``) what *also* have a handler
defined (i.e, the operation is "handled") will **not** be included
in the data of a section. This is shown in the example above where
the ``use 'SEC A':`` option in ``[SEC B]`` is not included in
``configparserenhanceddata["SEC B"]`` after processing.
Returns:
:class:`~configparserenhanced.ConfigParserEnhanced.ConfigParserEnhancedData`
Note:
Subclasses should not override this.
"""
if not hasattr(self, '_configparserenhanceddata'):
self._configparserenhanceddata = self.ConfigParserEnhancedData(owner=self)
return self._configparserenhanceddata
# ---------------------------------------
# P U B L I C A P I M E T H O D S
# ---------------------------------------
[docs] def write(
self, file_object, space_around_delimiters=True, section=None, use_base_class_parser=True
) -> int:
"""File writer utility for ConfigParserEnhanced objects.
Writes the output of :py:meth:`unroll_to_str` to a file.
Args:
file_object (:obj:`File Ptr`): Pointer to the file that should be written.
space_around_delimiters (bool): If ``True`` the delimiter is given an extra
space of padding.
section (:obj:`str`): If a section name is provided we only generate the specified
section (if it exists), otherwise we generate output for all sections.
Raises:
TypeError: If ``file_object`` is not a file pointer (instance or derivitive
of ``io.IOBase``).
"""
self._validate_parameter(file_object, (io.IOBase))
text = self.unroll_to_str(
section=section,
space_around_delimiters=space_around_delimiters,
use_base_class_parser=use_base_class_parser
)
file_object.write(text)
return 0
[docs] def assert_file_all_sections_handled(self) -> int:
"""
Checks that ALL the options within a file are fully handled.
This calls ``assert_section_all_options_handled`` on all the sections
of a .ini file.
Returns:
int: 0 indicates a successful parse with no errors. Otherwise if problems
are detected then the return code will be nonzero.
Note the return value only occurs if ``exception_control_level`` is
2 or lower (See **Raises** notes on this method for more details).
Raises:
ValueError: A ``ValueError`` is raised if there are any *unhandled options*
detected in the file(s) loaded in *any* section. The error can
be suppressed by setting ``exception_control_level`` to suppress
``SERIOUS`` errors (2 or lower).
If the exception is suppressed then this method will return a
nonzero integer.
"""
output = 0
for section in self.configparserenhanceddata.sections(parse=False):
err = self.assert_section_all_options_handled(section, do_raise=False)
if err != 0:
self.debug_message(0, err)
output = 1
if output != 0:
tmp_fileslist = [str(x) for x in self.inifilepath]
message = self.get_known_operations_message()
message += "\nThis configuration was loaded from the file(s):\n|- "
message += "\n|- ".join(tmp_fileslist)
self.debug_message(0, message)
self.exception_control_event("SERIOUS", ValueError, message.strip())
return output
[docs] def assert_section_all_options_handled(self, section_name: str, do_raise: bool = True) -> int:
"""
This method provides a validation capability against a *section* in a
.ini file to validate that all of the entries are handled. The use case
of this is fairly specific will flag any 'unhandled' options within
a section under an expectation that the intent or design of the .ini
file is that _all_ options are handled by some handler and _any_ that
are not indicate an error or typo of some sort.
Args:
section_name (str): The _name_ of the section that will be searched.
Note: in ``ConfigParserEnhanced`` this includes the fully parsed
section which includes the *transitive closure* of the dependency
graph imposed by ``use`` operations.
do_raise (bool): If True (default) then this assert will trigger
an ``exception_control_event`` that may raise a ``ValueError``
if there are unhandled entries detected.
If False then the return value is set to nonzero if unhandled
entries are found but the **ece** is not triggered.
Returns:
int: 0 indicates a successful parse with no errors, otherwise
any nonzero return value indicates there is at least one
entry in the section that was not properly handled.
IF a *bad* entry is found then the output will be a string
containing a message that indicates what the bad entries
are.
Raises:
ValueError: A ``ValueError`` is raised if there are any *unhandled options*
detected in the file(s) loaded in *any* section. The error can
be suppressed by setting ``exception_control_level`` to suppress
``SERIOUS`` errors (2 or lower).
If the exception is suppressed then this method will return a
nonzero integer.
"""
output = 0
section_data = self.configparserenhanceddata.get(section_name)
if len(section_data):
message = f"Unhandled option found in section `{section_name}`"
message += " or one of its dependent sections.\n"
message += "The following entries are unhandled:\n"
for k, v in section_data.items():
message += f"|- '{k}'\n"
if do_raise:
tmp_fileslist = [str(x) for x in self.inifilepath]
message += self.get_known_operations_message()
message += "\nThis configuration was loaded from the file(s):\n|- "
message += "\n|- ".join(tmp_fileslist)
self.exception_control_event("SERIOUS", ValueError, message.strip())
output = message.rstrip()
return output
[docs] def get_known_operations_message(self):
"""
Generate a string that lists valid **operations**.
Returns:
str: A string containing the message listing the valid operations.
"""
message = "Valid operations are:\n"
for op in self.get_known_operations():
message += f"|- '{op}'\n"
return message.rstrip()
[docs] def get_known_operations(self):
"""
Generate a list of *known* operation keywords based on the
existing handlers defined in this class.
There is potentially some data loss here since there is nothing in
ConfigParserEnhanced that would prevent someone from defining an
*operation* of the form ``foo_bar`` rather than ``foo-bar`` but since
the parser will always convert ``-`` to ``_``, in terms of processing
an operation there would be no distinction between ``foo-bar`` and
``foo_bar``.
Returns:
list: A list of strings is returned containing the list of
known operations based on existing handlers.
"""
# Regex that looks for ``_handler`` or ``handler`` at the front of a string
re_handler_name = re.compile(r"^_?handler_")
# Regex that looks for strings that match any one of these options:
# - "_handler_initialize"
# - "_handler_finalize"
# - "handler_initialize"
# - "handler_finalize"
re_handler_init_final = re.compile(r"^_?handler_((finalize)|(initialize))$")
# Regex that is used in the list comprehension below to strip the handler
# prefix out of the name when we attempt to generate the operation names.
# - Matches "_handler_" and "handler_"
re_strip_handler_prefix = re.compile(r"^_?handler_")
# This list comprehension scans through the defined methods and will
# pull out every method that starts with "_handler" or "handler"
# and then strips the optional leading "_" off, then strips the
# leading "handler_" component off and finally replaces "_" with "-"
# in the handler name to generate the operation name.
# wcmclen - 2021-09-03 - The TypedProperty entry for `parse_Section_last_result`
# was triggering the exception that required assignment before use
# in the `getmembers` call here. I removed that condition on the property
# but it might be worth figuring out why the inspection triggered it.
# Perhaps `TypedProperty` needs a proper _getter_ implemented?
output = [
re_strip_handler_prefix.sub("", x[0]).replace("_", "-")
for x in inspect.getmembers(self, predicate=inspect.ismethod)
if re_handler_name.search(x[0]) and not re_handler_init_final.search(x[0])
]
return output
[docs] def unroll_to_str(self, section=None, space_around_delimiters=True, use_base_class_parser=True) -> str:
"""Unroll a section or whole .ini file to a string
This method generates a string that is the equivalent to the original
``.ini`` file that is loaded with all the ``use`` operations processed
so that the output will look like a regular ``.ini`` file.
If ``use_base_class_parser`` is set then we instantiate a base-class
:py:class:`ConfigParserEnhanced` that will load and (re)parse the
:py:attr:`inifilepath` file(s) to generate a "clean" copy. Because all
options that have *handled* operations in them will be stripped from the
generated .ini file, the most common use case is that we just want the
``use`` operations processed and stripped, but this paramter gives the
option to disable that capability.
Args:
section (:obj:`str`): If a section name is provided we only generate the specified
section (if it exists), otherwise we generate output for all sections.
space_around_delimiters (bool): If ``True`` the delimiter is given an extra
space of padding.
use_base_class_parser (bool): If ``Tru`` then we instantiate a
:py:class:`ConfigParserEnhanced` object to parse the .ini file.
If ``False`` then we use the current class (possibly a sub-class)
of :py:class:`ConfigParserEnhanced`, which will strip out all options
that have *handled* operations.
Raises:
TypeError: If ``section`` is not a ``str`` or ``None`` object.
"""
def __generate_section(section: str, parser, delimiter=":"):
"""
Helper function (inner function)
This function generates the string containing the section header
and options.
"""
output = ""
if parser.configparserenhanceddata.has_section(section):
output += f"[{section}]\n"
for key, value in parser.configparserenhanceddata.items(section):
if value is None:
value = ""
output += delimiter.join([key, value]).strip() + "\n"
else:
raise KeyError(f"Section `{section}` was not found.")
return output
self._validate_parameter(section, (type(None), str))
output_str = ""
delimiter = ":"
if space_around_delimiters:
delimiter = " {} ".format(delimiter)
parser = None
if use_base_class_parser:
parser = ConfigParserEnhanced(filename=self.inifilepath)
else:
parser = self
# Turn off ECL notifications
ecl = parser.exception_control_level
parser.exception_control_level = 0
section_list = parser.configparserenhanceddata.sections()
if section is None:
for section in section_list:
output_str += __generate_section(section, parser, delimiter)
output_str += "\n"
else:
output_str += __generate_section(section, parser, delimiter)
# reset parser ECL (only useful when not using base class parser)
parser.exception_control_level = ecl
output_str = output_str.strip()
output_str += "\n"
return output_str
# -------------------------------------
# P A R S E R P U B L I C A P I
# -------------------------------------
[docs] def parse_all_sections(self):
"""Parse ALL sections in the .ini file.
This can be useful if the user wishes to pre-parse all the sections
of an ``ini`` file in one go.
"""
self.configparserenhanceddata.sections(True)
return
[docs] def parse_section(self, section, initialize=True, finalize=True):
"""Execute parser operations for the provided *section*.
Args:
section (str): The section name that will be parsed and retrieved.
initialize (bool): If True then :meth:`handler_initialize()` will be executed
at the start of the search.
finalize (bool): If True then :meth:`handler_finalize()` will be executed
at the end of the search.
Returns:
:attr:`~.HandlerParameters.data_shared` property from :class:`~.HandlerParameters`.
Unless :class:`~.HandlerParameters` is changed, this wil be a ``dict`` type.
"""
# If a previous run generated _loginfo, clear it before this run.
self._reset_lazy_attr("_loginfo")
self.debug_message(1, f"[" + "-"*58 + ']')
self.debug_message(1, f" Parse section `{section}` START")
self.debug_message(1, f"[" + "-"*58 + ']')
self._validate_parameter(section, (str))
if section == "":
raise ValueError("`section` cannot be empty.")
# Parse the requested section.
result = self._parse_section_r(section, initialize=initialize, finalize=finalize)
# caches the "data_shared" component of handler_parameters
self.parse_section_last_result = result
self.debug_message(1, f"[" + "-"*58 + ']')
self.debug_message(1, f" Parse section `{section}` FINISH")
self.debug_message(1, f"[" + "-"*58 + ']')
return result
# ---------------------------------
# D E C O R A T O R S
# ---------------------------------
[docs] def operation_handler(func_handler):
"""
Implements the ``operation_handler`` decorator.
This decorator is used on *handlers* and implements entry and exit
calls and exit value checks.
.. code-block:: python
:linenos:
@operation_handler
def handler_my_operation(self, section_name, handler_parameters) -> int:
# do stuff
return 0
Args:
func_handler (Callable): Reference to the handler function.
This gets managed through Python's decorator syntax.
"""
def wrapper(self, section_name, handler_parameters):
self._validate_parameter(section_name, (str))
self.enter_handler(handler_parameters)
output = func_handler(self, section_name, handler_parameters)
self.exit_handler(handler_parameters)
self._check_handler_rval(handler_parameters.handler_name, output)
return output
return wrapper
# ---------------------------------
# P U B L I C H A N D L E R S
# ---------------------------------
[docs] def enter_handler(self, handler_parameters):
"""General tasks to do when entering a handler
This executes some general operations that should be executed when entering
a handler. Generally, these are just logging and console messages for
debugging.
Args:
handler_parameters (HandlerParameters): The parameters passed to
the handler.
"""
self._validate_handlerparameters(handler_parameters)
handler_name = handler_parameters.handler_name
self.debug_message(1, f"Enter handler : {handler_name}") # Console
self.debug_message(1, f" -> raw_option : {handler_parameters.raw_option}") # Console
self.debug_message(2, f" -> op : {handler_parameters.op}") # Console
self.debug_message(2, f" -> params : {handler_parameters.params}") # Console
self.debug_message(2, f" -> value : {handler_parameters.value}") # Console
self.debug_message(3, f" -> data_shared : {handler_parameters.data_shared}") # Console
self.debug_message(4, f" -> data_internal: {handler_parameters.data_internal}") # Console
self._loginfo_add(
'handler-entry', {
'name': handler_name,
'entry': handler_parameters.raw_option,
'parameters': handler_parameters
}
)
return
[docs] def exit_handler(self, handler_parameters):
"""General tasks to do when entering a handler
This executes some general operations that should be executed when entering
a handler. Generally, these are just logging and console messages for
debugging.
Args:
handler_parameters (HandlerParameters): The parameters passed to
the handler.
"""
self._validate_handlerparameters(handler_parameters)
handler_name = handler_parameters.handler_name
self.debug_message(1, f"Exit handler : {handler_name}") # Console
self.debug_message(1, f" -> raw_option : {handler_parameters.raw_option}") # Console
self.debug_message(3, f" -> data_shared : {handler_parameters.data_shared}") # Console
self.debug_message(4, f" -> data_internal: {handler_parameters.data_internal}") # Console
self._loginfo_add(
'handler-exit', {
'name': handler_name,
'entry': handler_parameters.raw_option,
'parameters': handler_parameters
}
)
return
[docs] @operation_handler
def _generic_option_handler(self, section_name, handler_parameters) -> int:
"""Generic Handler Template
This handler is used for options whose ``key:value`` pair does not
get resolved to a proper ``<operation>`` and therefore do not get
routed to a ``handler_<operation>()`` method.
This method provides a great *template* for subclasses to use when
creating new custom handlers according to the naming scheme
``handler_<operation>()`` or ``_handler_<operation>()``.
Note:
This method should not be overridden by subclasses.
Args:
section_name (str): The name of the section being processed.
handler_parameters (HandlerParameters): The parameters passed to
the handler.
Returns:
int:
* 0 : SUCCESS
* [1-10]: Reserved for future use (WARNING)
* > 10 : An unknown failure occurred (CRITICAL)
"""
# -----[ Handler Content Start ]-------------------
# -----[ Handler Content End ]---------------------
return 0
[docs] @operation_handler
def handler_initialize(self, section_name, handler_parameters) -> int:
"""Initialize a recursive parse search.
This handler is called at the start of a recursive search of the
``.ini`` structure. Subclasses can override this method to perform setup
actions at the start of a search.
Args:
section_name (str): The name of the section being processed.
handler_parameters (HandlerParameters): The parameters passed to
the handler.
Returns:
int:
* 0 : SUCCESS
* [1-10]: Reserved for future use (WARNING)
* > 10 : An unknown failure occurred (CRITICAL)
"""
# -----[ Handler Content Start ]-------------------
# -----[ Handler Content End ]---------------------
return 0
[docs] @operation_handler
def handler_finalize(self, section_name, handler_parameters) -> int:
"""Finalize a recursive parse search.
This handler is called at the end of a search and can be used
to *finalize* processing of ``.ini`` sections or save/cache
data from the search that other handlers added to
``handler_parameters.data_shared``.
Args:
section_name (str): The name of the section being processed.
handler_parameters (HandlerParameters): The parameters passed to
the handler.
For this handler, the ``handler_parameters`` entries will only populate
the ``handler_name`` and ``data_shared`` properties.
Returns:
int:
* 0 : SUCCESS
* [1-10]: Reserved for future use (WARNING)
* > 10 : An unknown failure occurred (CRITICAL)
"""
# -----[ Handler Content Start ]-------------------
# -----[ Handler Content End ]---------------------
return 0
# ---------------------------------------------------
# P A R S E R H E L P E R S ( P R I V A T E )
# ---------------------------------------------------
[docs] def _parse_section_r(self, section_name, handler_parameters=None, initialize=True, finalize=True):
"""Recursive driver of the parser.
This is the main heavy lifter of the parser.
Args:
section_name (str): The name of the section being processed.
handler_parameters (HandlerParameters): The parameters passed to
the handler.
initialize (bool): If enabled *and* this level of recursion is the *root*
level then we will call ``handler_initialize()`` to do some preprocessing
activities.
finalize (bool): If enabled *and* this level of recursion is the *root*
level then we will call ``handler_finalize()`` to wrap up the
recursion.
Returns:
:attr:`~HandlerParameters.data_shared`
"""
self._validate_parameter(section_name, (str))
self._validate_parameter(initialize, (bool))
self._validate_parameter(finalize, (bool))
# Initialize handler_parameters if not currently set up.
if handler_parameters is None:
handler_parameters = self._new_handler_parameters()
# Check that we got the right data structure from _new_handler_parameters
# in case someone overrides this later on.
self._validate_parameter(handler_parameters, (HandlerParameters))
handler_parameters.section_root = section_name
# Pitfall: Only add 'sections_checked' for the _root_ node
# because configparserenhanceddata recurses through and we
# want it's "meta section" to encapsulate the result
# of the fully parsed entry from the the root section
# of the search only.
self.configparserenhanceddata._sections_checked.add(section_name)
else:
# If we got a handler_parameters handed to us (i.e., recursion)
# we should make a new HandlerParameters object and copy references
# of the parts that need to be updated by all levels of recursion
# but not allow side-effects to modify the other entries that are
# relevant to the caller's scope.
handler_parameters = self._new_handler_parameters(handler_parameters)
# Execute _handler_initialize to add a pre-processing step.
if initialize and section_name == handler_parameters.section_root:
handler_initialize_params = self._new_handler_parameters(handler_parameters)
handler_initialize_params.handler_name = "handler_initialize"
self.handler_initialize(section_name, handler_initialize_params)
if self.configparserdata.has_section(self.default_section_name):
self._parse_section_r(
self.default_section_name,
handler_parameters=handler_parameters,
initialize=False,
finalize=False
)
self.debug_message(1, f">>> Enter section : `{section_name}`") # Console Logging
self._loginfo_add('section-entry', {'name': section_name}) # Logging
# Load the section from the configparser.ConfigParser data.
current_section = None
try:
current_section = self.configparserdata[section_name]
except KeyError:
message = "ERROR: No section named `{}` was found in the configuration file {}.".format(
section_name, self.inifilepath
)
raise KeyError(message)
if current_section is None: # pragma: no cover (UNREACHABLE)
raise Exception("ERROR: Unable to load section `{}` for an unknown reason.".format(section_name))
# At this point we should know we have a valid/existing section.
self.configparserenhanceddata.add_section(section_name)
# Initialize and set processed_sections.
self._validate_handlerparameters(handler_parameters)
handler_parameters.data_internal['processed_sections'].add(section_name)
for sec_k, sec_v in current_section.items():
sec_k = str(sec_k).strip()
# sec_v should be either a string or a NoneType entry. In the
# general case of either `key: value` or `key = value`, the value
# will be a string. If the user specified `key:` (that is, with a
# separator, but without a value), then the value is the empty
# string "". If the user omits the separator *and* the value,
# e.g., by specifying only `key`, then the value will be a
# NoneType.
if sec_v is not None:
sec_v = str(sec_v).strip()
sec_v = sec_v.strip('"')
handler_parameters.raw_option = (sec_k, sec_v)
handler_parameters.value = sec_v
self.debug_message(2, f"==>")
self.debug_message(2, f"==> Entry : `{sec_k}` : `{sec_v}`") # Console
self.debug_message(2, f"==>")
self._loginfo_add('section-key-value', {'key': sec_k, 'value': sec_v}) # Logging
# sec_k_tok = shlex.split(sec_k) # DEPRECATED
sec_k_tok = self._tokenize_option_key(sec_k)
if not re.match(r"^[\w\-]+$", sec_k_tok[0]):
# Call generic_handler if the first entry has invalid characters
self._launch_generic_option_handler(section_name, handler_parameters, sec_k, sec_v)
else:
# Otherwise, it _could_ be a 'handled' operation
op, params = self._get_op_components_from_tokenized_option_key(sec_k_tok)
#op = self._apply_transformation_to_operation(op) # DEPRECATED
#params = [self._apply_transformation_to_parameter(x) for x in params] # DEPRECATED
handler_parameters.op = op
handler_parameters.params = params
self._loginfo_add('section-operation', {'op': op, 'params': params}) # Logging
self.debug_message(2, f" -> op : {handler_parameters.op}") # Console
self.debug_message(2, f" -> params : {handler_parameters.params}") # Console
self.debug_message(2, f" -> value : {handler_parameters.value}") # Console
handler_name, ophandler_f = self._locate_handler_method(handler_parameters.op)
if ophandler_f is not None:
handler_parameters.handler_name = handler_name
ophandler_f(section_name, handler_parameters)
else:
self._launch_generic_option_handler(section_name, handler_parameters, sec_k, sec_v)
# If we're exiting recursion from the root node and and finalize is
# enabled, we call the finalize handler.
if finalize and section_name == handler_parameters.section_root:
handler_finalize_params = self._new_handler_parameters(handler_parameters)
handler_finalize_params.handler_name = "handler_finalize"
self.handler_finalize(section_name, handler_finalize_params)
# Remove the section from the `processed_sections` field when we exit.
# - This properly enables a true depth-first search of `use` links.
self._validate_handlerparameters(handler_parameters)
handler_parameters.data_internal['processed_sections'].remove(section_name)
# Set up the return value.
output = handler_parameters.data_shared
# Finalize the logging data / output
self._loginfo_add('section-exit', {'name': section_name}) # Logging
self.debug_message(1, "Exit section: `{}`".format(section_name)) # Console
return output
def _tokenize_option_key(self, option_key):
"""
"""
option_key = str(option_key).strip()
option_key_tok = shlex.split(option_key)
return option_key_tok
def _get_op_components_from_tokenized_option_key(self, option_key_tok):
"""
Take a partitioned ``operation`` that comes in as a list and
split it up into the ``operation`` and ``parameters``.
Returns:
tuple: A tuple containing the ``(op, params)`` where ``op`` is
a string and ``params`` will be a list of 0..N parameters.
"""
op = option_key_tok[0]
params = option_key_tok[1 :]
# Clean up / apply necessary transformations to the operation and parameters
op = self._apply_transformation_to_operation(op)
params = [self._apply_transformation_to_parameter(x) for x in params]
return (op, params)
[docs] def _validate_handlerparameters(self, handler_parameters):
"""Check :class:`HandlerParameters`.
Check the ``handler_parameters`` object that's being passed around
to the handlers to verify that items in it have the proper types.
Raises:
TypeError: Raises a ``TypeError`` if
``handler_parameters.data_internal['processed_sections']`` is not a ``set``
type.
"""
self._validate_parameter(handler_parameters, (HandlerParameters))
if not isinstance(handler_parameters.data_internal['processed_sections'], set):
message = "`handler_parameters.data_internal['processed_sections']` " + \
"must be a `set` type."
raise TypeError(message)
return None
[docs] def _new_handler_parameters(self, handler_parameters=None) -> HandlerParameters:
"""Generate a new :class:`~configparserenhanced.HandlerParameters` object.
This is called inside the parser to generate :class:`HandlerParameters`.
If subclasses extend the :class:`~configparserenhanced.HandlerParameters`
class, this can be overridden.
Args:
handler_parameters: an existing ``HandlerParameters`` object to copy the
*persistent* state over from (i.e., ``data_shared``, ``data_internal``)
for continuity in the new object.
Returns:
:class:`~configparserenhanced.HandlerParameters` object.
"""
new_handler_parameters = HandlerParameters()
new_handler_parameters.data_internal['processed_sections'] = set()
# Copy the 'persistent' state from the old handler_parameters object.
if handler_parameters != None:
new_handler_parameters.data_internal = handler_parameters.data_internal
new_handler_parameters.data_shared = handler_parameters.data_shared
new_handler_parameters.section_root = handler_parameters.section_root
return new_handler_parameters
[docs] def _locate_handler_method(self, operation) -> tuple:
"""Convert ``operation`` to a handler name and get the reference to the handler.
This method converts the *operation* parameter (op) to a
handler name and searches for the existence of a matching
method.
Handlers may be of the format: ``_handler_<operation>`` for
internal/private handlers (that should not be overridden)
or ``handler_<operation>`` for handlers that are considered
fair game for subclasses to override and customize.
In the case where there are both public and private handlers for
the same operation defined, we will optionally raise a
``AmbiguousHandlerErorr`` if the ``exception_control_level`` is set
high enough to trigger "SERIOUS" events. Otherwise, we will choose
the *PUBLIC* handler definition to execute.
Args:
operation (str): The operation parameter that is converted to
a proper handler name of the form ``_handler_{operation}()``
or ``handler_{operation}()``.
Returns:
tuple: A tuple containing the ``(handler_name, handler_method)`` where:
- ``handler_name`` is a string.
- ``handler_method`` is either a reference to the handler method
if it exists, or None if it does not exist.
Raises:
AmbiguousHandlerError: An ``AmbiguousHandlerError`` might be raised
if both a *public* and a *private* handler of the same name are
defined and the ``exception_control_level`` is set to raise
exceptions for "SERIOUS" events.
"""
self._validate_parameter(operation, (str))
handler_name = operation
handler_name = self._apply_transformation_to_operation(handler_name)
handler_name_private = "_handler_{}".format(handler_name)
handler_name_public = "handler_{}".format(handler_name)
handler_public = self._locate_class_method(handler_name_public)
handler_private = self._locate_class_method(handler_name_private)
if (handler_private[1] is not None) and (handler_public[1] is not None):
message = "Ambiguous handler name."
message += " Both `{}` and `{}` exist".format(handler_name_private, handler_name_public)
message += " but only one is allowed."
self.exception_control_event("SERIOUS", AmbiguousHandlerError, message)
output = (None, None)
if handler_public[1] is not None:
self.debug_message(4, f" -> Using _public_ handler : `{handler_name_public}`") # Console
output = handler_public
elif handler_private[1] is not None:
self.debug_message(4, f" -> Using _private_ handler: `{handler_name_private}`") # Console
output = handler_private
else:
self.debug_message(4, f" -> No handler found for operation `{handler_name}`") # Console
return output
[docs] def _locate_class_method(self, method_name) -> tuple:
"""Helper that locates a class method (if it exists)
Args:
method_name (str): The name of the class method we're searching for.
Returns:
tuple: A tuple containing the name and a reference to the method if it
is found or None if not found. ``(method_name, method_ref or None)``
"""
output = (None, None)
method_f = getattr(self, method_name, None)
output = (method_name, method_f)
if output[1] == None:
self.debug_message(5, f" -> Class method `{method_name}` was not found.") # Console
return output
[docs] def _check_handler_rval(self, handler_name, handler_rval) -> 0:
"""Check the returned value from a handler.
Args:
handler_name (string): The name of the handler.
handler_rval (int): The return value from a handler being processed.
Returns:
int: Returns a 0 if everything is ok or a 1 if a warning occurred.
Raises:
RuntimeError: If ``handler_rval > 0`` and the ``exception_control_level``
is high enough, depending on the value of ``handler_rval``.
"""
self._validate_parameter(handler_name, (str))
self._validate_parameter(handler_rval, (int))
self.debug_message(2, f"_check_handler_rval({handler_name}, {handler_rval})")
output = 0
if handler_rval == 0:
pass
elif handler_rval <= 10:
# These rvals are currently reserved. If someone uses it we should
# throw a critical error to get the developers' attention to either
# expand rval handling or users should use something > 10.
self.exception_control_event(
"WARNING", RuntimeError, f"Handler `{handler_name}` returned {handler_rval}"
)
output = 1
else:
self.exception_control_event(
"CATASTROPHIC", RuntimeError, f"Handler `{handler_name}` returned {handler_rval}"
)
return output
[docs] def _launch_generic_option_handler(self, section_name, handler_parameters, sec_k, sec_v) -> int:
"""Launcher for ``_generic_option_handler()``
``_generic_option_handler()`` is called in two places inside the recursive parser.
1. It is called when the ``key:value`` pair in an option does not parse out
to ``operation`` and ``parameter`` fields.
2. It is called when the ``key:value`` pair in an option does parse to
an ``operation`` and a ``parameter`` field, but there are no handlers
defined for the ``operation``.
This helper just manages the call in both places so thay're done the same way.
Returns:
int: Returns the output value from ``_generic_option_handler()``
"""
self._validate_parameter(section_name, (str))
self._validate_handlerparameters(handler_parameters)
self._validate_parameter(sec_k, (str, None))
self._validate_parameter(sec_v, (str, None))
output = 0
self.configparserenhanceddata.set(handler_parameters.section_root, sec_k, sec_v)
handler_parameters.handler_name = "_generic_option_handler"
output = self._generic_option_handler(section_name, handler_parameters)
return output
# ---------------------------------------
# H A N D L E R S ( P R I V A T E )
# ---------------------------------------
[docs] @operation_handler
def _handler_use(self, section_name, handler_parameters) -> int:
"""
This is a handler that will get executed when we detect a `use` operation in
our parser.
Args:
section_name (str): The name of the section being processed.
handler_parameters (HandlerParameters): The parameters for the current operation.
Returns:
int:
* 0 : SUCCESS
* [1-10]: Reserved for future use (WARNING)
* > 10 : An unknown failure occurred (CRITICAL)
Todo:
Once we can use Python 3.8 in our environments, we can use the @final decorator
to mark this as something that should not be overridden. We also have to
import it: `from typing import final`.
https://stackoverflow.com/questions/321024/making-functions-non-override-able
"""
entry = handler_parameters.raw_option
handler_name = handler_parameters.handler_name
op1 = handler_parameters.op
op2 = handler_parameters.params[0]
if op2 not in handler_parameters.data_internal['processed_sections']:
self._parse_section_r(op2, handler_parameters, finalize=False)
else:
self._loginfo_add('cycle-detected', {'sec-src': section_name, 'sec-dst': op1}) # Logging
self._loginfo_add('handler-exit', {'name': handler_name, 'entry': entry}) # Logging
message = f"Detected a cycle in `use` dependencies in .ini file {self.inifilepath}.\n"
message += f"- cannot load [{op2}] from [{section_name}]."
self.exception_control_event("WARNING", ValueError, message)
return 0
# -------------------------------------
# H E L P E R S ( P R I V A T E )
# -------------------------------------
def _reset_configparserdata(self) -> int:
"""Reset the internal state for all of the ConfigParser data.
Resets these properties to their initial state:
- ``configparserdata``
- ``configparserenhanceddata``
- ``parse_section_last_result``
- ``_loginfo``
"""
self._reset_lazy_attr("_loginfo")
self._reset_lazy_attr("_configparserdata")
self._reset_lazy_attr("_configparserenhanceddata")
del self.parse_section_last_result
return 0
def _reset_lazy_attr(self, attribute: str) -> int:
"""Deletes an attribute of the class if it exists.
This is used to reset lazy-evaluated property private storage
back down to the initial ground-state.
"""
self._validate_parameter(attribute, (str))
if hasattr(self, attribute):
delattr(self, attribute)
return 0
[docs] def _loginfo_add(self, typeinfo, entry) -> None:
"""
If in debug mode, we can use this to log operations by appending to ``_loginfo``.
Args:
typeinfo (str): The kind of operation this is. This generates the
'type' entry in the ``_loginfo`` dict. (Required)
entry (dict): A dictionary containing log information that we're appending.
At minimum it should have: ``{ type: typestring }``.
Todo:
Once we can use Python 3.8 in our environments, we can use the @final decorator
to mark this as something that should not be overridden. We also have to
import it: `from typing import final`.
https://stackoverflow.com/questions/321024/making-functions-non-override-able
"""
if not hasattr(self, '_loginfo'):
self._loginfo = []
if self.debug_level > 0:
if not isinstance(entry, dict):
raise TypeError("Entry should be a `dict` type.")
entry['type'] = typeinfo
self._loginfo.append(entry)
return
[docs] def _loginfo_print(self, pretty=True) -> None:
"""
This is a helper to pretty-print the ``_loginfo`` object.
Todo:
Once we can use Python 3.8 in our environments, we can use the @final decorator
to mark this as something that should not be overridden. We also have to
import it: `from typing import final`.
https://stackoverflow.com/questions/321024/making-functions-non-override-able
"""
if pretty:
self.debug_message(1, "Loginfo:")
len_max_type = 0
len_max_key = 0
for entry in self._loginfo:
len_max_type = max(len_max_type, len(entry['type']))
for k, v in entry.items():
len_max_key = max(len_max_key, len(k))
special_keys = ["type", "name"]
for entry in self._loginfo:
for key in special_keys:
if key in entry.keys():
line = ("--> " if key != special_keys[0] else " ")
line += "{} : {} ".format(key.ljust(len_max_key), entry[key])
self.debug_message(1, line)
for k, v in entry.items():
if k not in special_keys:
line = "--> {} : {}".format(k.ljust(len_max_key), v)
self.debug_message(1, line)
self.debug_message(1, "")
else:
print(self._loginfo)
return
def _validate_parameter(self, parameter, type_restriction, exception_class="CATASTROPHIC") -> int:
"""Parameter validation with ExcpetionControl event on failure.
Returns:
int: 0 if typecheck succeeds. 1 if typecheck fails and the
``exception_control_level`` prevented the exception from
being raised.
Raises:
TypeError: If the typecheck fails.
"""
output = 0
# It's pretty common for someone to enter (int, None) to isinstance()
# which is wrong, `None` isn't a type.
if not isinstance(type_restriction, tuple):
type_restriction = tuple([type_restriction])
type_restriction = tuple([x if x != None else type(x) for x in type_restriction])
if not isinstance(parameter, type_restriction):
output = 1
self.exception_control_event(
exception_class, TypeError, f"`{parameter}` must be a `{type_restriction}` type."
)
return output
# ===========================================================
# I N N E R C L A S S ( E S )
# ===========================================================
[docs] class ConfigParserEnhancedData(Debuggable, ExceptionControl):
"""ConfigParserEnhancedData
This class is intended to serve as a *lite* analog to
:class:`ConfigParser` to provide a similar result but with
the :class:`~ConfigParserEnhanced` class's ability to parse ``.ini`` files and
follow entries that implement a ``use <section name>`` rule. In
this case, when a section parses through, we will return sections with
all options that *were not* handled by some handler.
For example, if we have this ``.ini`` file:
.. code-block:: ini
[SEC A]
opt1: value1-A
opt2: value2-A
[SEC B]
opt1: value1-B
use 'SEC A'
[SEC C]
use 'SEC A'
opt1: value1-C
and we pull the ``SEC B`` data from it using, say, ``options("SEC B")``,
the result we should expect is:
.. code-block:: ini
[SEC B]
opt1: value1-A
opt2: value2-A
but if we loaded ``options("SEC C")`` then we should get:
.. code-block:: ini
[SEC C]
opt1: value1-C
opt2: value2-A
Since the recursion of the ``use`` operations is a depth-first search, when there are
entries with the same keys, then the 'last one visited' will win.
When we parse a particular section, the result for a given
section name is the union of all options in the transitive closure of the
directed acyclic graph generated by the ``use`` operations. For example:
.. code-block:: ini
[SEC A]
use 'SEC B'
opt1: value1-A
opt2: value2-A
[SEC B]
use 'SEC A'
opt1: value1-B
The results of ``options('SEC A')`` and ``options('SEC B)`` will be different:
>>> options("SEC A")
[SEC A]
opt1: value1-A
opt2: value2-A
>>> options("SEC B")
[SEC B]
opt1: value1-B
opt2: value1-A
Note:
This *MUST* be an inner class of :class:`~ConfigParserEnhanced` because it
contains a 'hook' back to the instance of :class:`~ConfigParserEnhanced` in
in which this entry exists. This allows us to access the owner's
state so we can implement our lazy-evaluation and caching schemes. When
an intance of :class:`ConfigParserEnhanced` accesses a section via the ``configparserenhanceddata``
property, the parser will be invoked on this section to generate the result.
"""
def __init__(self, owner=None):
self._owner = owner
self._set_owner_options()
def __repr__(self):
repr_entries = ["owner={}".format(self._owner), "data={}".format(self.data)]
return "ConfigParserEnhancedData({})".format(", ".join(repr_entries))
@property
def data(self) -> dict:
"""
"""
if not hasattr(self, '_data'):
self._data = {}
return self._data
@data.setter
def data(self, value) -> dict:
"""
"""
if not isinstance(value, dict):
raise TypeError("data must be a `dict` type.")
self._data = value
return self._data
[docs] def items(self, section=None):
"""Iterator over all sections and their values in the ``.ini`` file.
"""
section_list = self.keys()
output = None
if section is None:
for sec_i in section_list:
self._parse_owner_section(sec_i)
output = self.data.items()
else:
output = self.options(section).items()
return output
[docs] def keys(self):
"""
Returns an iterable of sections in the ``.ini`` file.
Returns:
iterable object: containing the sections in the ``.ini`` file.
"""
section_list = self.data.keys()
if self._owner != None:
section_list = self._owner.configparserdata.sections()
return section_list
def __iter__(self):
for k in self.keys():
yield k
def __getitem__(self, key):
if not self.has_section(key):
raise KeyError(key)
return self.data[key]
def __len__(self) -> int:
"""
Returns the # of sections in the ``.ini`` file.
This will always be the total # of sections in the file as detected
by ``ConfigParser``. We may or may not have them parsed at this time
but accessing a given section via the ``[]`` or other means will cause
it to be parsed at that time if it hasn't been yet.
Returns:
int: The number of sections in the .ini file.
"""
return len(self.keys())
[docs] def sections(self, parse=False):
"""
Returns an iterable of sections in the ``.ini`` file.
Args:
parse (str,bool): Determines whether or not we will parse the sections
as well. This can be a string or a boolean type.
- ``False``: (default): Only returns the section names.
- ``True`` : Return the section names but also execute a parse of the sections.
- "force" : Same as ``True`` but we *force* a (re)parse of all sections
even if they've already been parsed before.
Returns:
iterable object: containing the sections in the ``.ini`` file.
"""
force_parse = False
# Check the parameters.
if not isinstance(parse, (bool, str)):
raise TypeError("parse option must be a bool or str type.")
if isinstance(parse, (str)):
parse = parse.lower()
force_parse = parse == "force"
if parse != "force":
self.exception_control_event(
"CATASTROPHIC",
ValueError,
"`parse` should be a bool or a"
"string that is set to 'force'"
)
if parse:
for section in self.keys():
self._parse_owner_section(section, force_parse)
return self.keys()
[docs] def has_section(self, section) -> bool:
"""Checks if the section exists in the data.
Returns:
boolean: True if the section exists, False if otherwise.
"""
if self._owner != None:
# If this section exists...
if self._owner.configparserdata.has_section(section):
# if we haven't already checked it then parse it.
if section not in self._sections_checked:
try:
self._parse_owner_section(section)
except KeyError: # pragma: no cover
# This might not be reachable.
self.exception_control_event(
"CATASTROPHIC",
KeyError,
"Reached 'unreachable' code? Please notify developers of this"
)
return self.has_section_no_parse(section)
[docs] def has_section_no_parse(self, section):
"""Check for existence of the section without parsing
This will return True if the section exists or False if
it does not. In this check, if the section does not exist
the owner parser will NOT be invoked.
Returns:
boolean: True if the section exists in the data and
False if it does not.
"""
return section in self.data.keys()
[docs] def options(self, section):
if not self.has_section(section):
raise KeyError("Section {} does not exist.".format(section))
return self.data[section]
[docs] def has_option(self, section, option) -> bool:
"""
"""
if self._owner != None:
self._parse_owner_section(section)
return (section in self.data.keys()) and (option in self.data[section].keys())
[docs] def get(self, section, option=None):
"""
Get a section/option pair, if it exists. If we have not
parsed the section yet, we should run the parser to
fully get the key data.
"""
if self._owner != None and section not in self._sections_checked:
self._parse_owner_section(section)
if self.has_section(section):
if option is None:
return self.data[section]
elif self.has_option(section, option):
return self.data[section][option]
else:
self.exception_control_event(
"CATASTROPHIC",
KeyError,
"Missing section:option -> '{}': '{}'".format(section, option)
)
# This is not reachable with a bad section name
# because the call to parse_owner_section(section) will
# raise a KeyError if the section name is bad, and
# the owner setter doesn't allow a NoneType to be assigned.
# But if someone assigned None to self._owner directly
# which Python won't prevent, we could get here... so
# this check helps prevent one from doing bad things.
raise KeyError("Missing section {}.".format(section))
[docs] def add_section(self, section, force=False):
"""Add a new empty section.
Adds a new empty section if an existing one does not
already exist. We can override this and force the new
section if we enable the ``force`` option.
Args:
section (str): The new section name.
force (boolean): If True then we will FORCE the
new section to be added regardless of whether
or not this will overwrite an existing section.
Returns:
dict: A dictionary containing the new section added.
"""
if (force) or (not self.has_section_no_parse(section)):
self.data[section] = {}
return self.data[section]
[docs] def set(self, section, option, value):
"""
Directly set an option. If the section is missing, we create an empty one.
"""
if not self.has_section(section):
self.add_section(section)
# Note: We overwrite the option, even if it's already there.
self.data[section][option] = value
return self.data[section][option]
# -------------------------------------
# H E L P E R S ( P R I V A T E )
# -------------------------------------
@property
def _owner(self):
if not hasattr(self, '_owner_data'):
self._owner_data = None
return self._owner_data
@_owner.setter
def _owner(self, value):
if not isinstance(value, (ConfigParserEnhanced)):
raise TypeError("Owner class must be a ConfigParserEnhanced or derivitive.")
self._owner_data = value
return self._owner_data
@property
def _sections_checked(self):
"""
Implements a set that contains section names that
have already been parsed via lazy evaluation.
"""
if not hasattr(self, '_sections_checked_data'):
self._sections_checked_data = set()
return self._sections_checked_data
def _set_owner_options(self):
"""
Get options from the owner class, if we have an owner class.
Todo: Test what happens if self._owner == None
"""
if self._owner != None:
self.exception_control_level = self._owner.exception_control_level
self.debug_level = self._owner.debug_level
return
def _parse_owner_section(self, section, force_parse=False):
"""Parse the section from the owner class.
The ``force_parse`` parameter determins if we will perform a parse of the
section even if it's already been parsed.
Args:
section (str): The section name of the section to parse.
force_parse (bool,str): Determins if we should parse the section or not.
Raises:
TypeError: If the ``force_parse`` option is not a ``bool`` or ``str`` type.
"""
# Check the parameters.
if not isinstance(force_parse, (bool)):
self.exception_control_event(
"CATASTROPHIC", TypeError, "`force_parse` must be a `bool` type."
)
if self._owner != None:
do_parse_section = section not in self._sections_checked
do_parse_section = do_parse_section or force_parse
if do_parse_section:
self._set_owner_options()
self._sections_checked.add(section)
self._owner.parse_section(section)
return