Source code for whey.config.whey

#!/usr/bin/env python3
#
#  whey.py
"""
Parser for whey's own configuration.
"""
#
#  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, List, Set, Type, cast

# 3rd party
import dist_meta.entry_points
from dom_toml.parser import TOML_TYPES, AbstractConfigParser, BadConfigError
from natsort import natsorted
from shippinglabel.classifiers import validate_classifiers

# this package
from whey import additional_files
from whey.builder import AbstractBuilder, SDistBuilder, WheelBuilder

__all__ = (
		"WheyParser",
		"backfill_classifiers",
		"get_default_builders",
		"get_entry_points",
		"license_lookup",
		)

#: Mapping of license short codes to license names used in trove classifiers.
license_lookup = {
		"Apache-2.0": "Apache Software License",
		"BSD": "BSD License",
		"BSD-2-Clause": "BSD License",
		"BSD-3-Clause": "BSD License",
		"AGPL-3.0-only": "GNU Affero General Public License v3",
		"AGPL-3.0": "GNU Affero General Public License v3",
		"AGPL-3.0-or-later": "GNU Affero General Public License v3 or later (AGPLv3+)",
		"AGPL-3.0+": "GNU Affero General Public License v3 or later (AGPLv3+)",
		"FDL": "GNU Free Documentation License (FDL)",
		"GFDL-1.1-only": "GNU Free Documentation License (FDL)",
		"GFDL-1.1-or-later": "GNU Free Documentation License (FDL)",
		"GFDL-1.2-only": "GNU Free Documentation License (FDL)",
		"GFDL-1.2-or-later": "GNU Free Documentation License (FDL)",
		"GFDL-1.3-only": "GNU Free Documentation License (FDL)",
		"GFDL-1.3-or-later": "GNU Free Documentation License (FDL)",
		"GFDL-1.2": "GNU Free Documentation License (FDL)",
		"GFDL-1.1": "GNU Free Documentation License (FDL)",
		"GFDL-1.3": "GNU Free Documentation License (FDL)",
		"GPL": "GNU General Public License (GPL)",
		"GPL-1.0-only": "GNU General Public License (GPL)",
		"GPL-1.0-or-later": "GNU General Public License (GPL)",
		"GPLv2": "GNU General Public License v2 (GPLv2)",
		"GPL-2.0-only": "GNU General Public License v2 (GPLv2)",
		"GPLv2+": "GNU General Public License v2 or later (GPLv2+)",
		"GPL-2.0-or-later": "GNU General Public License v2 or later (GPLv2+)",
		"GPLv3": "GNU General Public License v3 (GPLv3)",
		"GPL-3.0-only": "GNU General Public License v3 (GPLv3)",
		"GPLv3+": "GNU General Public License v3 or later (GPLv3+)",
		"GPL-3.0-or-later": "GNU General Public License v3 or later (GPLv3+)",
		"LGPLv2": "GNU Lesser General Public License v2 (LGPLv2)",
		"LGPLv2+": "GNU Lesser General Public License v2 or later (LGPLv2+)",
		"LGPLv3": "GNU Lesser General Public License v3 (LGPLv3)",
		"LGPL-3.0-only": "GNU Lesser General Public License v3 (LGPLv3)",
		"LGPLv3+": "GNU Lesser General Public License v3 or later (LGPLv3+)",
		"LGPL-3.0-or-later": "GNU Lesser General Public License v3 or later (LGPLv3+)",
		"LGPL": "GNU Library or Lesser General Public License (LGPL)",
		"MIT": "MIT License",
		"PSF-2.0": "Python Software Foundation License",
		}


