From de453f8eb62074aaa50dfc213d2ba8fd9cafaec9 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 23 Apr 2025 14:29:53 +0200 Subject: [PATCH 1/6] Refactor DCDoc usage - Moved default text handling to a separate function - Prepare to read metadata fields from dataclasses.Field.metadata - Added a dataclass holder for the dataclass field metadata for documentation Signed-off-by: Cristian Le --- .../settings/documentation.py | 25 ++++++++++++++++--- .../settings/skbuild_model.py | 21 ++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py index cd6fc1cb8..87f85bb97 100644 --- a/src/scikit_build_core/settings/documentation.py +++ b/src/scikit_build_core/settings/documentation.py @@ -5,7 +5,7 @@ import inspect import textwrap from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar, cast from packaging.specifiers import SpecifierSet from packaging.version import Version @@ -15,6 +15,10 @@ if TYPE_CHECKING: from collections.abc import Generator +T = TypeVar("T") +U = TypeVar("U") + + __all__ = ["pull_docs"] @@ -56,6 +60,19 @@ def __str__(self) -> str: return f"{docs}\n{self.name} = {self.default}\n" +def get_metadata_field(field: dataclasses.Field[U], field_name: str, default: T) -> T: + if field_name in field.metadata: + return cast("T", field.metadata[field_name]) + return default + + +def sanitize_default_field(text: str) -> str: + text = text.replace("'", '"') + text = text.replace("True", "true") + text = text.replace("False", "false") + return text # noqa: RET504 + + def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: """ Makes documentation for a dataclass. @@ -87,7 +104,7 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: default = '""' yield DCDoc( - f"{prefix}{field.name}".replace("_", "-"), - default.replace("'", '"').replace("True", "true").replace("False", "false"), - docs[field.name], + name=f"{prefix}{field.name}".replace("_", "-"), + default=sanitize_default_field(default), + docs=docs[field.name], ) diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index adc92aaea..9945ddb2f 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -1,4 +1,5 @@ import dataclasses +from collections.abc import Iterator, Mapping from pathlib import Path from typing import Any, Dict, List, Literal, Optional, Union @@ -29,6 +30,26 @@ def __dir__() -> List[str]: return __all__ +@dataclasses.dataclass +class SettingsFieldMetadata(Mapping): # type: ignore[type-arg] + """ + Convenience dataclass to store field metadata for documentation. + """ + + def __contains__(self, key: Any) -> bool: + return any(key == field.name for field in dataclasses.fields(self)) + + def __getitem__(self, key: str) -> Any: + return getattr(self, key) + + def __len__(self) -> int: + return len(dataclasses.fields(self)) + + def __iter__(self) -> "Iterator[str]": + for field in dataclasses.fields(self): + yield field.name + + class CMakeSettingsDefine(str): """ A str subtype for automatically normalizing bool and list values From 2b00a74df276353189870e2e561adbd4bd79124e Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 23 Apr 2025 15:02:17 +0200 Subject: [PATCH 2/6] Move deprecated handling to field metadata Signed-off-by: Cristian Le --- src/scikit_build_core/settings/documentation.py | 2 ++ src/scikit_build_core/settings/skbuild_docs.py | 4 +--- src/scikit_build_core/settings/skbuild_model.py | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py index 87f85bb97..293fb5c50 100644 --- a/src/scikit_build_core/settings/documentation.py +++ b/src/scikit_build_core/settings/documentation.py @@ -54,6 +54,7 @@ class DCDoc: name: str default: str docs: str + deprecated: bool = False def __str__(self) -> str: docs = "\n".join(f"# {s}" for s in textwrap.wrap(self.docs, width=78)) @@ -107,4 +108,5 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: name=f"{prefix}{field.name}".replace("_", "-"), default=sanitize_default_field(default), docs=docs[field.name], + deprecated=get_metadata_field(field, "deprecated", False), # noqa: FBT003 ) diff --git a/src/scikit_build_core/settings/skbuild_docs.py b/src/scikit_build_core/settings/skbuild_docs.py index 65a03c34d..5aaba1596 100644 --- a/src/scikit_build_core/settings/skbuild_docs.py +++ b/src/scikit_build_core/settings/skbuild_docs.py @@ -13,14 +13,12 @@ def __dir__() -> list[str]: version = ".".join(__version__.split(".")[:2]) -INV = {"cmake.minimum-version", "ninja.minimum-version"} - def mk_skbuild_docs() -> str: """ Makes documentation for the skbuild model. """ - items = [x for x in mk_docs(ScikitBuildSettings) if x.name not in INV] + items = [x for x in mk_docs(ScikitBuildSettings) if not x.deprecated] for item in items: if item.name == "minimum-version": item.default = f'"{version}" # current version' diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index 9945ddb2f..d373daf67 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -36,6 +36,8 @@ class SettingsFieldMetadata(Mapping): # type: ignore[type-arg] Convenience dataclass to store field metadata for documentation. """ + deprecated: bool = False + def __contains__(self, key: Any) -> bool: return any(key == field.name for field in dataclasses.fields(self)) @@ -74,7 +76,9 @@ def escape_semicolons(item: str) -> str: @dataclasses.dataclass class CMakeSettings: - minimum_version: Optional[Version] = None + minimum_version: Optional[Version] = dataclasses.field( + default=None, metadata=SettingsFieldMetadata(deprecated=True) + ) """ DEPRECATED in 0.8; use version instead. """ @@ -135,7 +139,9 @@ class SearchSettings: @dataclasses.dataclass class NinjaSettings: - minimum_version: Optional[Version] = None + minimum_version: Optional[Version] = dataclasses.field( + default=None, metadata=SettingsFieldMetadata(deprecated=True) + ) """ DEPRECATED in 0.8; use version instead. """ From b965df76a3af9cd9e9a62add0e3bb3fbd7684d86 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Wed, 23 Apr 2025 15:17:46 +0200 Subject: [PATCH 3/6] Move the special default handling to the dataclass definition Signed-off-by: Cristian Le --- .../settings/documentation.py | 11 ++++++++++- .../settings/skbuild_docs.py | 16 +++------------- .../settings/skbuild_model.py | 19 ++++++++++++++++--- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py index 293fb5c50..40ace2164 100644 --- a/src/scikit_build_core/settings/documentation.py +++ b/src/scikit_build_core/settings/documentation.py @@ -10,6 +10,7 @@ from packaging.specifiers import SpecifierSet from packaging.version import Version +from .. import __version__ from .._compat.typing import get_args, get_origin if TYPE_CHECKING: @@ -26,6 +27,9 @@ def __dir__() -> list[str]: return __all__ +version_display = ".".join(__version__.split(".")[:2]) + + def _get_value(value: ast.expr) -> str: assert isinstance(value, ast.Constant) assert isinstance(value.value, str) @@ -93,7 +97,12 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: yield from mk_docs(field_type, prefix=f"{prefix}{field.name}[].") continue - if field.default is not dataclasses.MISSING and field.default is not None: + if default_before_format := get_metadata_field(field, "display_default", None): + assert isinstance(default_before_format, str) + default = default_before_format.format( + version=version_display, + ) + elif field.default is not dataclasses.MISSING and field.default is not None: default = repr( str(field.default) if isinstance(field.default, (Path, Version, SpecifierSet)) diff --git a/src/scikit_build_core/settings/skbuild_docs.py b/src/scikit_build_core/settings/skbuild_docs.py index 5aaba1596..01723d3bb 100644 --- a/src/scikit_build_core/settings/skbuild_docs.py +++ b/src/scikit_build_core/settings/skbuild_docs.py @@ -1,6 +1,5 @@ from __future__ import annotations -from .. import __version__ from .documentation import mk_docs from .skbuild_model import ScikitBuildSettings @@ -11,22 +10,13 @@ def __dir__() -> list[str]: return __all__ -version = ".".join(__version__.split(".")[:2]) - - def mk_skbuild_docs() -> str: """ Makes documentation for the skbuild model. """ - items = [x for x in mk_docs(ScikitBuildSettings) if not x.deprecated] - for item in items: - if item.name == "minimum-version": - item.default = f'"{version}" # current version' - if item.name == "install.strip": - item.default = "true" - if item.name == "wheel.packages": - item.default = '["src/", "python/", ""]' - return "\n".join(str(item) for item in items) + return "\n".join( + str(item) for item in mk_docs(ScikitBuildSettings) if not item.deprecated + ) if __name__ == "__main__": diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index d373daf67..e2089b4f0 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -36,6 +36,7 @@ class SettingsFieldMetadata(Mapping): # type: ignore[type-arg] Convenience dataclass to store field metadata for documentation. """ + display_default: Optional[str] = None deprecated: bool = False def __contains__(self, key: Any) -> bool: @@ -201,7 +202,12 @@ class SDistSettings: @dataclasses.dataclass class WheelSettings: - packages: Optional[Union[List[str], Dict[str, str]]] = None + packages: Optional[Union[List[str], Dict[str, str]]] = dataclasses.field( + default=None, + metadata=SettingsFieldMetadata( + display_default='["src/", "python/", ""]' + ), + ) """ A list of packages to auto-copy into the wheel. If this is not set, it will default to the first of ``src/``, ``python/``, or @@ -327,7 +333,9 @@ class InstallSettings: The components to install. If empty, all default components are installed. """ - strip: Optional[bool] = None + strip: Optional[bool] = dataclasses.field( + default=None, metadata=SettingsFieldMetadata(display_default="true") + ) """ Whether to strip the binaries. True for release builds on scikit-build-core 0.5+ (0.5-0.10.5 also incorrectly set this for debug builds). @@ -409,7 +417,12 @@ class ScikitBuildSettings: Enable early previews of features not finalized yet. """ - minimum_version: Optional[Version] = None + minimum_version: Optional[Version] = dataclasses.field( + default=None, + metadata=SettingsFieldMetadata( + display_default='"{version}" # current version' + ), + ) """ If set, this will provide a method for backward compatibility. """ From 30093e63c24520033ed303f6b5c0c6bb311f8872 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 24 Apr 2025 18:00:10 -0400 Subject: [PATCH 4/6] tests: add some tests for more coverage Signed-off-by: Henry Schreiner --- src/scikit_build_core/settings/documentation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py index 40ace2164..835e5b297 100644 --- a/src/scikit_build_core/settings/documentation.py +++ b/src/scikit_build_core/settings/documentation.py @@ -53,7 +53,7 @@ def pull_docs(dc: type[object]) -> dict[str, str]: } -@dataclasses.dataclass +@dataclasses.dataclass(frozen=True) class DCDoc: name: str default: str @@ -72,10 +72,7 @@ def get_metadata_field(field: dataclasses.Field[U], field_name: str, default: T) def sanitize_default_field(text: str) -> str: - text = text.replace("'", '"') - text = text.replace("True", "true") - text = text.replace("False", "false") - return text # noqa: RET504 + return text.replace("'", '"').replace("True", "true").replace("False", "false") def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: From ced24cb51c86e575ac9ab6ae9b242c0a309a271d Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 24 Apr 2025 18:24:32 -0400 Subject: [PATCH 5/6] refactor: use TypedDict Signed-off-by: Henry Schreiner --- .../settings/documentation.py | 19 +++---- .../settings/skbuild_model.py | 49 +++++++++---------- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py index 835e5b297..6adf623ec 100644 --- a/src/scikit_build_core/settings/documentation.py +++ b/src/scikit_build_core/settings/documentation.py @@ -4,8 +4,8 @@ import dataclasses import inspect import textwrap +import typing from pathlib import Path -from typing import TYPE_CHECKING, TypeVar, cast from packaging.specifiers import SpecifierSet from packaging.version import Version @@ -13,12 +13,9 @@ from .. import __version__ from .._compat.typing import get_args, get_origin -if TYPE_CHECKING: +if typing.TYPE_CHECKING: from collections.abc import Generator -T = TypeVar("T") -U = TypeVar("U") - __all__ = ["pull_docs"] @@ -65,12 +62,6 @@ def __str__(self) -> str: return f"{docs}\n{self.name} = {self.default}\n" -def get_metadata_field(field: dataclasses.Field[U], field_name: str, default: T) -> T: - if field_name in field.metadata: - return cast("T", field.metadata[field_name]) - return default - - def sanitize_default_field(text: str) -> str: return text.replace("'", '"').replace("True", "true").replace("False", "false") @@ -94,7 +85,9 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: yield from mk_docs(field_type, prefix=f"{prefix}{field.name}[].") continue - if default_before_format := get_metadata_field(field, "display_default", None): + if default_before_format := field.metadata.get("skbuild", {}).get( + "display_default", None + ): assert isinstance(default_before_format, str) default = default_before_format.format( version=version_display, @@ -114,5 +107,5 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: name=f"{prefix}{field.name}".replace("_", "-"), default=sanitize_default_field(default), docs=docs[field.name], - deprecated=get_metadata_field(field, "deprecated", False), # noqa: FBT003 + deprecated=field.metadata.get("skbuild", {}).get("deprecated", False), ) diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index e2089b4f0..85765d0df 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -1,7 +1,6 @@ import dataclasses -from collections.abc import Iterator, Mapping from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Optional, TypedDict, Union from packaging.specifiers import SpecifierSet from packaging.version import Version @@ -22,6 +21,7 @@ "SDistSettings", "ScikitBuildSettings", "SearchSettings", + "SettingsFieldMetadata", "WheelSettings", ] @@ -30,27 +30,26 @@ def __dir__() -> List[str]: return __all__ -@dataclasses.dataclass -class SettingsFieldMetadata(Mapping): # type: ignore[type-arg] - """ - Convenience dataclass to store field metadata for documentation. - """ - - display_default: Optional[str] = None - deprecated: bool = False +class SettingsFieldMetadataInner(TypedDict): + display_default: Optional[str] + deprecated: bool - def __contains__(self, key: Any) -> bool: - return any(key == field.name for field in dataclasses.fields(self)) - def __getitem__(self, key: str) -> Any: - return getattr(self, key) +class SettingsFieldMetadata(TypedDict, total=False): + skbuild: SettingsFieldMetadataInner - def __len__(self) -> int: - return len(dataclasses.fields(self)) - def __iter__(self) -> "Iterator[str]": - for field in dataclasses.fields(self): - yield field.name +def mk_metadata( + *, display_default: Optional[str] = None, deprecated: bool = False +) -> SettingsFieldMetadata: + """ + A helper function to create metadata for a field. + """ + return SettingsFieldMetadata( + skbuild=SettingsFieldMetadataInner( + display_default=display_default, deprecated=deprecated + ) + ) class CMakeSettingsDefine(str): @@ -78,7 +77,7 @@ def escape_semicolons(item: str) -> str: @dataclasses.dataclass class CMakeSettings: minimum_version: Optional[Version] = dataclasses.field( - default=None, metadata=SettingsFieldMetadata(deprecated=True) + default=None, metadata=mk_metadata(deprecated=True) ) """ DEPRECATED in 0.8; use version instead. @@ -141,7 +140,7 @@ class SearchSettings: @dataclasses.dataclass class NinjaSettings: minimum_version: Optional[Version] = dataclasses.field( - default=None, metadata=SettingsFieldMetadata(deprecated=True) + default=None, metadata=mk_metadata(deprecated=True) ) """ DEPRECATED in 0.8; use version instead. @@ -204,7 +203,7 @@ class SDistSettings: class WheelSettings: packages: Optional[Union[List[str], Dict[str, str]]] = dataclasses.field( default=None, - metadata=SettingsFieldMetadata( + metadata=mk_metadata( display_default='["src/", "python/", ""]' ), ) @@ -334,7 +333,7 @@ class InstallSettings: """ strip: Optional[bool] = dataclasses.field( - default=None, metadata=SettingsFieldMetadata(display_default="true") + default=None, metadata=mk_metadata(display_default="true") ) """ Whether to strip the binaries. True for release builds on scikit-build-core @@ -419,9 +418,7 @@ class ScikitBuildSettings: minimum_version: Optional[Version] = dataclasses.field( default=None, - metadata=SettingsFieldMetadata( - display_default='"{version}" # current version' - ), + metadata=mk_metadata(display_default='"{version}" # current version'), ) """ If set, this will provide a method for backward compatibility. From a3d86dfbaebf6bc97b34798c83ddcaefd5201312 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 24 Apr 2025 18:27:09 -0400 Subject: [PATCH 6/6] refactor: simpler TypedDict Signed-off-by: Henry Schreiner --- .../settings/documentation.py | 6 ++-- .../settings/skbuild_model.py | 31 +++++-------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py index 6adf623ec..06dadb360 100644 --- a/src/scikit_build_core/settings/documentation.py +++ b/src/scikit_build_core/settings/documentation.py @@ -85,9 +85,7 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: yield from mk_docs(field_type, prefix=f"{prefix}{field.name}[].") continue - if default_before_format := field.metadata.get("skbuild", {}).get( - "display_default", None - ): + if default_before_format := field.metadata.get("display_default", None): assert isinstance(default_before_format, str) default = default_before_format.format( version=version_display, @@ -107,5 +105,5 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]: name=f"{prefix}{field.name}".replace("_", "-"), default=sanitize_default_field(default), docs=docs[field.name], - deprecated=field.metadata.get("skbuild", {}).get("deprecated", False), + deprecated=field.metadata.get("deprecated", False), ) diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index 85765d0df..d25a4a843 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -30,28 +30,11 @@ def __dir__() -> List[str]: return __all__ -class SettingsFieldMetadataInner(TypedDict): +class SettingsFieldMetadata(TypedDict, total=False): display_default: Optional[str] deprecated: bool -class SettingsFieldMetadata(TypedDict, total=False): - skbuild: SettingsFieldMetadataInner - - -def mk_metadata( - *, display_default: Optional[str] = None, deprecated: bool = False -) -> SettingsFieldMetadata: - """ - A helper function to create metadata for a field. - """ - return SettingsFieldMetadata( - skbuild=SettingsFieldMetadataInner( - display_default=display_default, deprecated=deprecated - ) - ) - - class CMakeSettingsDefine(str): """ A str subtype for automatically normalizing bool and list values @@ -77,7 +60,7 @@ def escape_semicolons(item: str) -> str: @dataclasses.dataclass class CMakeSettings: minimum_version: Optional[Version] = dataclasses.field( - default=None, metadata=mk_metadata(deprecated=True) + default=None, metadata=SettingsFieldMetadata(deprecated=True) ) """ DEPRECATED in 0.8; use version instead. @@ -140,7 +123,7 @@ class SearchSettings: @dataclasses.dataclass class NinjaSettings: minimum_version: Optional[Version] = dataclasses.field( - default=None, metadata=mk_metadata(deprecated=True) + default=None, metadata=SettingsFieldMetadata(deprecated=True) ) """ DEPRECATED in 0.8; use version instead. @@ -203,7 +186,7 @@ class SDistSettings: class WheelSettings: packages: Optional[Union[List[str], Dict[str, str]]] = dataclasses.field( default=None, - metadata=mk_metadata( + metadata=SettingsFieldMetadata( display_default='["src/", "python/", ""]' ), ) @@ -333,7 +316,7 @@ class InstallSettings: """ strip: Optional[bool] = dataclasses.field( - default=None, metadata=mk_metadata(display_default="true") + default=None, metadata=SettingsFieldMetadata(display_default="true") ) """ Whether to strip the binaries. True for release builds on scikit-build-core @@ -418,7 +401,9 @@ class ScikitBuildSettings: minimum_version: Optional[Version] = dataclasses.field( default=None, - metadata=mk_metadata(display_default='"{version}" # current version'), + metadata=SettingsFieldMetadata( + display_default='"{version}" # current version' + ), ) """ If set, this will provide a method for backward compatibility.