diff --git a/Cargo.lock b/Cargo.lock index 5654f93..878387e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,33 +4,28 @@ version = 3 [[package]] name = "ahash" -version = "0.8.1" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464b3811b747f8f7ebc8849c9c728c39f6ac98a055edad93baf9eb330e3f8f9d" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bumpalo" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "cfg-if" @@ -40,22 +35,20 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -65,9 +58,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -75,49 +68,31 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" - -[[package]] -name = "js-sys" -version = "0.3.60" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" -dependencies = [ - "wasm-bindgen", -] +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "libc" -version = "0.2.125" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -130,15 +105,15 @@ checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] name = "once_cell" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -146,37 +121,44 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-targets", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.20.0" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ "cfg-if", "indoc", "libc", "memoffset", "parking_lot", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -185,9 +167,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.0" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" dependencies = [ "once_cell", "target-lexicon", @@ -195,9 +177,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.0" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" dependencies = [ "libc", "pyo3-build-config", @@ -205,42 +187,43 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.0" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.42", + "syn", ] [[package]] name = "pyo3-macros-backend" -version = "0.20.0" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ "heck", "proc-macro2", + "pyo3-build-config", "quote", - "syn 2.0.42", + "syn", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags", ] @@ -258,38 +241,41 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" - -[[package]] -name = "smallvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] [[package]] -name = "syn" -version = "1.0.92" +name = "serde_derive" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "syn", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "syn" -version = "2.0.42" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -298,15 +284,15 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.3" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fa7e55043acb85fca6b3c01485a2eeb6b69c5d21002e273c79e465f43b7ac1" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "toml" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "indexmap", "serde", @@ -318,12 +304,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-xid" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" - [[package]] name = "unindent" version = "0.2.3" @@ -343,98 +323,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.83" +name = "windows-targets" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "cfg-if", - "wasm-bindgen-macro", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.83" +name = "windows_aarch64_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.92", - "wasm-bindgen-shared", -] +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] -name = "wasm-bindgen-macro" -version = "0.2.83" +name = "windows_aarch64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.83" +name = "windows_i686_gnu" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.92", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "wasm-bindgen-shared" -version = "0.2.83" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows_i686_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", -] +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" +name = "windows_x86_64_gnu" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] -name = "windows_i686_gnu" -version = "0.36.1" +name = "windows_x86_64_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] -name = "windows_i686_msvc" -version = "0.36.1" +name = "windows_x86_64_msvc" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" +name = "zerocopy" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" +name = "zerocopy-derive" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/README.md b/README.md index db95231..b9af05c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ A better TOML library for python implemented in rust. library, it passes all the [standard TOML tests](https://github.com/BurntSushi/toml-test) as well as having 100% coverage on python code. Other TOML libraries for python I tried all failed to parse some valid TOML. * Performance: see [github.com/pwwang/toml-bench](https://github.com/pwwang/toml-bench) - - rtoml is much faster than pure Python TOML libraries. + rtoml is the fastest Python TOML libraries at the time of writing. +* `None`-value handling: rtoml has flexible support for `None` values, instead of simply ignoring them. ## Install @@ -33,38 +34,50 @@ installed before you can install rtoml. #### load ```python -def load(toml: Union[str, Path, TextIO]) -> Dict[str, Any]: ... +def load(toml: Union[str, Path, TextIO], *, none_value: Optional[str] = None) -> Dict[str, Any]: ... ``` -Parse TOML via a string or file and return a python dictionary. The `toml` argument may be a `str`, -`Path` or file object from `open()`. +Parse TOML via a string or file and return a python dictionary. + +* `toml`: a `str`, `Path` or file object from `open()`. +* `none_value`: controlling which value in `toml` is loaded as `None` in python. By default, `none_value` is `None`, which means nothing is loaded as `None` #### loads ```python -def loads(toml: str) -> Dict[str, Any]: ... +def loads(toml: str, *, none_value: Optional[str] = None) -> Dict[str, Any]: ... ``` Parse a TOML string and return a python dictionary. (provided to match the interface of `json` and similar libraries) +* `toml`: a `str` containing TOML. +* `none_value`: controlling which value in `toml` is loaded as `None` in python. By default, `none_value` is `None`, which means nothing is loaded as `None` + #### dumps ```python -def dumps(obj: Any, *, pretty: bool = False) -> str: ... +def dumps(obj: Any, *, pretty: bool = False, none_value: Optional[str] = "null") -> str: ... ``` Serialize a python object to TOML. -If `pretty` is true, output has a more "pretty" format. +* `obj`: a python object to be serialized. +* `pretty`: if `True` the output has a more "pretty" format. +* `none_value`: controlling how `None` values in `obj` are serialized. `none_value=None` means `None` values are ignored. #### dump ```python -def dump(obj: Any, file: Union[Path, TextIO], *, pretty: bool = False) -> int: ... +def dump( + obj: Any, file: Union[Path, TextIO], *, pretty: bool = False, none_value: Optional[str] = "null" +) -> int: ... ``` -Serialize a python object to TOML and write it to a file. `file` may be a `Path` or file object from `open()`. +Serialize a python object to TOML and write it to a file. -If `pretty` is true, output has a more "pretty" format. +* `obj`: a python object to be serialized. +* `file`: a `Path` or file object from `open()`. +* `pretty`: if `True` the output has a more "pretty" format. +* `none_value`: controlling how `None` values in `obj` are serialized. `none_value=None` means `None` values are ignored. -### Example +### Examples ```py from datetime import datetime, timezone, timedelta @@ -116,3 +129,33 @@ server = "192.168.1.1" ports = [8001, 8001, 8002] """ ``` + +An example of `None`-value handling: + +```python +obj = { + 'a': None, + 'b': 1, + 'c': [1, 2, None, 3], +} + +# Ignore None values +assert rtoml.dumps(obj, none_value=None) == """\ +b = 1 +c = [1, 2, 3] +""" + +# Serialize None values as '@None' +assert rtoml.dumps(obj, none_value='@None') == """\ +a = "@None" +b = 1 +c = [1, 2, "@None", 3] +""" + +# Deserialize '@None' back to None +assert rtoml.load("""\ +a = "@None" +b = 1 +c = [1, 2, "@None", 3] +""", none_value='@None') == obj +``` diff --git a/example.py b/example.py index ba1ce16..7eda407 100644 --- a/example.py +++ b/example.py @@ -1,4 +1,5 @@ -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone + import rtoml obj = { diff --git a/rtoml/__init__.py b/rtoml/__init__.py index 4e24a72..95b549e 100644 --- a/rtoml/__init__.py +++ b/rtoml/__init__.py @@ -1,6 +1,6 @@ from io import TextIOBase from pathlib import Path -from typing import Any, Dict, TextIO, Union +from typing import Any, Dict, Optional, TextIO, Union from . import _rtoml @@ -13,49 +13,67 @@ TomlSerializationError = _rtoml.TomlSerializationError -def load(toml: Union[str, Path, TextIO]) -> Dict[str, Any]: +def load(toml: Union[str, Path, TextIO], *, none_value: Optional[str] = None) -> Dict[str, Any]: """ - Parse TOML via a string or file and return a python dict. The `toml` argument may be a `str`, - `Path` or file object from `open()`. + Parse TOML via a string or file and return a python dict. + + Args: + toml: a `str`, `Path` or file object from `open()`. + none_value: controlling which value in `toml` is loaded as `None` in python. + By default, `none_value` is `None`, which means nothing is loaded as `None`. """ if isinstance(toml, Path): toml = toml.read_text(encoding='UTF-8') elif isinstance(toml, (TextIOBase, TextIO)): toml = toml.read() - return loads(toml) + return loads(toml, none_value=none_value) -def loads(toml: str) -> Dict[str, Any]: +def loads(toml: str, *, none_value: Optional[str] = None) -> Dict[str, Any]: """ Parse a TOML string and return a python dict. (provided to match the interface of `json` and similar libraries) + + Args: + toml: a `str` containing TOML. + none_value: controlling which value in `toml` is loaded as `None` in python. + By default, `none_value` is `None`, which means nothing is loaded as `None`. """ if not isinstance(toml, str): raise TypeError(f'invalid toml input, must be str not {type(toml)}') - return _rtoml.deserialize(toml) + return _rtoml.deserialize(toml, none_value=none_value) -def dumps(obj: Any, *, pretty: bool = False) -> str: +def dumps(obj: Any, *, pretty: bool = False, none_value: Optional[str] = 'null') -> str: """ Serialize a python object to TOML. - If `pretty` is true, output has a more "pretty" format. + Args: + obj: a python object to be serialized. + pretty: if true, output has a more "pretty" format. + none_value: controlling how `None` values in `obj` are serialized. + `none_value=None` means `None` values are ignored. """ if pretty: serialize = _rtoml.serialize_pretty else: serialize = _rtoml.serialize - return serialize(obj) + return serialize(obj, none_value=none_value) -def dump(obj: Any, file: Union[Path, TextIO], *, pretty: bool = False) -> int: +def dump(obj: Any, file: Union[Path, TextIO], *, pretty: bool = False, none_value: Optional[str] = 'null') -> int: """ - Serialize a python object to TOML and write it to a file. `file` may be a `Path` or file object from `open()`. + Serialize a python object to TOML and write it to a file. - If `pretty` is true, output has a more "pretty" format. + Args: + obj: a python object to be serialized. + file: a `Path` or file object from `open()`. + pretty: if `True` the output has a more "pretty" format. + none_value: controlling how `None` values in `obj` are serialized. + `none_value=None` means `None` values are ignored. """ - s = dumps(obj, pretty=pretty) + s = dumps(obj, pretty=pretty, none_value=none_value) if isinstance(file, Path): return file.write_text(s, encoding='UTF-8') else: diff --git a/rtoml/_rtoml.pyi b/rtoml/_rtoml.pyi index 60c3b68..3c85687 100644 --- a/rtoml/_rtoml.pyi +++ b/rtoml/_rtoml.pyi @@ -2,9 +2,9 @@ from typing import Any __version__: str -def deserialize(toml: str) -> Any: ... -def serialize(obj: Any) -> str: ... -def serialize_pretty(obj: Any) -> str: ... +def deserialize(toml: str, none_value: str | None = None) -> Any: ... +def serialize(obj: Any, none_value: str | None = 'null') -> str: ... +def serialize_pretty(obj: Any, none_value: str | None = 'null') -> str: ... class TomlParsingError(ValueError): ... class TomlSerializationError(ValueError): ... diff --git a/src/de.rs b/src/de.rs index bdf8756..ab152c8 100644 --- a/src/de.rs +++ b/src/de.rs @@ -20,11 +20,12 @@ pub type NoHashSet = HashSet>; pub struct PyDeserializer<'py> { py: Python<'py>, + none_value: Option<&'py str>, } impl<'py> PyDeserializer<'py> { - pub fn new(py: Python<'py>) -> Self { - Self { py } + pub fn new(py: Python<'py>, none_value: Option<&'py str>) -> Self { + Self { py, none_value } } } @@ -78,7 +79,10 @@ impl<'de, 'py> Visitor<'de> for PyDeserializer<'py> { where E: de::Error, { - Ok(value.into_py(self.py)) + match self.none_value { + Some(none_value) if value == none_value => Ok(self.py.None()), + _ => Ok(value.into_py(self.py)), + } } fn visit_unit(self) -> Result { @@ -91,7 +95,7 @@ impl<'de, 'py> Visitor<'de> for PyDeserializer<'py> { { let mut elements = Vec::new(); - while let Some(elem) = seq.next_element_seed(PyDeserializer::new(self.py))? { + while let Some(elem) = seq.next_element_seed(PyDeserializer::new(self.py, self.none_value))? { elements.push(elem); } @@ -102,7 +106,7 @@ impl<'de, 'py> Visitor<'de> for PyDeserializer<'py> { where A: MapAccess<'de>, { - match map_access.next_entry_seed(PhantomData::, PyDeserializer::new(self.py))? { + match map_access.next_entry_seed(PhantomData::, PyDeserializer::new(self.py, self.none_value))? { Some((first_key, first_value)) if first_key == DATETIME_MAPPING_KEY => { let py_string = first_value.extract::<&str>(self.py).map_err(de::Error::custom)?; let dt: TomlDatetime = TomlDatetime::from_str(py_string).map_err(de::Error::custom)?; @@ -119,7 +123,7 @@ impl<'de, 'py> Visitor<'de> for PyDeserializer<'py> { dict.set_item(first_key, first_value).map_err(de::Error::custom)?; while let Some((key, value)) = - map_access.next_entry_seed(PhantomData::, PyDeserializer::new(self.py))? + map_access.next_entry_seed(PhantomData::, PyDeserializer::new(self.py, self.none_value))? { if key_set.insert(hash_builder.hash_one(&key)) { dict.set_item(key, value).map_err(de::Error::custom)?; diff --git a/src/lib.rs b/src/lib.rs index e5865a4..7412f7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,16 +16,16 @@ create_exception!(_rtoml, TomlParsingError, PyValueError); create_exception!(_rtoml, TomlSerializationError, PyValueError); #[pyfunction] -fn deserialize(py: Python, toml_data: String) -> PyResult { +fn deserialize(py: Python, toml_data: String, none_value: Option<&str>) -> PyResult { let mut deserializer = Deserializer::new(&toml_data); - let seed = de::PyDeserializer::new(py); + let seed = de::PyDeserializer::new(py, none_value); seed.deserialize(&mut deserializer) .map_err(|e| TomlParsingError::new_err(e.to_string())) } #[pyfunction] -fn serialize(py: Python, obj: &PyAny) -> PyResult { - let s = SerializePyObject::new(py, obj); +fn serialize(py: Python, obj: &PyAny, none_value: Option<&str>) -> PyResult { + let s = SerializePyObject::new(py, obj, none_value); match to_toml_string(&s) { Ok(s) => Ok(s), Err(e) => Err(TomlSerializationError::new_err(e.to_string())), @@ -33,8 +33,8 @@ fn serialize(py: Python, obj: &PyAny) -> PyResult { } #[pyfunction] -fn serialize_pretty(py: Python, obj: &PyAny) -> PyResult { - let s = SerializePyObject::new(py, obj); +fn serialize_pretty(py: Python, obj: &PyAny, none_value: Option<&str>) -> PyResult { + let s = SerializePyObject::new(py, obj, none_value); match to_toml_string_pretty(&s) { Ok(s) => Ok(s), Err(e) => Err(TomlSerializationError::new_err(e.to_string())), diff --git a/src/py_type.rs b/src/py_type.rs index e87eed8..97ce441 100644 --- a/src/py_type.rs +++ b/src/py_type.rs @@ -1,6 +1,6 @@ use pyo3::once_cell::GILOnceCell; use pyo3::prelude::*; -use pyo3::types::{PyByteArray, PyBytes, PyDate, PyDateTime, PyDelta, PyDict, PyList, PyString, PyTime, PyTuple}; +use pyo3::types::{PyByteArray, PyBytes, PyDate, PyDateTime, PyDict, PyList, PyString, PyTime, PyTuple}; #[derive(Clone)] #[cfg_attr(debug_assertions, derive(Debug))] @@ -23,7 +23,6 @@ pub struct PyTypeLookup { pub datetime: usize, pub date: usize, pub time: usize, - pub timedelta: usize, } static TYPE_LOOKUP: GILOnceCell = GILOnceCell::new(); @@ -51,7 +50,6 @@ impl PyTypeLookup { .get_type_ptr() as usize, date: PyDate::new(py, 2000, 1, 1).unwrap().get_type_ptr() as usize, time: PyTime::new(py, 0, 0, 0, 0, None).unwrap().get_type_ptr() as usize, - timedelta: PyDelta::new(py, 0, 0, 0, false).unwrap().get_type_ptr() as usize, } } diff --git a/src/ser.rs b/src/ser.rs index ddfc5c4..0519877 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -11,14 +11,16 @@ use crate::py_type::PyTypeLookup; pub struct SerializePyObject<'py> { obj: &'py PyAny, py: Python<'py>, + none_value: Option<&'py str>, ob_type_lookup: &'py PyTypeLookup, } impl<'py> SerializePyObject<'py> { - pub fn new(py: Python<'py>, obj: &'py PyAny) -> Self { + pub fn new(py: Python<'py>, obj: &'py PyAny, none_value: Option<&'py str>) -> Self { Self { obj, py, + none_value, ob_type_lookup: PyTypeLookup::cached(py), } } @@ -27,6 +29,7 @@ impl<'py> SerializePyObject<'py> { Self { obj, py: self.py, + none_value: self.none_value, ob_type_lookup: self.ob_type_lookup, } } @@ -57,7 +60,7 @@ impl<'py> Serialize for SerializePyObject<'py> { // ugly but this seems to be just marginally faster than a guarded match, also allows for custom cases // if we wanted to add them if ob_type == lookup.none { - serializer.serialize_str("null") + serializer.serialize_str(self.none_value.unwrap_or("null")) } else if ob_type == lookup.int { serialize!(i64) } else if ob_type == lookup.bool { @@ -78,7 +81,9 @@ impl<'py> Serialize for SerializePyObject<'py> { for (k, v) in py_dict { let v_ob_type = v.get_type_ptr() as usize; - if v_ob_type == lookup.dict { + if self.none_value.is_none() && (v_ob_type == lookup.none || k.is_none()) { + continue; + } else if v_ob_type == lookup.dict { dict_items.push((k, v)); } else if v_ob_type == lookup.list || v_ob_type == lookup.tuple { array_items.push((k, v)); @@ -88,17 +93,17 @@ impl<'py> Serialize for SerializePyObject<'py> { } let mut map = serializer.serialize_map(Some(len))?; for (k, v) in simple_items { - let key = table_key(k)?; + let key = table_key(k, self.none_value)?; let value = self.with_obj(v); map.serialize_entry(key, &value)?; } for (k, v) in array_items { - let key = table_key(k)?; + let key = table_key(k, self.none_value)?; let value = self.with_obj(v); map.serialize_entry(key, &value)?; } for (k, v) in dict_items { - let key = table_key(k)?; + let key = table_key(k, self.none_value)?; let value = self.with_obj(v); map.serialize_entry(key, &value)?; } @@ -107,14 +112,18 @@ impl<'py> Serialize for SerializePyObject<'py> { let py_list: &PyList = self.obj.downcast().map_err(map_py_err)?; let mut seq = serializer.serialize_seq(Some(py_list.len()))?; for element in py_list { - seq.serialize_element(&self.with_obj(element))? + if self.none_value.is_some() || !element.is_none() { + seq.serialize_element(&self.with_obj(element))? + } } seq.end() } else if ob_type == lookup.tuple { let py_tuple: &PyTuple = self.obj.downcast().map_err(map_py_err)?; let mut seq = serializer.serialize_seq(Some(py_tuple.len()))?; for element in py_tuple { - seq.serialize_element(&self.with_obj(element))? + if self.none_value.is_some() || !element.is_none() { + seq.serialize_element(&self.with_obj(element))? + } } seq.end() } else if ob_type == lookup.datetime { @@ -151,11 +160,11 @@ fn map_py_err(err: I) -> O { O::custom(err.to_string()) } -fn table_key(key: &PyAny) -> Result<&str, E> { +fn table_key<'py, E: SerError>(key: &'py PyAny, none_value: Option<&'py str>) -> Result<&'py str, E> { if let Ok(py_string) = key.downcast::() { py_string.to_str().map_err(map_py_err) } else if key.is_none() { - Ok("null") + Ok(none_value.unwrap_or("null")) } else if let Ok(key) = key.extract::() { Ok(if key { "true" } else { "false" }) } else { diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index cae6dd8..841ea1c 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -127,11 +127,14 @@ def test_data(): @pytest.mark.benchmark(group='load-data.toml') -def test_load_data_toml(benchmark): +def test_loads_data_toml(benchmark): data_toml_str = data_toml_path.read_text() - # benchmark(rtoml.loads, data_toml_bytes, name='loads-bytes') - benchmark(rtoml.loads, data_toml_str, name='loads-str') - benchmark(rtoml.load, data_toml_path, name='load-path') + benchmark(rtoml.loads, data_toml_str) + + +@pytest.mark.benchmark(group='load-data.toml') +def test_load_data_toml(benchmark): + benchmark(rtoml.load, data_toml_path) @pytest.mark.benchmark(group='load-dict') diff --git a/tests/test_dump.py b/tests/test_dump.py index 8e758ba..f1fc6c3 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -10,7 +10,9 @@ [ ({'text': '\nfoo\nbar\n'}, 'text = "\\nfoo\\nbar\\n"\n'), ({'foo': 'bar'}, 'foo = "bar"\n'), + (None, '"!NONE"'), ([1, 2, 3], '[1, 2, 3]'), + ([1, 2, None], '[1, 2, "!NONE"]'), (datetime(1979, 5, 27, 7, 32), '1979-05-27T07:32:00'), (datetime(1979, 5, 27, 7, 32, tzinfo=timezone.utc), '1979-05-27T07:32:00Z'), (date(2022, 12, 31), '2022-12-31'), @@ -18,14 +20,27 @@ ({'x': datetime(1979, 5, 27, 7, 32)}, 'x = 1979-05-27T07:32:00\n'), # order changed to avoid https://github.com/alexcrichton/toml-rs/issues/142 ({'x': {'a': 1}, 'y': 4}, 'y = 4\n\n[x]\na = 1\n'), + ({'x': 1, 'y': None}, 'x = 1\ny = "!NONE"\n'), ((1, 2, 3), '[1, 2, 3]'), + ((1, 2, None), '[1, 2, "!NONE"]'), ({'emoji': '😷'}, 'emoji = "😷"\n'), ({'bytes': b'123'}, 'bytes = [49, 50, 51]\n'), # TODO: should this be a string of "123" ({'polish': 'Witaj świecie'}, 'polish = "Witaj świecie"\n'), ], ) def test_dumps(input_obj, output_toml): - assert rtoml.dumps(input_obj) == output_toml + assert rtoml.dumps(input_obj, none_value='!NONE') == output_toml + + @pytest.mark.parametrize( + 'input_obj,output_toml', + [ + ([1, 2, None], '[1, 2]'), + ((1, 2, None), '[1, 2]'), + (None, 'null'), + ], + ) + def test_dumps_no_none(input_obj, output_toml): + assert rtoml.dumps(input_obj, none_value=None) == output_toml @pytest.mark.parametrize( @@ -64,3 +79,32 @@ def test_dump_file(tmp_path): def test_varied_list(): assert rtoml.dumps({'test': [1, '2']}) == 'test = [1, "2"]\n' + + +@pytest.mark.parametrize( + 'input_obj, none_value, output_toml', + [ + ({'test': None}, 'null', 'test = "null"\n'), + ({'test': None}, 'foo', 'test = "foo"\n'), + ({'test': None}, None, ''), + ({None: 'test'}, 'null', 'null = "test"\n'), + ({None: 'test'}, 'foo', 'foo = "test"\n'), + ({None: 'test'}, None, ''), + ({'test': [1, None, 2]}, 'null', 'test = [1, "null", 2]\n'), + ({'test': [1, None, 2]}, 'foo', 'test = [1, "foo", 2]\n'), + ({'test': [1, None, 2]}, None, 'test = [1, 2]\n'), + ( + {'test': {'x': [{'y': [1, None, 2]}], 'z': None}}, + 'null', + '[test]\nz = "null"\n\n[[test.x]]\ny = [1, "null", 2]\n', + ), + ( + {'test': {'x': [{'y': [1, None, 2]}], 'z': None}}, + 'foo', + '[test]\nz = "foo"\n\n[[test.x]]\ny = [1, "foo", 2]\n', + ), + ({'test': {'x': [{'y': [1, None, 2]}], 'z': None}}, None, '[[test.x]]\ny = [1, 2]\n'), + ], +) +def test_none_value(input_obj, none_value, output_toml): + assert rtoml.dumps(input_obj, none_value=none_value) == output_toml diff --git a/tests/test_load.py b/tests/test_load.py index 7c48640..5b5e83e 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -227,3 +227,15 @@ def test_subtable(): """ with pytest.raises(rtoml.TomlParsingError, match='duplicate key: `apple` for key `fruit` at line 5 column 1'): rtoml.loads(s) + + +def test_none_value(): + assert rtoml.loads('x = "null"') == {'x': 'null'} + assert rtoml.loads('x = "null"', none_value='null') == {'x': None} + assert rtoml.loads('x = "null"', none_value='') == {'x': 'null'} + assert rtoml.loads('x = ""', none_value='') == {'x': None} + + # Reproducible with the same none_value repr + s = {'x': None} + assert rtoml.dumps(s, none_value='py:None') == 'x = "py:None"\n' + assert rtoml.loads('x = "py:None"\n', none_value='py:None') == s