diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 12898de73df..18bf31c49eb 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -7,7 +7,7 @@ from setuptools.extern import tomli from setuptools.extern._validate_pyproject import validate from setuptools.config import expand as _expand -from setuptools.errors import OptionError +from setuptools.errors import OptionError, FileError def read_configuration(filepath, expand=True, ignore_option_errors=False): @@ -28,6 +28,9 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False): """ filepath = os.path.abspath(filepath) + if not os.path.isfile(filepath): + raise FileError(f"Configuration file {filepath!r} does not exist.") + with open(filepath, "rb") as file: asdict = tomli.load(file) diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py new file mode 100644 index 00000000000..ce87843b16b --- /dev/null +++ b/setuptools/config/setupcfg.py @@ -0,0 +1,67 @@ +"""Automatically convert ``setup.cfg`` file into a ``pyproject.toml``-equivalent +in memory data structure, and then proceed to load the configuration. +""" +import os +from typing import Union + +from setuptools.errors import FileError +from setuptools.extern.ini2toml.base_translator import BaseTranslator +from setuptools.extern.ini2toml.drivers import configparser as configparser_driver +from setuptools.extern.ini2toml.drivers import plain_builtins as plain_builtins_driver +from setuptools.extern.ini2toml.plugins import setuptools_pep621 as setuptools_plugin + +from setuptools.config import pyprojecttoml as pyproject_config + + +_Path = Union[os.PathLike, str, None] + + +def convert(setupcfg_file: _Path) -> dict: + """Convert the ``setup.cfg`` file into a data struct similar to + the one that would be obtained by parsing a ``pyproject.toml`` + """ + with open(setupcfg_file, "r") as f: + ini_text = f.read() + + translator = BaseTranslator( + ini_loads_fn=configparser_driver.parse, + toml_dumps_fn=plain_builtins_driver.convert, + plugins=[setuptools_plugin.activate], + ini_parser_opts={}, + ) + return translator.translate(ini_text, profile_name="setup.cfg") + + +expand_configuration = pyproject_config.expand_configuration + + +def read_configuration( + filepath: _Path, expand: bool = True, ignore_option_errors: bool = False +): + """Read given configuration file and returns options from it as a dict. + + :param str|unicode filepath: Path to configuration file to get options from. + + :param bool expand: Whether to expand directives and other computed values + (i.e. post-process the given configuration) + + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. + + :rtype: dict + """ + filepath = os.path.abspath(filepath) + + if not os.path.isfile(filepath): + raise FileError(f"Configuration file {filepath!r} does not exist.") + + asdict = convert(filepath) + + with pyproject_config._ignore_errors(ignore_option_errors): + pyproject_config.validate(asdict) + + if expand: + root_dir = os.path.dirname(filepath) + return expand_configuration(asdict, root_dir, ignore_option_errors) diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py new file mode 100644 index 00000000000..4e10f834ff2 --- /dev/null +++ b/setuptools/tests/config/test_setupcfg.py @@ -0,0 +1,93 @@ +from textwrap import dedent + +from setuptools.config.setupcfg import convert, read_configuration + +EXAMPLE = { + "LICENSE": "----- MIT LICENSE TEXT PLACEHOLDER ----", + "README.md": "hello world", + "pyproject.toml": dedent("""\ + [build-system] + requires = ["setuptools>=42", "wheel"] + build-backend = "setuptools.build_meta" + """), + "setup.cfg": dedent("""\ + [metadata] + name = example-pkg + version = 0.0.1 + author = Example Author + author_email = author@example.com + description = A small example package + long_description = file: README.md + long_description_content_type = text/markdown + url = https://github.com/pypa/sampleproject + project_urls = + Bug Tracker = https://github.com/pypa/sampleproject/issues + classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + Operating System :: OS Independent + + [options] + package_dir = + = src + packages = find: + python_requires = >=3.6 + install_requires = + peppercorn + entry_points = file: entry_points.ini + + [options.extras_require] + dev = + check-manifest + test = + coverage + + [options.packages.find] + where = src + """), + "entry_points.ini": dedent("""\ + [my.plugin.group] + add_one = example_package.example:add_one + """), + "src/example_package/__init__.py": "", + "src/example_package/example.py": "def add_one(number):\n return number + 1", + "src/example_package/package_data.csv": "42", + "src/example_package/nested/__init__.py": "", +} + + +def create_project(parent_dir, files): + for file, content in files.items(): + path = parent_dir / file + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(content) + + +def test_convert(tmp_path): + create_project(tmp_path, EXAMPLE) + pyproject = convert(tmp_path / "setup.cfg") + project = pyproject["project"] + assert project["name"] == "example-pkg" + assert project["version"] == "0.0.1" + assert project["readme"]["file"] == "README.md" + assert project["readme"]["content-type"] == "text/markdown" + assert project["urls"]["Homepage"] == "https://github.com/pypa/sampleproject" + assert set(project["dependencies"]) == {"peppercorn"} + assert set(project["optional-dependencies"]["dev"]) == {"check-manifest"} + assert set(project["optional-dependencies"]["test"]) == {"coverage"} + setuptools = pyproject["tool"]["setuptools"] + from pprint import pprint + pprint(setuptools) + assert set(setuptools["dynamic"]["entry-points"]["file"]) == {"entry_points.ini"} + assert setuptools["packages"]["find"]["where"] == ["src"] + assert setuptools["packages"]["find"]["namespaces"] is False + + +def test_read_configuration(tmp_path): + create_project(tmp_path, EXAMPLE) + pyproject = read_configuration(tmp_path / "setup.cfg") + project = pyproject["project"] + ep_value = "example_package.example:add_one" + assert project["entry-points"]["my.plugin.group"]["add_one"] == ep_value + setuptools = pyproject["tool"]["setuptools"] + assert set(setuptools["packages"]) == {"example_package", "example_package.nested"}