# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Configuration file formats and their stateless content parsers.
Holds the :class:`ConfigFormat` enum, the optional third-party parser probes
that decide which formats are enabled, and :func:`parse_content`, the stateless
dispatch used by :class:`~click_extra.config.option.ConfigOption` for every format that
does not need the CLI parameter structure.
"""
from __future__ import annotations
import importlib.util
import json
import logging
import sys
from enum import Enum
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib # type: ignore[import-not-found]
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
logger = logging.getLogger(__name__)
_OPTIONAL_PARSERS: tuple[tuple[str, str, str], ...] = (
# (import module name, click-extra[extra] name, display label).
("yaml", "yaml", "YAML"),
("json5", "json5", "JSON5"),
("jsonc", "jsonc", "JSONC"),
("hjson", "hjson", "Hjson"),
("xmltodict", "xml", "XML"),
)
"""Third-party parsers each gating one optional configuration format.
Each entry pairs the importable module name (probed without importing it) with
the ``click-extra[extra]`` install target and the human-readable format label
used in the disabled-support debug message."""
PARSER_SUPPORT: dict[str, bool] = {}
"""Availability of each optional parser, keyed by ``click-extra[extra]`` name.
Populated once at import time by probing each module in ``_OPTIONAL_PARSERS``
with :func:`importlib.util.find_spec`. Read by :class:`ConfigFormat` to mark the
matching format as enabled or disabled. The probe does not import the module, so
the actual parser is loaded lazily by :func:`parse_content` only when used."""
for _module_name, _extra, _label in _OPTIONAL_PARSERS:
PARSER_SUPPORT[_extra] = importlib.util.find_spec(_module_name) is not None
if not PARSER_SUPPORT[_extra]:
logger.debug(
f"{_label} support disabled: install click-extra[{_extra}] to enable it."
)
[docs]
def parse_content(fmt: ConfigFormat, content: str) -> Any:
"""Parse content with a single stateless format.
INI is excluded: it needs the CLI parameter structure for type
coercion and is handled by ConfigOption.load_ini_config.
.. note::
Optional third-party parsers are imported lazily, at the point of use,
rather than at module load. Only enabled formats reach this function
(disabled ones are filtered out of ``ConfigOption.file_format_patterns``),
so the import always resolves for the formats actually parsed here.
"""
match fmt:
case ConfigFormat.TOML:
return tomllib.loads(content)
case ConfigFormat.YAML:
import yaml
return yaml.full_load(content)
case ConfigFormat.JSON:
return json.loads(content)
case ConfigFormat.JSON5:
import json5
return json5.loads(content)
case ConfigFormat.JSONC:
import jsonc
return jsonc.loads(content)
case ConfigFormat.HJSON:
import hjson
return hjson.loads(content)
case ConfigFormat.XML:
import xmltodict
return xmltodict.parse(content)
case ConfigFormat.PYPROJECT_TOML:
return tomllib.loads(content).get("tool", {})
raise ValueError(f"{fmt!r} is not handled by parse_content().")