Add Map passthrough default: leave unmapped values untouched#214
Conversation
Map rejects a key it does not know, or folds every miss to a fixed default. Neither covers the common case of a table that exists only to rewrite a few sentinel values ahead of a stricter step, where unlisted values should flow through unchanged. Add a PASSTHROUGH sentinel (in the same default= slot as UNDEFINED, and exported alongside it). Map(..., default=PASSTHROUGH) returns the value as-is on a miss, so a table rewrites only the keys it names. This is the difference between "unknown means this value" and "only touch what I named", which is what a scraped source of sentinel strings wants ahead of an enum or coercer. Document it in the validators guide and the device and sensor conversions cookbook recipe, including the enum-sentinel case.
📝 WalkthroughWalkthroughAdds a ChangesPASSTHROUGH sentinel
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/probatio/validators/coerce.py (1)
227-235: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: add
__slots__ = ()to_Passthroughfor a tighter sentinel.This prevents accidental attribute mutation on the singleton instance and signals intent that the class is a fixed sentinel with no instance state.
♻️ Optional refactor
class _Passthrough: """Type of the ``PASSTHROUGH`` sentinel: a ``Map`` miss returns the value unchanged.""" + __slots__ = () + def __repr__(self) -> str: """Render as ``PASSTHROUGH`` so it reads clearly in debug output.""" return "PASSTHROUGH"
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 94262329-42bf-4f65-93f9-8894071accaf
📒 Files selected for processing (7)
docs/src/content/docs/guides/validators.mddocs/src/content/docs/recipes/cookbook.mdsrc/probatio/__init__.pysrc/probatio/validators/__init__.pysrc/probatio/validators/coerce.pytests/__snapshots__/test_public_surface.ambrtests/validators/test_coerce.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
src/probatio/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/probatio/**/*.py: Keep the public API aligned with voluptuous, and do not rename or re-signature public names without a documented reason.
Every public function and class must have a docstring.
Do not copy code from voluptuous; implement behavior cleanly and independently while matching behavior rather than source.
Files:
src/probatio/validators/__init__.pysrc/probatio/validators/coerce.pysrc/probatio/__init__.py
{src/probatio,tests}/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Match the surrounding code style and prefer small, well-named functions.
Files:
src/probatio/validators/__init__.pysrc/probatio/validators/coerce.pysrc/probatio/__init__.pytests/validators/test_coerce.py
tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Every test must have a one-line docstring describing what it verifies.
Files:
tests/validators/test_coerce.py
🔇 Additional comments (8)
src/probatio/validators/coerce.py (1)
227-235: LGTM!Also applies to: 238-253, 280-284
src/probatio/validators/__init__.py (1)
10-10: LGTM!Also applies to: 170-170
src/probatio/__init__.py (1)
126-126: LGTM!Also applies to: 330-330
tests/__snapshots__/test_public_surface.ambr (1)
1218-1220: LGTM!tests/validators/test_coerce.py (1)
12-12: LGTM!Also applies to: 270-286
docs/src/content/docs/guides/validators.md (1)
697-710: LGTM!docs/src/content/docs/recipes/cookbook.md (2)
143-167: LGTM! TheAllordering explanation and theMap+Maybe(VehicleType)interaction are correct:Maprewrites known sentinels toNone, passes unknown values through, andMaybeallowsNonewhile coercing real values via the enum.
150-150: 🎯 Functional CorrectnessNo change needed:
StrEnumis supported. The project already targets Python 3.12+, so this example is safe as written.> Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Pull request overview
This PR adds a PASSTHROUGH sentinel to the probatio validation library, giving Map a third miss-handling mode. Previously a Map miss was either rejected or folded to one fixed default= value; Map(..., default=PASSTHROUGH) now returns an unmapped value unchanged, so a table rewrites only the keys it names. This fits the real use case of cleaning up a few sentinel strings (like "N.v.t.") ahead of a stricter enum or coercer step. The sentinel lives in the same default= slot as UNDEFINED and is exported at the top level.
Changes:
- Add the
_Passthroughtype andPASSTHROUGHsingleton, and handle it inMap.__call__before the fixed-default and rejection paths. - Export
PASSTHROUGHfromprobatioandprobatio.validators, and record it in the public-surface snapshot. - Document the new mode in the validators guide and cookbook, and add tests for hit, unhashable miss, and repr.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/probatio/validators/coerce.py |
Adds the _Passthrough/PASSTHROUGH sentinel and the passthrough branch in Map.__call__; updates the Map docstring. |
src/probatio/validators/__init__.py |
Re-exports PASSTHROUGH from coerce and adds it to __all__. |
src/probatio/__init__.py |
Exposes PASSTHROUGH at the top level and in __all__. |
tests/validators/test_coerce.py |
Adds tests for passthrough on a normal miss, an unhashable miss, and the sentinel repr. |
tests/__snapshots__/test_public_surface.ambr |
Records PASSTHROUGH as a public value in the surface snapshot. |
docs/src/content/docs/guides/validators.md |
Documents default=PASSTHROUGH with a runnable example. |
docs/src/content/docs/recipes/cookbook.md |
Adds the enum-sentinel recipe using All(Map(..., default=PASSTHROUGH), Maybe(enum)). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Breaking change
None.
PASSTHROUGHis a new opt-indefault=value forMap; existingMapcalls behave exactly as before.Proposed change
Maptoday either rejects a key it does not know, or (withdefault=) folds every miss to one fixed value. Neither fits a table that exists only to rewrite a few sentinel values ahead of a stricter step, where unlisted values should flow through unchanged.This adds a
PASSTHROUGHsentinel.Map(..., default=PASSTHROUGH)returns the value as-is on a miss, so a table rewrites only the keys it names. That is the difference between "unknown means this value" and "only touch what I named", which is what a scraped source of sentinel strings ("N.v.t.","Niet geregistreerd") wants ahead of an enum or a coercer.The sentinel lives in the same
default=slot asUNDEFINEDand is exported alongside it. Documented in the validators guide and the device and sensor conversions cookbook recipe, the latter showing the enum-sentinel case asAll(Map(..., default=PASSTHROUGH), Maybe(enum)), with a note thatMapmust run before a producing enum base.This came out of a real migration (an RDW client moving off mashumaro): a scraped government dataset sends sentinel strings for missing enum values, and there was no clean per-table way to null just those ahead of enum coercion.
Type of change
Additional information
Checklist
uv run --no-sync just testpasses locally. A pull request cannot be merged unless CI is green.uv run --no-sync just lintpasses.uv run --no-sync just typecheckpasses.