diff --git a/CHANGELOG.md b/CHANGELOG.md index 15673b4..13280d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/releases/2.2.3.md b/docs/releases/2.2.3.md new file mode 100644 index 0000000..d8525e9 --- /dev/null +++ b/docs/releases/2.2.3.md @@ -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 diff --git a/src/excelalchemy/__init__.py b/src/excelalchemy/__init__.py index 0f4d887..01061ae 100644 --- a/src/excelalchemy/__init__.py +++ b/src/excelalchemy/__init__.py @@ -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 ( diff --git a/src/excelalchemy/helper/pydantic.py b/src/excelalchemy/helper/pydantic.py index e96a979..729f000 100644 --- a/src/excelalchemy/helper/pydantic.py +++ b/src/excelalchemy/helper/pydantic.py @@ -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.""" @@ -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]: diff --git a/tests/integration/test_excelalchemy_workflows.py b/tests/integration/test_excelalchemy_workflows.py index 0fb86f1..da64713 100644 --- a/tests/integration/test_excelalchemy_workflows.py +++ b/tests/integration/test_excelalchemy_workflows.py @@ -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; ' + " is not supported", + ) + async def test_passing_non_config_object_raises_config_error(self): class NotImporterConfigModel(BaseModel): name: str = FieldMeta(label='姓名')