Skip to content

feat: check for deprecated setup.cfg settings #572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,9 @@ _readthedocs

# Default cookiecutter output
/package

# VSCode
/.vscode

# UV output
/uv.lock
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- [[[end]]] -->

[repo-review]: https://repo-review.readthedocs.io
Expand Down
51 changes: 25 additions & 26 deletions docs/pages/guides/packaging_classic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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/

<!-- prettier-ignore-end -->
9 changes: 7 additions & 2 deletions docs/pages/guides/packaging_simple.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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/

<!-- prettier-ignore-end -->

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ 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"
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"
Expand Down
53 changes: 53 additions & 0 deletions src/sp_repo_review/checks/setupcfg.py
Original file line number Diff line number Diff line change
@@ -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__()}
3 changes: 3 additions & 0 deletions src/sp_repo_review/families.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ def get_families(pyproject: dict[str, Any]) -> dict[str, Family]:
"docs": Family(
name="Documentation",
),
"setupcfg": Family(
name="Setuptools Config",
),
}
35 changes: 35 additions & 0 deletions tests/test_setupcfg.py
Original file line number Diff line number Diff line change
@@ -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 = [email protected]
""")
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 = [email protected]
""")
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