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
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,37 @@ All notable changes to this project will be documented in this file.

The format is inspired by Keep a Changelog and versioned according to PEP 440.

## [2.2.2] - Unreleased
## [2.2.3] - Unreleased

This release continues the stable 2.x line with a focused validation fix in
the Pydantic adapter layer.

### Fixed

- Restored explicit `ProgrammaticError` handling for unsupported
`Annotated[..., Field(...), ExcelMeta(...)]` declarations that use native
Python types instead of `ExcelFieldCodec` subclasses
- Tightened codec resolution in the Pydantic adapter so unsupported
declarations fail at the codec resolution boundary instead of being treated
as valid runtime metadata
- Added a regression test for unsupported annotated declarations to prevent
native Python annotations from slipping through the workbook schema path

### Compatibility Notes

- No public import or export workflow API was removed in this release
- Valid `ExcelFieldCodec` and `CompositeExcelFieldCodec` declarations continue
to work unchanged
- Unsupported native annotations with `ExcelMeta(...)` now fail early with the
intended `ProgrammaticError`

### Release Summary

- unsupported annotated declarations now fail with the intended error again
- codec resolution is stricter and easier to reason about
- the validation fix is protected by an explicit integration regression test

## [2.2.2] - 2026-04-03

This release continues the stable 2.x line with stronger developer ergonomics,
clearer public API guidance, and better release-time smoke coverage.
Expand Down
78 changes: 78 additions & 0 deletions docs/releases/2.2.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 2.2.3 Release Checklist

This checklist is intended for the `2.2.3` release on top of the stable 2.x
line.

## Purpose

- publish the next stable 2.x bugfix release of ExcelAlchemy
- present `2.2.3` as a focused validation-correctness release
- restore the intended failure mode for unsupported annotated declarations
- ship regression coverage for the declaration guard in the Pydantic adapter

## Release Positioning

`2.2.3` should be presented as a small, correctness-focused release:

- the public import and export workflow API stays stable
- valid codec-based declarations continue to work unchanged
- unsupported native annotations with `ExcelMeta(...)` now fail early with the
intended `ProgrammaticError`
- the validation path is safer and easier to reason about

## Before Tagging

1. Confirm the intended version in `src/excelalchemy/__init__.py`.
2. Review the `2.2.3` section in `CHANGELOG.md`.
3. Confirm the Pydantic adapter fix in
`src/excelalchemy/helper/pydantic.py`.
4. Confirm the regression test in
`tests/integration/test_excelalchemy_workflows.py`.

## Local Verification

Run these commands from the repository root:

```bash
uv sync --extra development
uv run ruff check src/excelalchemy/helper/pydantic.py \
tests/integration/test_excelalchemy_workflows.py
uv run pyright
uv run pytest tests/integration/test_excelalchemy_workflows.py -q
rm -rf dist
uv build
uvx twine check dist/*
```

## GitHub Release Steps

1. Push the release commit to the default branch.
2. In GitHub Releases, draft a new release.
3. Create a new tag: `v2.2.3`.
4. Use the `2.2.3` section from `CHANGELOG.md` as the release notes base.
5. Publish the release and monitor the `Upload Python Package` workflow.

## Release Focus

When reviewing the final release notes, make sure they communicate these three
themes clearly:

- unsupported annotated declarations now fail with the intended error again
- codec resolution in the Pydantic adapter is stricter and more explicit
- the fix is protected by a regression test

## Recommended Release Messaging

Prefer wording that emphasizes stability and correctness:

- "continues the stable 2.x line"
- "restores the intended ProgrammaticError path"
- "tightens codec resolution in the Pydantic adapter"
- "adds regression coverage for unsupported annotated declarations"

## Done When

- the tag `v2.2.3` is published
- the GitHub Release notes clearly describe the validation fix
- the regression test passes in CI
- the published package version matches the release tag
2 changes: 1 addition & 1 deletion src/excelalchemy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""A Python Library for Reading and Writing Excel Files"""

__version__ = '2.2.2'
__version__ = '2.2.3'
from excelalchemy._primitives.constants import CharacterSet, DataRangeOption, DateFormat, Option
from excelalchemy._primitives.deprecation import ExcelAlchemyDeprecationWarning
from excelalchemy._primitives.identity import (
Expand Down
14 changes: 12 additions & 2 deletions src/excelalchemy/helper/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
from excelalchemy.metadata import FieldMetaInfo, extract_declared_field_metadata


def _resolve_excel_codec_type(annotation: object) -> type[ExcelFieldCodec]:
if isinstance(annotation, type):
if issubclass(annotation, ExcelFieldCodec):
return annotation
unsupported = repr(cast(object, annotation))
else:
unsupported = str(annotation)
raise ProgrammaticError(msg(MessageKey.VALUE_TYPE_DECLARATION_UNSUPPORTED, value_type=unsupported))


@dataclass(frozen=True)
class PydanticFieldAdapter:
"""Provide a stable view over one Pydantic field."""
Expand All @@ -34,9 +44,9 @@ def excel_codec(self) -> type[ExcelFieldCodec]:
args = [arg for arg in get_args(annotation) if arg is not type(None)]
if len(args) != 1:
raise ProgrammaticError(msg(MessageKey.UNSUPPORTED_FIELD_TYPE_DECLARATION, annotation=annotation))
return cast(type[ExcelFieldCodec], args[0])
return _resolve_excel_codec_type(args[0])

return cast(type[ExcelFieldCodec], annotation)
return _resolve_excel_codec_type(annotation)

@property
def value_type(self) -> type[ExcelFieldCodec]:
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/test_excelalchemy_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,25 @@ class AnnotatedImporter(BaseModel):
template.startswith('data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,')
)

async def test_annotated_native_python_type_with_excelmeta_raises_programmatic_error(self):
class UnsupportedAnnotatedImporter(BaseModel):
name: Annotated[str, Field(min_length=3), ExcelMeta(label='Name', order=1)]

config = ImporterConfig(
UnsupportedAnnotatedImporter,
creator=self.creator,
minio=cast(Minio, self.minio),
)

with self.assertRaises(ProgrammaticError) as cm:
ExcelAlchemy(config)

self.assertEqual(
str(cm.exception),
'Field definitions must use an ExcelFieldCodec or CompositeExcelFieldCodec subclass; '
"<class 'str'> is not supported",
)

async def test_passing_non_config_object_raises_config_error(self):
class NotImporterConfigModel(BaseModel):
name: str = FieldMeta(label='姓名')
Expand Down