Skip to content

Commit

Permalink
Automatically convert setup.cfg => pyproject.toml
Browse files Browse the repository at this point in the history
This is the initial implementation of the "configuration driver"
that indirectly reads `setup.cfg` by first converting it to a data
structure corresponding to `pyproject.toml` and then expanding it.

This idea is based on the approach defined in pypa#2685.

LIMITATION: Differently from the `legacy_setupcfg` "configuration driver",
`setupcfg` does not support reading other distutils file.
The `find_others` flag is removed because of that.
  • Loading branch information
abravalheri committed Dec 11, 2021
1 parent ce80181 commit a846e5c
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 1 deletion.
5 changes: 4 additions & 1 deletion setuptools/config/pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand Down
67 changes: 67 additions & 0 deletions setuptools/config/setupcfg.py
Original file line number Diff line number Diff line change
@@ -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)
93 changes: 93 additions & 0 deletions setuptools/tests/config/test_setupcfg.py
Original file line number Diff line number Diff line change
@@ -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"}

0 comments on commit a846e5c

Please sign in to comment.