diff --git a/.gitignore b/.gitignore index e8e22b14..a0746427 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,9 @@ _readthedocs # Default cookiecutter output /package + +# VSCode +/.vscode + +# UV output +/uv.lock diff --git a/README.md b/README.md index 269f53fd..f4b0b67a 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,9 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - `RF201`: Avoid using deprecated config settings - `RF202`: Use (new) lint config section +### Setuptools Config +- [`SCFG001`](https://learn.scientific-python.org/development/guides/packaging-classic#SCFG001): Avoid deprecated setup.cfg names + [repo-review]: https://repo-review.readthedocs.io diff --git a/docs/pages/guides/packaging_classic.md b/docs/pages/guides/packaging_classic.md index 8f868969..e62d739c 100644 --- a/docs/pages/guides/packaging_classic.md +++ b/docs/pages/guides/packaging_classic.md @@ -15,29 +15,27 @@ packaging styles, but this document is intended to outline a recommended style that existing packages should slowly adopt. The reasoning for each decision is outlined as well. -There are several popular packaging systems. This guide covers [Setuptools][], -which is the oldest system and supports compiled extensions. If you are not -working on legacy code or are willing to make a larger change, other systems -like [Hatch][] are drastically simpler - most of this page is unneeded for those -systems. Even setuptools supports modern config now, though setup.py is still -also required for compiled packages to be supported. - -Also see the [Python packaging guide][], especially the [Python packaging -tutorial][]. +There are several popular packaging systems. This guide covers the old +configuration style for [Setuptools][]. Unless you really need it, you should be +using the modern style described in +[Simple Packaging](/guides/packaging-simple/). The modern style is guided by +Python Enhancement Proposals (PEPs), and is more stable than the +setuptools-specific mechanisms that evolve over the years. This page is kept to +help users with legacy code (and hopefully upgrade it). {: .note } -> Raw source lives in git and has a `setup.py`. You _can_ install directly from -> git via pip, but normally users install from distributions hosted on PyPI. -> There are three options: **A)** A source package, called an SDist and has a -> name that ends in `.tar.gz`. This is a copy of the GitHub repository, stripped -> of a few specifics like CI files, and possibly with submodules included (if -> there are any). **B)** A pure python wheel, which ends in `.whl`; this is only -> possible if there are no compiled extensions in the library. This does _not_ -> contain a setup.py, but rather a `PKG_INFO` file that is rendered from -> setup.py (or from another build system). **C)** If not pure Python, a -> collection of wheels for every binary platform, generally one per supported -> Python version and OS as well. +> Raw source lives in git and has a `pyproject.toml` and/or a `setup.py`. You +> _can_ install directly from git via pip, but normally users install from +> distributions hosted on PyPI. There are three options: **A)** A source +> package, called an SDist and has a name that ends in `.tar.gz`. This is a copy +> of the GitHub repository, stripped of a few specifics like CI files, and +> possibly with submodules included (if there are any). **B)** A pure python +> wheel, which ends in `.whl`; this is only possible if there are no compiled +> extensions in the library. This does _not_ contain a setup.py, but rather a +> `PKG_INFO` file that is rendered from setup.py (or from another build system). +> **C)** If not pure Python, a collection of wheels for every binary platform, +> generally one per supported Python version and OS as well. > > Developer requirements (users of A or git) are generally higher than the > requirements to use B or C. Poetry and optionally flit create SDists that @@ -87,8 +85,8 @@ these "[hypermodern][]" packaging tools is growing in scientific Python packages. All tools build the same wheels (and they often build setuptools compliant SDists, as well). -{% rr PP003 %} Note that `"wheel"` is never required; it is injected -automatically by setuptools only when needed. +{% rr PP003 %} Note that `"wheel"` is never required; it was injected +automatically by setuptools in older versions, and is no longer used at all. ### Special additions: NumPy @@ -326,9 +324,12 @@ where = src # extern ``` +{% rr SCFG001 %} Note that all keys use underscores; using a dash will cause +warnings and eventually failures. + And, a possible `setup.py`; though in recent versions of pip, there no longer is -a need to include a legacy `setup.py` file, even for editable installs, unless -you are building extensions. +a need to include a legacy `setup.py` file, even for editable installs or +building extensions. ```python #!/usr/bin/env python @@ -479,7 +480,5 @@ the app. [manifest.in]: https://packaging.python.org/guides/using-manifest-in/ [setuptools]: https://setuptools.readthedocs.io/en/latest/userguide/index.html [setuptools cfg]: https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html -[python packaging guide]: https://packaging.python.org -[python packaging tutorial]: https://packaging.python.org/tutorials/packaging-projects/ diff --git a/docs/pages/guides/packaging_simple.md b/docs/pages/guides/packaging_simple.md index dd7493ba..ae2292ba 100644 --- a/docs/pages/guides/packaging_simple.md +++ b/docs/pages/guides/packaging_simple.md @@ -16,8 +16,11 @@ much; they all use a [standard configuration language][metadata] introduced in [PEP 621][]. The PyPA's Flit is a great option. [scikit-build-core][] and [meson-python][] are being developed to support this sort of configuration, enabling binary extension packages to benefit too. These [PEP 621][] tools -currently include [Hatch][], [PDM][], [Flit][], and [Setuptools][]. [Poetry][] -will eventually gain support in 2.0. +currently include [Hatch][], [PDM][], [Flit][], [Setuptools][], [Poetry][] 2.0, +and compiled backends (see the next page). + +Also see the [Python packaging guide][], especially the [Python packaging +tutorial][]. {: .note-title } @@ -204,6 +207,8 @@ This is tool specific. [pep 621]: https://www.python.org/dev/peps/pep-0621 [scikit-build-core]: https://scikit-build-core.readthedocs.io [meson-python]: https://meson-python.readthedocs.io +[python packaging guide]: https://packaging.python.org +[python packaging tutorial]: https://packaging.python.org/tutorials/packaging-projects/ diff --git a/pyproject.toml b/pyproject.toml index 8fa544ad..e0331c2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ ruff = "sp_repo_review.checks.ruff:repo_review_checks" mypy = "sp_repo_review.checks.mypy:repo_review_checks" github = "sp_repo_review.checks.github:repo_review_checks" readthedocs = "sp_repo_review.checks.readthedocs:repo_review_checks" +setupcfg = "sp_repo_review.checks.setupcfg:repo_review_checks" [project.entry-points."repo_review.fixtures"] workflows = "sp_repo_review.checks.github:workflows" @@ -76,6 +77,7 @@ dependabot = "sp_repo_review.checks.github:dependabot" precommit = "sp_repo_review.checks.precommit:precommit" readthedocs = "sp_repo_review.checks.readthedocs:readthedocs" ruff = "sp_repo_review.checks.ruff:ruff" +setupcfg = "sp_repo_review.checks.setupcfg:setupcfg" [project.entry-points."repo_review.families"] scikit-hep = "sp_repo_review.families:get_families" diff --git a/src/sp_repo_review/checks/setupcfg.py b/src/sp_repo_review/checks/setupcfg.py new file mode 100644 index 00000000..54dcc4b3 --- /dev/null +++ b/src/sp_repo_review/checks/setupcfg.py @@ -0,0 +1,53 @@ +# SCFG: setup.cfg +## SCFG0xx: setup.cfg checks + +from __future__ import annotations + +import configparser + +from .._compat.importlib.resources.abc import Traversable +from . import mk_url + + +def setupcfg(root: Traversable) -> configparser.ConfigParser | None: + setupcfg_path = root.joinpath("setup.cfg") + if setupcfg_path.is_file(): + config = configparser.ConfigParser() + with setupcfg_path.open("r") as f: + config.read_file(f) + return config + return None + + +class SCFG: + family = "setupcfg" + + +class SCFG001(SCFG): + "Avoid deprecated setup.cfg names" + + url = mk_url("packaging-classic") + + @staticmethod + def check(setupcfg: configparser.ConfigParser | None) -> str | None: + if setupcfg is None: + return None + invalid = [] + if setupcfg.has_section("metadata"): + invalid += [ + f"metadata.{x}" for x, _ in setupcfg.items("metadata") if "-" in x + ] + if setupcfg.has_section("options"): + invalid += [ + f"options.{x}" for x, _ in setupcfg.items("options") if "-" in x + ] + if invalid: + return ( + "Invalid setup.cfg options found, only underscores allowed: " + + ", ".join(invalid) + ) + return "" + + +def repo_review_checks() -> dict[str, SCFG]: + return {p.__name__: p() for p in SCFG.__subclasses__()} diff --git a/src/sp_repo_review/families.py b/src/sp_repo_review/families.py index 5fabb7c6..2e6dd1f1 100644 --- a/src/sp_repo_review/families.py +++ b/src/sp_repo_review/families.py @@ -51,4 +51,7 @@ def get_families(pyproject: dict[str, Any]) -> dict[str, Family]: "docs": Family( name="Documentation", ), + "setupcfg": Family( + name="Setuptools Config", + ), } diff --git a/tests/test_setupcfg.py b/tests/test_setupcfg.py new file mode 100644 index 00000000..853bb6a0 --- /dev/null +++ b/tests/test_setupcfg.py @@ -0,0 +1,35 @@ +import configparser + +from repo_review.testing import compute_check + + +def test_scfg001(): + setupcfg = configparser.ConfigParser() + setupcfg.read_string(""" + [metadata] + name = foo + version = 1.0 + description = A test package + author = Me + author_email = me@example.com + """) + assert compute_check("SCFG001", setupcfg=setupcfg).result + + +def test_scfg001_invalid(): + setupcfg = configparser.ConfigParser() + setupcfg.read_string(""" + [metadata] + name = foo + version = 1.0 + description = A test package + author = Me + author-email = me@example.com + """) + answer = compute_check("SCFG001", setupcfg=setupcfg) + assert not answer.result + assert "metadata.author-email" in answer.err_msg + + +def test_no_setupcfg(): + assert compute_check("SCFG001", setupcfg=None).result is None