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: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
project = "python-introspect"
copyright = "2025, Tristan Simas"
author = "Tristan Simas"
release = "0.1.6"
release = "0.1.7"
version = "0.1"

# -- General configuration ---------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-introspect"
version = "0.1.6"
version = "0.1.7"
description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
4 changes: 3 additions & 1 deletion src/python_introspect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
type resolution for framework-specific types (lazy configs, proxies, etc.)
"""

__version__ = "0.1.6"
__version__ = "0.1.7"

from .signature_analyzer import (
SignatureAnalyzer,
Expand Down Expand Up @@ -41,6 +41,7 @@
mark_enableable,
)
from .validation import (
AnnotatedDataclassValidationMixin,
AnnotationValidationError,
overlay_non_none_dataclass,
validate_annotated_dataclass,
Expand Down Expand Up @@ -95,6 +96,7 @@
"is_enableable",
"mark_enableable",
# Runtime annotation validation
"AnnotatedDataclassValidationMixin",
"AnnotationValidationError",
"overlay_non_none_dataclass",
"validate_annotated_dataclass",
Expand Down
13 changes: 13 additions & 0 deletions src/python_introspect/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ class AnnotationValidationError(ValueError):
DataclassT = TypeVar("DataclassT")


class AnnotatedDataclassValidationMixin:
"""Validate a dataclass instance from its resolved annotations after construction.

Dataclass declarations inherit this mixin when their annotations are the
authoritative runtime schema. Keeping the lifecycle hook on a nominal base
also preserves validation when another package recreates the dataclass with
the same bases.
"""

def __post_init__(self) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inherited hook is incompatible with dataclasses that declare InitVar fields because dataclasses pass each InitVar value into __post_init__; with this zero-argument signature those classes fail during construction before validation runs. That makes the mixin unusable for a supported dataclass feature even though it is intended as a generic lifecycle hook.

Severity: medium


🤖 Was this useful? React with 👍 or 👎

validate_annotated_dataclass(self)


def overlay_non_none_dataclass(
base: DataclassT,
overlay: object,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_version_available(self):
"""Test that __version__ is available."""
assert hasattr(python_introspect, "__version__")
assert isinstance(python_introspect.__version__, str)
assert python_introspect.__version__ == "0.1.6"
assert python_introspect.__version__ == "0.1.7"

def test_signature_analyzer_import(self):
"""Test SignatureAnalyzer is importable."""
Expand Down
21 changes: 16 additions & 5 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from dataclasses import dataclass
from dataclasses import dataclass, make_dataclass
from enum import Enum
from typing import Annotated, Literal

import pytest
from annotated_types import Ge, Gt, Le, MinLen, Predicate

from python_introspect import (
AnnotatedDataclassValidationMixin,
overlay_non_none_dataclass,
validate_annotated_dataclass,
)
Expand All @@ -21,13 +22,10 @@ class Mode(Enum):


@dataclass(frozen=True)
class BaseConfig:
class BaseConfig(AnnotatedDataclassValidationMixin):
port: Port = 7000
mode: Mode = Mode.FIRST

def __post_init__(self) -> None:
validate_annotated_dataclass(self)


@dataclass(frozen=True)
class ChildConfig(BaseConfig):
Expand All @@ -53,6 +51,19 @@ def test_validation_rejects_wrong_nominal_types_without_coercion() -> None:
ChildConfig(mode="first")


def test_inherited_validation_survives_dataclass_recreation() -> None:
RecreatedConfig = make_dataclass(
"RecreatedConfig",
(("port", Port, 7000),),
bases=(AnnotatedDataclassValidationMixin,),
frozen=True,
)

assert RecreatedConfig(port=8000).port == 8000
with pytest.raises(ValueError, match="port must be at least"):
RecreatedConfig(port=0)


@dataclass(frozen=True)
class OptionalConfig:
port: Port | None = None
Expand Down
Loading