#!/usr/bin/env python3
#
# configvar.py
"""
Base class for ``YAML`` configuration values.
"""
#
# Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
#
# stdlib
from textwrap import dedent, indent
from typing import Any, Callable, Optional, Type, Union
# 3rd party
from domdf_python_tools.stringlist import StringList
from typing_inspect import is_literal_type # type: ignore[import]
# this package
from configconfig.metaclass import ConfigVarMeta
from configconfig.utils import RawConfigVarsType, get_yaml_type, tab
from configconfig.validator import Validator
__all__ = ["ConfigVar"]
[docs]class ConfigVar(metaclass=ConfigVarMeta):
"""
Base class for ``YAML`` configuration values.
The class docstring should be the description of the config var, with an example,
and the name of the class should be the variable name.
Alternatively, for a more Pythonic naming approach, the variable name can
be set with the ``name`` class variable.
.. latex:vspace:: -5px
:bold-title:`Example:`
.. code-block:: python
class platforms(ConfigVar):
\"\"\"
A case-insensitive list of platforms to perform tests for.
Example:
.. code-block:: yaml
platforms:
- Windows
- Linux
These values determine the GitHub test workflows to enable,
and the Trove classifiers used on PyPI.
\"\"\"
dtype = List[Literal["Windows", "macOS", "Linux"]]
default: List[str] = ["Windows", "macOS", "Linux"]
category: str = "packaging"
.. latex:vspace:: -10px
""" # noqa: D300,D301
dtype: Type
"""
The allowed type or types in the ``YAML`` configuration file.
"""
rtype: Type
"""
The variable type passed to Jinja2.
If ``None`` :attr:`~configconfig.configvar.ConfigVar.dtype` is used.
Ignored for ``dtype=bool``.
"""
required: bool
"""
Flag to indicate whether the configuration value is required. Default :py:obj:`False`.
"""
default: Union[Callable[[RawConfigVarsType], Any], Any]
"""
The default value of the configuration value if it is optional. Defaults to ``''`` if unset.
May also be set to a callable which returns a dynamic or mutable default value.
"""
category: str
"""
The category the :class:`~configconfig.configvar.ConfigVar` is listed under in the documentation.
"""
__name__: str
[docs] @classmethod
def validator(cls, value: Any) -> Any:
"""
Function to call to validate the values.
* The callable must have a single required argument (the value).
* Should raise :exc:`ValueError` if values are invalid, and return the values if they are valid.
* May change the values (e.g. make lowercase) before returning.
"""
return value
def __new__(cls, raw_config_vars: RawConfigVarsType) -> Any: # noqa: D102
# Exists purely so mypy knows about the signature
return cls.get(raw_config_vars) # pragma: no cover
[docs] @classmethod
def get(cls, raw_config_vars: Optional[RawConfigVarsType] = None) -> Any:
"""
Returns the value of this :class:`~configconfig.configvar.ConfigVar`.
:param raw_config_vars: Dictionary to obtain the value from.
:rtype: See the :attr:`~.ConfigVar.rtype` attribute.
"""
return cls.validator(cls.validate(raw_config_vars))
[docs] @classmethod
def validate(cls, raw_config_vars: Optional[RawConfigVarsType] = None) -> Any:
"""
Validate the value obtained from the ``YAML`` file and coerce into the appropriate return type.
:param raw_config_vars: Dictionary to obtain the value from.
:rtype: See the :attr:`~.ConfigVar.rtype` attribute.
"""
if raw_config_vars is None:
raw_config_vars = {}
if cls.rtype is None:
cls.rtype = cls.dtype
validator = Validator(cls)
return validator.validate(raw_config_vars)
[docs] @classmethod
def make_documentation(cls) -> str:
"""
Returns the reStructuredText documentation for the :class:`~.ConfigVar`.
"""
docstring = cls.__doc__ or ''
docstring = (indent(dedent(docstring), tab))
if not docstring.startswith('\n'):
docstring = '\n' + docstring
buf = StringList()
buf.indent_type = " "
buf.blankline(ensure_single=True)
buf.append(f".. conf:: {cls.__name__}")
buf.append(docstring)
buf.blankline()
buf.indent_size += 1
buf.append(f"**Required**: {'yes' if cls.required else 'no'}")
buf.blankline()
buf.blankline()
if not cls.required:
if cls.default == []:
buf.append("**Default**: [ ]")
elif cls.default == {}:
buf.append("**Default**: { }")
elif isinstance(cls.default, Callable): # type: ignore[arg-type]
buf.append(f"**Default**: The value of :conf:`{cls.default.__name__}`")
elif isinstance(cls.default, bool):
buf.append(f"**Default**: :py:obj:`{cls.default}`")
elif isinstance(cls.default, str):
if cls.default == '':
buf.append("**Default**: <blank>")
else:
buf.append(f"**Default**: ``{cls.default}``")
else:
buf.append(f"**Default**: {cls.default}")
buf.blankline()
buf.blankline()
buf.append(f"**Type**: {get_yaml_type(cls.dtype)}")
if is_literal_type(cls.dtype):
valid_values = ", ".join(f"``{x}``" for x in cls.dtype.__args__)
buf.blankline()
buf.blankline()
buf.append(f"**Allowed values**: {valid_values}")
elif hasattr(cls.dtype, "__args__") and is_literal_type(cls.dtype.__args__[0]):
valid_values = ", ".join(f"``{x}``" for x in cls.dtype.__args__[0].__args__)
buf.blankline()
buf.blankline()
buf.append(f"**Allowed values**: {valid_values}")
buf.indent_size -= 1
return str(buf)