diff --git a/CHANGELOG.md b/CHANGELOG.md index 1788b9e..22e716f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ 0.2.2 is the first 0.2.x release published to PyPI. The 0.2.0 and 0.2.1 tags exist in git, but their publish runs were cancelled, so upgrading from PyPI goes straight from 0.1.16 to 0.2.2. The notes below cover every change in that jump. +## 0.2.6 + +Added: +- `FieldLabel`: `Annotated` metadata declaring the display label of a field whose name does not read well to users, and `declared_field_label` to read it. Form and table builders derive every other label from the field name. + ## 0.2.5 Fixed: diff --git a/pyproject.toml b/pyproject.toml index 114bc21..d0ea8da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-introspect" -version = "0.2.5" +version = "0.2.6" description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints" readme = "README.md" requires-python = ">=3.10" diff --git a/src/python_introspect/__init__.py b/src/python_introspect/__init__.py index 2dcf2a2..b619c4c 100644 --- a/src/python_introspect/__init__.py +++ b/src/python_introspect/__init__.py @@ -9,7 +9,7 @@ type resolution for framework-specific types (lazy configs, proxies, etc.) """ -__version__ = "0.2.5" +__version__ = "0.2.6" from .signature_analyzer import ( SignatureAnalyzer, @@ -40,6 +40,7 @@ mark_enableable, ) from .choices import AnnotationChoices, declared_annotation_choices +from .field_label import FieldLabel, declared_field_label from .validation import ( AnnotatedDataclassValidationMixin, AnnotationValidationError, @@ -122,6 +123,8 @@ "validate_annotation_value", "AnnotationChoices", "declared_annotation_choices", + "FieldLabel", + "declared_field_label", # Dataclass projection "dataclass_from_mapping", "project_dataclass", diff --git a/src/python_introspect/field_label.py b/src/python_introspect/field_label.py new file mode 100644 index 0000000..bb509de --- /dev/null +++ b/src/python_introspect/field_label.py @@ -0,0 +1,29 @@ +"""Display labels declared as ``Annotated`` metadata.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Annotated, get_args, get_origin + + +@dataclass(frozen=True, slots=True) +class FieldLabel: + """Display label for a field whose name does not read well to users. + + Declared as ``Annotated[T, FieldLabel("...")]``. Interfaces derive every + other label from the field name and every tooltip from the field docstring. + """ + + text: str + + +def declared_field_label(annotation: object) -> str | None: + """Return the label declared on an ``Annotated`` annotation, if any.""" + + if get_origin(annotation) is not Annotated: + return None + _base, *metadata = get_args(annotation) + for item in metadata: + if isinstance(item, FieldLabel): + return item.text + return None diff --git a/tests/test_field_label.py b/tests/test_field_label.py new file mode 100644 index 0000000..ae41123 --- /dev/null +++ b/tests/test_field_label.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass, fields +from typing import Annotated, get_type_hints + +from python_introspect import FieldLabel, declared_field_label + + +@dataclass(frozen=True) +class _Row: + labelled: Annotated[int, FieldLabel("Shown Name")] = 0 + plain: int = 0 + + +def test_declared_field_label_reads_annotated_metadata_only(): + hints = get_type_hints(_Row, include_extras=True) + labels = {field.name: declared_field_label(hints[field.name]) for field in fields(_Row)} + + assert labels == {"labelled": "Shown Name", "plain": None}