Skip to content
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
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ include CHANGELOG.md

recursive-include testing *.bash
prune nextgen
prune .cursor

recursive-include docs *.md
include docs/examples/version_scheme_code/*.py
include docs/examples/version_scheme_code/*.toml
include mkdocs.yml
include uv.lock
7 changes: 2 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,8 @@ dependencies = [
'typing-extensions; python_version < "3.10"',
]
[project.optional-dependencies]
rich = [
"rich",
]
toml = [
]
rich = ["rich"]
toml = []

[dependency-groups]
docs = [
Expand Down
24 changes: 6 additions & 18 deletions src/setuptools_scm/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,24 +291,12 @@ def from_file(
- **kwargs: additional keyword arguments to pass to the Configuration constructor
"""

try:
if pyproject_data is None:
pyproject_data = _read_pyproject(
Path(name), missing_section_ok=missing_section_ok
)
except FileNotFoundError:
if missing_file_ok:
log.warning("File %s not found, using empty configuration", name)
pyproject_data = PyProjectData(
path=Path(name),
tool_name="setuptools_scm",
project={},
section={},
is_required=False,
section_present=False,
)
else:
raise
if pyproject_data is None:
pyproject_data = _read_pyproject(
Path(name),
missing_section_ok=missing_section_ok,
missing_file_ok=missing_file_ok,
)
args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)

args.update(read_toml_overrides(args["dist_name"]))
Expand Down
75 changes: 58 additions & 17 deletions src/setuptools_scm/_integration/pyproject_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import warnings

from dataclasses import dataclass
from pathlib import Path
from typing import NamedTuple
from typing import Sequence

from .. import _log
from .setuptools import read_dist_name_from_setup_cfg
from .._requirement_cls import extract_package_name
from .toml import TOML_RESULT
from .toml import read_toml_content

Expand All @@ -16,13 +16,39 @@
_ROOT = "root"


class PyProjectData(NamedTuple):
@dataclass
class PyProjectData:
path: Path
tool_name: str
project: TOML_RESULT
section: TOML_RESULT
is_required: bool
section_present: bool
project_present: bool

@classmethod
def for_testing(
cls,
is_required: bool = False,
section_present: bool = False,
project_present: bool = False,
project_name: str | None = None,
) -> PyProjectData:
"""Create a PyProjectData instance for testing purposes."""
if project_name is not None:
project = {"name": project_name}
assert project_present
else:
project = {}
return cls(
path=Path("pyproject.toml"),
tool_name="setuptools_scm",
project=project,
section={},
is_required=is_required,
section_present=section_present,
project_present=project_present,
)

@property
def project_name(self) -> str | None:
Expand All @@ -33,6 +59,10 @@ def verify_dynamic_version_when_required(self) -> None:
if self.is_required and not self.section_present:
# When setuptools-scm is in build-system.requires but no tool section exists,
# we need to verify that dynamic=['version'] is set in the project section
# But only if there's actually a project section
if not self.project_present:
# No project section, so don't auto-activate setuptools_scm
return
dynamic = self.project.get("dynamic", [])
if "version" not in dynamic:
raise ValueError(
Expand All @@ -43,29 +73,41 @@ def verify_dynamic_version_when_required(self) -> None:


def has_build_package(
requires: Sequence[str], build_package_names: Sequence[str]
requires: Sequence[str], canonical_build_package_name: str
) -> bool:
for requirement in requires:
import re

# Remove extras like [toml] first
clean_req = re.sub(r"\[.*?\]", "", requirement)
# Split on version operators and take first part
package_name = re.split(r"[><=!~]", clean_req)[0].strip().lower()
if package_name in build_package_names:
package_name = extract_package_name(requirement)
if package_name == canonical_build_package_name:
return True
return False


def read_pyproject(
path: Path = Path("pyproject.toml"),
tool_name: str = "setuptools_scm",
build_package_names: Sequence[str] = ("setuptools_scm", "setuptools-scm"),
canonical_build_package_name: str = "setuptools-scm",
missing_section_ok: bool = False,
missing_file_ok: bool = False,
) -> PyProjectData:
defn = read_toml_content(path)
try:
defn = read_toml_content(path)
except FileNotFoundError:
if missing_file_ok:
log.warning("File %s not found, using empty configuration", path)
return PyProjectData(
path=path,
tool_name=tool_name,
project={},
section={},
is_required=False,
section_present=False,
project_present=False,
)
else:
raise

requires: list[str] = defn.get("build-system", {}).get("requires", [])
is_required = has_build_package(requires, build_package_names)
is_required = has_build_package(requires, canonical_build_package_name)

try:
section = defn.get("tool", {})[tool_name]
Expand All @@ -87,8 +129,9 @@ def read_pyproject(
section_present = False

project = defn.get("project", {})
project_present = "project" in defn
pyproject_data = PyProjectData(
path, tool_name, project, section, is_required, section_present
path, tool_name, project, section, is_required, section_present, project_present
)

# Verify dynamic version when setuptools-scm is used as build dependency indicator
Expand Down Expand Up @@ -121,8 +164,6 @@ def get_args_for_pyproject(
if dist_name is None:
# minimal pep 621 support for figuring the pretend keys
dist_name = pyproject.project_name
if dist_name is None:
dist_name = read_dist_name_from_setup_cfg()
if _ROOT in kwargs:
if kwargs[_ROOT] is None:
kwargs.pop(_ROOT, None)
Expand Down
21 changes: 21 additions & 0 deletions src/setuptools_scm/_integration/setup_cfg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

import os

import setuptools


def read_dist_name_from_setup_cfg(
input: str | os.PathLike[str] = "setup.cfg",
) -> str | None:
# minimal effort to read dist_name off setup.cfg metadata
import configparser

parser = configparser.ConfigParser()
parser.read([input], encoding="utf-8")
dist_name = parser.get("metadata", "name", fallback=None)
return dist_name


def _dist_name_from_legacy(dist: setuptools.Distribution) -> str | None:
return dist.metadata.name or read_dist_name_from_setup_cfg()
Loading
Loading