#!/usr/bin/env python3
#
# pep621.py
"""
:pep:`621` configuration parser.
"""
#
# Copyright © 2021 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 typing import Dict, cast
# 3rd party
import pyproject_parser.parsers
from dom_toml.parser import TOML_TYPES, BadConfigError
from domdf_python_tools.words import Plural, word_join
from pyproject_parser.type_hints import ProjectDict
__all__ = ("PEP621Parser", )
_field = Plural("field", "fields")
[docs]class PEP621Parser(pyproject_parser.parsers.PEP621Parser, inherit_defaults=True):
"""
Parser for :pep:`621` metadata from ``pyproject.toml``.
"""
defaults = {
"description": None,
"readme": None,
"requires-python": None,
"license": None,
}
def _parse(
self,
config: Dict[str, TOML_TYPES],
set_defaults: bool = False,
) -> ProjectDict:
dynamic_fields = config.get("dynamic", [])
parsed_config = {"dynamic": dynamic_fields}
for key in self.keys:
if key in config and key in dynamic_fields:
raise BadConfigError(f"{key!r} was listed in 'project.dynamic' but a value was given.")
elif key not in config:
# Ignore absent values
pass
elif hasattr(self, f"parse_{key.replace('-', '_')}"):
parsed_config[key] = getattr(self, f"parse_{key.replace('-', '_')}")(config)
elif key in config:
parsed_config[key] = config[key]
if set_defaults:
for key, value in self.defaults.items():
parsed_config.setdefault(key, value)
for key, factory in self.factories.items():
value = factory()
parsed_config.setdefault(key, value)
return cast(ProjectDict, parsed_config)
[docs] def parse( # type: ignore[override]
self,
config: Dict[str, TOML_TYPES],
set_defaults: bool = False,
) -> ProjectDict:
"""
Parse the TOML configuration.
:param config:
:param set_defaults: If :py:obj:`True`, the values in
:attr:`dom_toml.parser.AbstractConfigParser.defaults` and
:attr:`dom_toml.parser.AbstractConfigParser.factories`
will be set as defaults for the returned mapping.
"""
dynamic_fields = set(config.get("dynamic", []))
if "name" in dynamic_fields:
raise BadConfigError("The 'project.name' field may not be dynamic.")
elif "name" not in config:
raise BadConfigError("The 'project.name' field must be provided.")
if dynamic_fields:
# TODO: Support the remaining fields as dynamic
# TODO: dynamic version numbers by parsing AST for __version__ in __init__.py
supported_dynamic = {"classifiers", "requires-python", "dependencies"}
unsupported_fields = dynamic_fields - supported_dynamic
if unsupported_fields:
supported = word_join(sorted(supported_dynamic), oxford=True, use_repr=True)
unsupported = word_join(sorted(unsupported_fields), oxford=True, use_repr=True)
raise BadConfigError(
f"Unsupported dynamic {_field(len(unsupported_fields))} {unsupported}.\n"
f"note: whey only supports {supported} as dynamic fields."
)
if "version" not in config:
raise BadConfigError("The 'project.version' field must be provided.")
return self._parse(config, set_defaults)