Repository navigation
Conversation
to_spec emitted object arrays with a raw tolist(), so numpy scalars such as np.bool_ in fitted state (e.g. SimpleImputer.statistics_) leaked into the 'JSON-compatible' spec: fingerprint_, reproducibility_report() and json.dumps(to_spec()) raised, and to_spec(path) left a truncated file behind, overwriting any good spec at that path. Builtin dtypes such as Preprocessor(dtype=float) were written as builtins:float, which from_spec always refuses. Object-array elements are now encoded one by one (the format is unchanged, so existing specs still load), builtin dtype specifiers are stored as the numpy dtype they denote, types a spec could never load raise PretabSerializationError at save time, and to_spec serializes to text before opening the target file, so a failure never truncates an existing spec. A JSON-writer failure is raised as PretabSerializationError as well. The to_spec, fingerprint_ and dtype docstrings and the reproducibility guide describe the save-time errors and the dtype handling. Fixes #66
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #66.
A fitted
Preprocessorwhose object-dtype state holds numpy scalars (such asnp.bool_inSimpleImputer.statistics_orOneHotEncoder.categories_) could not be fingerprinted or saved:fingerprint_,reproducibility_report()andjson.dumps(to_spec())raisedTypeError, andto_spec(path)left a truncated file behind, overwriting any spec already at that path.Preprocessor(dtype=float)(orint,bool) saved a spec thatfrom_specalways refused.Root cause
_encodewrote an object array asobj.tolist(), which keeps the original element objects, so numpy scalars inside it never went through the scalar conversion and reachedjson.dumps._encodewrote everytypeasmodule:qualname. Builtin dtype specifiers resolve to thebuiltinsmodule, which the loader's allow-list refuses.to_spec(path)opened the file with"w"and streamedjson.dumpinto it, so an encoding error left a truncated file.Fix
_encode(pretab/compose/serialize.py) encodes the elements of an object array one by one through the new_map_elementshelper. The__ndarray__format is unchanged._decode_ndarrayfills an object array element by element with_decode, so a decoded element that is itself a sequence stays one element.bool,int,floatandcomplex(_BUILTIN_DTYPES) are written as{"__npdtype__": ...}for the numpy dtype they denote. Any other type from a module outside the allow-list raisesPretabSerializationErrorwhen the spec is built._spec_jsoninpretab/preprocessor.pyserializes a spec to JSON text and raisesPretabSerializationErrorfor aTypeErrororValueErrorfrom the JSON writer.Preprocessor.to_specbuilds the text before it openspath, andfingerprint_uses the same helper.Behaviour changes
dtype(float,int,bool,complex) is stored as the numpy dtype it denotes. Afterfrom_spec, thedtypeparameter is thatnp.dtype(for exampledtype('float64')), not the builtin type, andfingerprint_of such a preprocessor changes. Fingerprints of the default preprocessor, one without imputers and a one-hot preprocessor are unchanged (compared withmain).to_spec()andfingerprint_raisePretabSerializationErrorinstead ofTypeErrorwhen the spec cannot be written as JSON.to_spec()serializes the spec once even withoutpath, so this error is raised there instead of in a laterjson.dumps.pretab,sklearn,numpyandscipyraisesPretabSerializationErrorat save time. Before, the spec was written andfrom_specrefused it.to_spec(path)leaves an existing file atpathuntouched.Documentation
Preprocessor.to_specdocstring: new Raises section (a value or type a spec cannot store or load; raised beforepathis opened).Preprocessor.fingerprint_docstring: new Raises section.Preprocessordocstring,dtypeparameter: a builtin specifier such asfloatis stored in a spec as the numpy dtype it denotes.docs/core_concepts/reproducibility.md("Portable serialization"):PretabSerializationErroris raised when the spec is built, and a failedto_spec(path)leaves an existing spec at that path untouched._map_elementsand_spec_json.Tests
test_object_arrays_of_numpy_scalars_serialize: the issue'snp.bool_/Noneobject column;json.dumps(to_spec())andfingerprint_work, and a file round trip reproducestransform. Fails onmain, passes here.test_builtin_and_numpy_dtypes_round_trip[float|int]: a builtindtypesurvives a JSON round trip with identical output and fingerprint. Fails onmain, passes here.test_builtin_and_numpy_dtypes_round_trip[float32]: the same fornp.float32. Guard test: passes on both by design.test_failed_save_leaves_an_existing_spec_untouched: a spec that the JSON writer rejects raisesPretabSerializationError, and the file saved earlier at the same path is unchanged. Fails onmain, passes here.test_types_a_spec_cannot_load_are_rejected_at_save_time:_encode(list)raisesPretabSerializationError. Fails onmain, passes here.test_object_arrays_written_before_element_encoding_still_load: an object array in the 1.0.0 format decodes to the same 2-D object array. Guard test: passes on both by design.Reproduction
Output of the script from #66 on this branch:
Each call runs in its own
try/except, as in the issue, and prints its result; bothfrom_speccalls also comparetransformwith the original preprocessor.Checks
just test: 1445 passed, 59 skipped, 7 xfailed; coverage 93.74% (gate 90%).scripts/quickstart.pypasses.just docs: strict build succeeds.just check: all pre-commit and pre-push hooks pass (ruff format, ruff lint, prettier, file hygiene, pyright with 0 errors).Merge notes
Base:
main.The PR for Default to_spec() files contain bare NaN tokens and are not valid JSON for strict parsers #80 (fix(serialization): write specs as strict JSON without NaN tokens #134) changes the same code. Both add the same
_map_elementshelper, which git merges once, and they conflict in the ndarray branch of_encode, in_decode_ndarray, into_specandfingerprint_inpretab/preprocessor.py, and at the end oftests/integration/test_serialization.py, where both append tests. Resolution:_encode: keep this PR's object branch,data = _map_elements(data, obj.ndim, _encode)(_encodethen tags non-finite floats itself), followed by Default to_spec() files contain bare NaN tokens and are not valid JSON for strict parsers #80'selif obj.dtype.kind == "f" and not np.isfinite(obj).all(): data = _map_elements(data, obj.ndim, _encode_float)._decode_ndarray: keep this PR's element-by-element object branch (its_decodeknows the__float__tag), then decode float arrays withdata = _map_elements(data, len(payload["shape"]), _decode)whendtype.kind == "f"._encode_object_elementand_decode_float_element.pretab/preprocessor.py: keep this PR'sto_specandfingerprint_and passallow_nan=Falsein_spec_json(json.dumps(spec, allow_nan=False, **kwargs)) instead of Default to_spec() files contain bare NaN tokens and are not valid JSON for strict parsers #80's two direct dumps. Optionally say "strict JSON" in its docstring.Resolved this way, the full suite passes (1449 passed, 59 skipped, 7 xfailed), and the scripts from to_spec / fingerprint_ fail or write unloadable specs for object arrays of numpy scalars and builtin dtypes #66 and Default to_spec() files contain bare NaN tokens and are not valid JSON for strict parsers #80 both show the fixed behaviour.
The PR for Standalone transformers drop DataFrame feature names, so reordered columns are silently misaligned #85 (fix(transformers): record and check DataFrame feature names #135) changes
_representation_summaryand the..exceptionsimport inpretab/compose/serialize.py. Git merges it cleanly with this branch.This change was previously part of #75, which is closed in favour of one pull request per issue.