Skip to content

Commit 5fc2fb6

Browse files
authored
Merge pull request #58 from RayCarterLab/v2.2
feat(v2.2.1): release: prepare ExcelAlchemy 2.2.1
2 parents c1ed091 + 38dbbf4 commit 5fc2fb6

6 files changed

Lines changed: 263 additions & 58 deletions

File tree

CHANGELOG.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,47 @@ All notable changes to this project will be documented in this file.
44

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

7-
## [2.2.0] - Unreleased
7+
## [2.2.1] - Unreleased
8+
9+
This release continues the stable 2.x line with deeper metadata layering,
10+
stronger internal immutability, and tighter type boundaries around the
11+
Pydantic adapter layer.
12+
13+
### Added
14+
15+
- Added regression tests that verify split metadata layers behave like
16+
immutable value objects
17+
- Added regression tests that verify facade-level mutation replaces internal
18+
metadata layers rather than mutating them in place
19+
20+
### Changed
21+
22+
- Made `DeclaredFieldMeta`, `RuntimeFieldBinding`,
23+
`WorkbookPresentationMeta`, and `ImportConstraints` frozen internal
24+
structures
25+
- Updated `FieldMetaInfo` mutation paths to replace internal layer objects via
26+
structural updates instead of mutating them in place
27+
- Normalized workbook presentation internals so character sets and options are
28+
stored in immutable forms
29+
- Tightened key type boundaries in the Pydantic adapter around annotations,
30+
codecs, and normalized input payloads
31+
32+
### Compatibility Notes
33+
34+
- No public import or export workflow API was removed in this release
35+
- `FieldMeta(...)` and `ExcelMeta(...)` remain the stable public metadata entry
36+
points
37+
- The metadata layering changes are internal and preserve the public 2.x
38+
surface
39+
40+
### Release Summary
41+
42+
- metadata internals are now more immutable and easier to reason about
43+
- facade-level metadata updates preserve 2.x ergonomics while reducing hidden
44+
shared state
45+
- the Pydantic adapter layer now has clearer type boundaries
46+
47+
## [2.2.0] - 2026-04-03
848

949
This release continues the stable 2.x line with runtime consolidation,
1050
clearer configuration ergonomics, and a stronger protocol-first storage story.

docs/releases/2.2.0.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ line.
55

66
## Purpose
77

8-
- publish the next stable 2.x refinement release of ExcelAlchemy
8+
- publish the stable `2.2.0` refinement release of ExcelAlchemy
99
- present `2.2.0` as a runtime-consolidation and developer-ergonomics release
1010
- keep the public 2.x workflow stable while making the internal import runtime
1111
more explicit

docs/releases/2.2.1.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# 2.2.1 Release Checklist
2+
3+
This checklist is intended for the `2.2.1` release on top of the stable 2.x
4+
line.
5+
6+
## Purpose
7+
8+
- publish the next stable 2.x refinement release of ExcelAlchemy
9+
- present `2.2.1` as a metadata-consolidation and typing-tightening release
10+
- keep the public 2.x workflow stable while making internal metadata structures
11+
more immutable
12+
- continue reducing hidden shared state and internal type ambiguity
13+
14+
## Release Positioning
15+
16+
`2.2.1` should be presented as an architectural refinement release:
17+
18+
- the public import and export workflow API stays stable
19+
- metadata internals become more immutable and easier to reason about
20+
- facade-level metadata mutation remains ergonomic while internal layering gets
21+
safer
22+
- the Pydantic adapter layer continues moving toward clearer type boundaries
23+
24+
## Before Tagging
25+
26+
1. Confirm the intended version in `src/excelalchemy/__init__.py`.
27+
2. Review the `2.2.1` section in `CHANGELOG.md`.
28+
3. Confirm `README.md`, `README-pypi.md`, and `MIGRATIONS.md` still describe
29+
the recommended public paths correctly.
30+
4. Confirm `README_cn.md` remains aligned with the current release position.
31+
5. Confirm the compatibility notes for:
32+
- `FieldMeta(...)` and `ExcelMeta(...)` as stable public metadata entry points
33+
- internal metadata layering remaining an implementation detail
34+
- `storage=...` as the recommended backend path
35+
36+
## Local Verification
37+
38+
Run these commands from the repository root:
39+
40+
```bash
41+
uv sync --extra development
42+
uv run ruff check .
43+
uv run pyright
44+
uv run pytest tests
45+
rm -rf dist
46+
uv build
47+
uvx twine check dist/*
48+
```
49+
50+
Optional smoke tests:
51+
52+
```bash
53+
uv venv .pkg-smoke-base --python 3.14
54+
uv pip install --python .pkg-smoke-base/bin/python dist/*.whl
55+
.pkg-smoke-base/bin/python -c "import excelalchemy; print(excelalchemy.__version__)"
56+
```
57+
58+
## GitHub Release Steps
59+
60+
1. Push the release commit to the default branch.
61+
2. In GitHub Releases, draft a new release.
62+
3. Create a new tag: `v2.2.1`.
63+
4. Use the `2.2.1` section from `CHANGELOG.md` as the release notes base.
64+
5. Publish the release and monitor the `Upload Python Package` workflow.
65+
66+
## Release Focus
67+
68+
When reviewing the final release notes, make sure they communicate these three
69+
themes clearly:
70+
71+
- metadata internals are now more immutable and less prone to hidden shared state
72+
- facade-level metadata updates preserve 2.x ergonomics while internal layers
73+
are replaced structurally
74+
- the Pydantic adapter layer now has clearer type boundaries
75+
76+
## Recommended Release Messaging
77+
78+
Prefer wording that emphasizes refinement and stability:
79+
80+
- "continues the stable 2.x line"
81+
- "keeps the public import/export workflow API stable"
82+
- "makes metadata internals more immutable"
83+
- "tightens internal type boundaries without forcing public API changes"
84+
85+
## PyPI Verification
86+
87+
After the workflow completes:
88+
89+
1. Confirm the new release appears on PyPI.
90+
2. Confirm the long description renders correctly.
91+
3. Confirm screenshots and absolute links still work on the PyPI project page.
92+
4. Test base install:
93+
94+
```bash
95+
pip install -U ExcelAlchemy
96+
```
97+
98+
5. Test optional Minio install:
99+
100+
```bash
101+
pip install -U "ExcelAlchemy[minio]"
102+
```
103+
104+
6. Run one template-generation example.
105+
7. Run one import flow and one export flow.
106+
107+
## Done When
108+
109+
- the tag `v2.2.1` is published
110+
- the GitHub Release notes clearly communicate the three release themes
111+
- PyPI renders the project description correctly
112+
- CI, typing, tests, and package publishing all pass for the tagged release