[docs]def get_default_builders() -> Dict[str, Type[AbstractBuilder]]: """ Returns a mapping of builder categories to builder classes to use as the default builders. """ return {"sdist": SDistBuilder, "binary": WheelBuilder, "wheel": WheelBuilder}
[docs]class WheyParser(AbstractConfigParser): """ Parser for the ``[tool.whey]`` table from ``pyproject.toml``. .. autosummary-widths:: 1/2 """ defaults = { "source-dir": '.', "license-key": None, "platforms": None, "python-versions": None, "python-implementations": None, } factories = { "additional-files": list, "base-classifiers": list, "builders": get_default_builders, }
[docs] def parse_package(self, config: Dict[str, TOML_TYPES]) -> str: """ Parse the ``package`` key, giving the name of the importable package. This defaults to :pep621:`project.name <name>` if unspecified. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ package = config["package"] self.assert_type(package, str, ["tool", "whey", "package"]) return package
[docs] def parse_source_dir(self, config: Dict[str, TOML_TYPES]) -> str: """ Parse the ``source-dir`` key, giving the name of the directory containing the project's source. This defaults to ``'.'`` if unspecified. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ source_dir = config["source-dir"] self.assert_type(source_dir, str, ["tool", "whey", "source-dir"]) return source_dir
[docs] def parse_license_key(self, config: Dict[str, TOML_TYPES]) -> str: """ Parse the ``license-key`` key, giving the identifier of the project's license. Optional. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ license_key = config["license-key"] self.assert_type(license_key, str, ["tool", "whey", "license-key"]) return license_key
[docs] def parse_additional_files(self, config: Dict[str, TOML_TYPES]) -> List[additional_files.AdditionalFilesEntry]: """ Parse the ``additional-files`` key, giving `MANIFEST.in`_-style entries for additional files to include in distributions. .. _MANIFEST.in: https://packaging.python.org/guides/using-manifest-in/ :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ # noqa: D400 entries = config["additional-files"] for idx, file in enumerate(entries): self.assert_indexed_type(file, str, ["tool", "whey", "additional-files"], idx=idx) parsed_additional_files = [] for entry in entries: parsed_entry = additional_files.from_entry(entry) if parsed_entry is not None: parsed_additional_files.append(parsed_entry) return parsed_additional_files
[docs] def parse_platforms(self, config: Dict[str, TOML_TYPES]) -> List[str]: """ Parse the ``platforms`` key, giving a list of supported platforms. Optional. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ platforms = config["platforms"] for idx, plat in enumerate(platforms): self.assert_indexed_type(plat, str, ["tool", "whey", "platforms"], idx=idx) return platforms
[docs] @staticmethod def parse_python_versions(config: Dict[str, TOML_TYPES]) -> List[str]: """ Parse the ``python-versions`` key, giving a list of supported Python versions. Optional. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ python_versions = config["python-versions"] for idx, version in enumerate(python_versions): if not isinstance(version, (str, int, float)): raise TypeError( f"Invalid type for 'tool.whey.python-versions[{idx}]': " f"expected {str!r}, {int!r} or {float!r}, got {type(version)!r}" ) if str(version)[0] in "12": raise BadConfigError( f"Invalid value for 'tool.whey.python-versions[{idx}]': whey only supports Python 3-only projects." ) return list(map(str, python_versions))
[docs] def parse_python_implementations(self, config: Dict[str, TOML_TYPES]) -> List[str]: """ Parse the ``python-implementations`` key, giving a list of supported Python implementations. Optional. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ python_implementations = config["python-implementations"] for idx, impl in enumerate(python_implementations): self.assert_indexed_type(impl, str, ["tool", "whey", "python-implementations"], idx=idx) return python_implementations
[docs] def parse_base_classifiers(self, config: Dict[str, TOML_TYPES]) -> Set[str]: """ Parse the ``base-classifiers`` key, giving a list `trove classifiers <https://pypi.org/classifiers/>`__. This list will be extended with the appropriate classifiers for supported platforms, Python versions and implementations, and the project's license. Ignored if :pep621:`classifiers` is not listed in :pep621:`dynamic` :param config: The unparsed TOML config for the ``[tool.whey]`` table. :rtype: .. latex:clearpage:: """ parsed_classifiers = set() for idx, classifier in enumerate(config["base-classifiers"]): self.assert_indexed_type(classifier, str, ["tool", "whey", "python-implementations"], idx=idx) parsed_classifiers.add(classifier) return parsed_classifiers
[docs] def parse_builders(self, config: Dict[str, TOML_TYPES]) -> Dict[str, Type[AbstractBuilder]]: """ Parse the ``builders`` table, which lists gives the entry points to use for the sdist and wheel builders. This allows the user to select a custom builder with additional functionality. :param config: The unparsed TOML config for the ``[tool.whey]`` table. """ parsed_builders = get_default_builders() builders = config["builders"] entry_points: Dict[str, dist_meta.entry_points.EntryPoint] = get_entry_points() self.assert_type(builders, dict, ["tool", "whey", "builders"]) for builder_type in ["binary", "sdist", "wheel"]: if builder_type in builders: entry_point_name = builders[builder_type] if entry_point_name not in entry_points: raise BadConfigError( f"Unknown {builder_type} builder {entry_point_name!r}. \n" f"Is it registered as an entry point under 'whey.builder'?" ) parsed_builders[builder_type] = cast(Type[AbstractBuilder], entry_points[entry_point_name].load()) return parsed_builders
@property def keys(self) -> List[str]: """ The keys to parse from the TOML file. """ return [ "package", "source-dir", "additional-files", "license-key", "base-classifiers", "platforms", "python-versions", "python-implementations", "builders", ]
[docs]def backfill_classifiers(config: Dict[str, TOML_TYPES]) -> List[str]: """ Backfill `trove classifiers <https://pypi.org/classifiers/>`_ for supported platforms, Python versions and implementations, and the project's license, as appropriate. :param config: The parsed config from ``pyproject.toml``. """ # noqa: D400 # TODO: Typing :: Typed parsed_classifiers = set(config["base-classifiers"]) platforms = config["platforms"] license_key = config["license-key"] python_versions = config["python-versions"] python_implementations = config["python-implementations"] if license_key in license_lookup: parsed_classifiers.add(f"License :: OSI Approved :: {license_lookup[license_key]}") if platforms: if set(platforms) == {"Windows", "macOS", "Linux"}: parsed_classifiers.add("Operating System :: OS Independent") else: if "Windows" in platforms: parsed_classifiers.add("Operating System :: Microsoft :: Windows") if "Linux" in platforms: parsed_classifiers.add("Operating System :: POSIX :: Linux") if "macOS" in platforms: parsed_classifiers.add("Operating System :: MacOS") if python_versions: for version in python_versions: parsed_classifiers.add(f"Programming Language :: Python :: {version}") parsed_classifiers.add("Programming Language :: Python :: 3 :: Only") if python_implementations: for implementation in python_implementations: parsed_classifiers.add(f"Programming Language :: Python :: Implementation :: {implementation}") parsed_classifiers.add("Programming Language :: Python") validate_classifiers(parsed_classifiers) return natsorted(parsed_classifiers)
[docs]def get_entry_points(group: str = "whey.builder") -> Dict[str, dist_meta.entry_points.EntryPoint]: r""" Returns an iterable over `EntryPoint`_ objects in the ``group`` group. :param group: :rtype: :class:`Iterable <typing.Iterable>`\[`EntryPoint`_\] .. _EntryPoint: https://docs.python.org/3/library/importlib.metadata.html#entry-points """ eps = dist_meta.entry_points.get_entry_points(group) entry_points: Dict[str, dist_meta.entry_points.EntryPoint] = {} for entry_point in eps: # pylint: disable=use-dict-comprehension if entry_point.group == group: entry_points[entry_point.name] = entry_point return entry_points