src/excelalchemy/helper/pydantic.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections.abc import Generator, Iterable, Mapping
22
from dataclasses import dataclass
33
from types import UnionType
4-
from typing import Any, Union, cast, get_args, get_origin
4+
from typing import Union, cast, get_args, get_origin
55

66
from pydantic import BaseModel, ValidationError
77
from pydantic.fields import FieldInfo
@@ -23,23 +23,23 @@ class PydanticFieldAdapter:
2323
raw_field: FieldInfo
2424

2525
@property
26-
def annotation(self) -> Any:
26+
def annotation(self) -> object:
2727
return self.raw_field.annotation
2828

2929
@property
30-
def excel_codec(self) -> type[Any]:
30+
def excel_codec(self) -> type[ExcelFieldCodec]:
3131
annotation = self.annotation
3232
origin = get_origin(annotation)
3333
if origin in (UnionType, Union):
3434
args = [arg for arg in get_args(annotation) if arg is not type(None)]
3535
if len(args) != 1:
3636
raise ProgrammaticError(msg(MessageKey.UNSUPPORTED_FIELD_TYPE_DECLARATION, annotation=annotation))
37-
return cast(type[Any], args[0])
37+
return cast(type[ExcelFieldCodec], args[0])
3838

39-
return cast(type[Any], annotation)
39+
return cast(type[ExcelFieldCodec], annotation)
4040

4141
@property
42-
def value_type(self) -> type[Any]:
42+
def value_type(self) -> type[ExcelFieldCodec]:
4343
"""Backward-compatible alias for excel_codec."""
4444
return self.excel_codec
4545

@@ -67,14 +67,14 @@ def runtime_metadata(self) -> FieldMetaInfo:
6767
declared = self.declared_metadata
6868
return declared.bind_runtime(
6969
required=self.required,
70-
excel_codec=cast(type[ExcelFieldCodec], self.excel_codec),
70+
excel_codec=self.excel_codec,
7171
parent_label=declared.label,
7272
parent_key=Key(self.name),
7373
key=Key(self.name),
7474
offset=0,
7575
)
7676

77-
def validate_value(self, raw_value: Any) -> Any:
77+
def validate_value(self, raw_value: object) -> object:
7878
if raw_value is None:
7979
if self.allows_none and not self.required:
8080
return None
@@ -116,12 +116,12 @@ def get_model_field_names(model: type[BaseModel]) -> list[str]:
116116

117117

118118
def instantiate_pydantic_model[ModelT: BaseModel](
119-
data: Mapping[str, Any],
119+
data: Mapping[str, object],
120120
model: type[ModelT],
121121
) -> ModelT | list[ExcelCellError | ExcelRowError]:
122122
"""Instantiate a Pydantic model and return mapped Excel errors when validation fails."""
123123
model_adapter = PydanticModelAdapter(model)
124-
normalized_data: dict[str, Any] = {}
124+
normalized_data: dict[str, object] = {}
125125
errors: list[ExcelCellError | ExcelRowError] = []
126126
failed_fields: set[str] = set()
127127

@@ -158,18 +158,14 @@ def _extract_pydantic_model(model: PydanticModelAdapter) -> Generator[FieldMetaI
158158
inherited = sub_field_info.inherited_from(declared_metadata)
159159
yield inherited.bind_runtime(
160160
required=field_adapter.required,
161-
excel_codec=cast(type[ExcelFieldCodec], excel_codec),
161+
excel_codec=excel_codec,
162162
parent_label=declared_metadata.label,
163163
parent_key=Key(field_adapter.name),
164164
key=key,
165165
offset=offset,
166166
)
167-
168-
elif issubclass(excel_codec, ExcelFieldCodec):
169-
yield field_adapter.runtime_metadata()
170-
171167
else:
172-
raise ProgrammaticError(msg(MessageKey.VALUE_TYPE_DECLARATION_UNSUPPORTED, value_type=excel_codec))
168+
yield field_adapter.runtime_metadata()
173169

174170

175171
def _handle_error(
@@ -188,7 +184,7 @@ def _handle_error(
188184

189185

190186
def _model_validate[ModelT: BaseModel](
191-
data: dict[str, Any],
187+
data: dict[str, object],
192188
model: type[ModelT],
193189
model_adapter: PydanticModelAdapter,
194190
failed_fields: set[str],

0 commit comments

Comments
 (0)