[HZ-5510] Introduced number types and removed the default_int_type config - #837
[HZ-5510] Introduced number types and removed the default_int_type config#837yuce wants to merge 4 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #837 +/- ##
==========================================
- Coverage 94.21% 94.16% -0.05%
==========================================
Files 413 414 +1
Lines 27646 27688 +42
==========================================
+ Hits 26046 26073 +27
- Misses 1600 1615 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ihsandemir
left a comment
There was a problem hiding this comment.
What about reader side? Why dont we convert to Int8, Int16, Int64, etc, on the read side?
| return str(self.value) | ||
|
|
||
|
|
||
| class Float64: |
There was a problem hiding this comment.
should it be named to Double to be in sync with Java naming?
There was a problem hiding this comment.
Double is mostly just meaningful to Java developers.
Float64 communicates that this type is a 64bit float.
Also there is some inconsistency about this even in the Java client, e.g., compact serializer has the writeFloat64 method.
|
|
||
| def write(self, out, obj): | ||
| out.write_short(obj) | ||
| out.write_short(int(obj)) |
There was a problem hiding this comment.
| out.write_short(int(obj)) | |
| out.write_short(Int16(obj)) |
and similar for the other int changes below.
There was a problem hiding this comment.
That would be incorrect. We must convert Int16 to Python's int type, since that's what write_short expects.
ihsandemir
left a comment
There was a problem hiding this comment.
Nice cleanup, explicit number types read much better than the old default_int_type config.
I ran the unit tests, mypy and black on the branch and they all pass, and I did not find a functional bug in the change itself. Most comments below are about the new number_types classes: they have no __eq__ / __hash__ and do not subclass int / float, which makes them awkward to use in normal code. There is also a docs gap, and one test that quietly stopped covering three serializers.
| @@ -0,0 +1,162 @@ | |||
| from typing import Self | |||
There was a problem hiding this comment.
Self is imported but never used anywhere in the file. Looks like a leftover and can be dropped.
| __all__ = "Int8", "Int16", "Int32", "Int64", "Float32", "Float64", "BigInt" | ||
|
|
||
|
|
||
| class Int8: |
There was a problem hiding this comment.
These types have no __eq__ / __hash__, so they compare by identity:
Int32(1) == Int32(1) # False
Int32(1) == 1 # FalseThis also affects map.get_all(): it puts the keys in a dict, so get_all([Int32(1), Int32(1)]) cannot dedupe and sends the same key to the member twice.
A simpler fix for the whole file: let Int8..Int64 subclass int, and Float32/Float64 subclass float. Then equality, hashing, arithmetic, indexing and Map[str, int] type hints all work for free. Today Int32(5) + 1 and sum([Int32(1), Int32(2)]) raise TypeError.
Dispatch keeps working, because lookup_default_serializer matches the exact type (obj_type is int), which is the same reason bool works today.
| MIN_VALUE = MIN_BYTE | ||
| MAX_VALUE = MAX_BYTE | ||
|
|
||
| def __init__(self, value: int): |
There was a problem hiding this comment.
The range is checked but the type is not, so a wrong value is only caught much later:
Int8(1.5) # accepted here, fails in to_data with "__int__ returned non-int (type float)"
Int8("3") # raises TypeError from the comparison, not the intended ValueErrorAn isinstance(value, int) check before the range check would report it at the call site.
| Corresponds to Java ``float``. | ||
| """ | ||
|
|
||
| def __init__(self, value: float | int): |
There was a problem hiding this comment.
No range check here, unlike the Int types. Float32(1e300) is accepted and then fails at write time with float too large to pack with f format.
A range check would be good. It may also be worth a docstring note that precision is lost: Float32(1.1) reads back as 1.100000023841858. That is expected for a 32-bit float, but it surprises people.
|
|
||
|
|
||
| class Float64: | ||
| """Float32 represents a 64-bit floating point number |
There was a problem hiding this comment.
Typo: should be Float64 represents a 64-bit floating point number. This line shows up in the generated API docs.
| obj = REFERENCE_OBJECTS[name] | ||
| # bool is an instance of int, so need to exclude that specifically --YT | ||
| if not isinstance(deserialized, bool) and isinstance(deserialized, int): | ||
| obj = int(obj) |
There was a problem hiding this comment.
This does nothing. REFERENCE_OBJECTS holds plain int values for Byte/Short/Integer/Long/BigInteger, so int(obj) never converts anything.
Looks like a leftover from an earlier version where the reference values were wrapped.
| ss = self._create_serialization_service(is_big_endian) | ||
| obj = REFERENCE_OBJECTS[name] | ||
| data = ss.to_data(obj) | ||
| if name == "BigInteger": |
There was a problem hiding this comment.
Only BigInteger is wrapped here, while test_serialize above also wraps Byte, Short and Long. So for those three the reference object stays a plain int and is serialized as Integer.
The test still passes, because REFERENCE_OBJECTS["Long"] is -50992225 and fits into an int32. But that means ByteSerializer, ShortSerializer and LongSerializer round-trips are no longer tested at all, and a real regression in them would go unnoticed.
Could you wrap them here the same way test_serialize does?
| from hazelcast import HazelcastClient, Int8, Int16, Int32, Int64, Float32, Float64 | ||
| from hazelcast.core import HazelcastJsonValue | ||
| from hazelcast.number_types import BigInt | ||
| from hazelcast.serialization import MAX_BYTE, MAX_SHORT, MAX_INT, MAX_LONG |
There was a problem hiding this comment.
These are unused now that test_variable_integer is gone.
Minor: BigInt is imported from hazelcast.number_types while the others come from hazelcast. BigInt is in __all__, so it can come from hazelcast too.
| @@ -0,0 +1,15 @@ | |||
| import asyncio | |||
There was a problem hiding this comment.
A few small things in this example:
- The client is never shut down and there is no
if __name__ == "__main__":guard, so the process does not exit cleanly. Other examples, e.g.examples/asyncio/map_basic_example.py, do both. value_i8is read from a key"i8"that was written asInt32(10).Int32is also the one type that needs no wrapper, since a plainintalready maps to it, soInt8would show the feature better.mapshadows the builtin.
| .. autoclass:: FlakeIdGeneratorConfig | ||
| .. autoclass:: ReliableTopicConfig | ||
| .. autoclass:: IntType | ||
| .. autoclass:: EvictionPolicy |
There was a problem hiding this comment.
IntType is removed here, but nothing is added for the new types, and docs/serialization.rst still describes the old behaviour: line 23 maps int to Byte/Short/Integer/Long/BigInteger, and line 37 tells users to configure this with default_int_type. That argument is removed by this PR, so following the docs now raises InvalidConfigurationError: Unrecognized config option: default_int_type.
Could Int8..BigInt get an entry here and a short section in serialization.rst?
Python has a single integer type. |
Hazelcast has a number of default serializers for numeric types, modeled after the corresponding Java types: signed 8, 16, 32, 64 bit integers, 32 and 64 bit floating point numbers. Go, C++ and .Net clients have corresponding native types, so they are able to serialize numeric values with the intended type. Python and Node.js clients have a different story though.
Python has a single unbounded integer type, and a single floating point type. When a user saves a number using the Python client, it is not immediately apparent which data type is used for storage. Consider the following example:
A configuration based mechanism was used as a workaround to solve this problem, and ensure a number is stored with the intended type. Python Client has the default_int_type configuration which sets the integer type to be used when storing an integer to the cluster. It must be set during client creation, and cannot be changed later:
The obvious problem with this approach is, the integer/number setting is global to a client, and can be set only once. If an application uses different number types, there is no way of storing values with different types using the client.
As I’ve shown, serializing number types is problematic using the Python Client. How about deserialization? The values are deserialized to the expected type:
intso there’s not a problem, unless the user wants to put the same value back to the cluster. That problem is out of scope for this proposal, and a possible solution will be explained in a future proposal.This problem can be easily solved by introducing missing number types. For example, the
hazelcast.Int16class can be used to serialize a value as a 16 bit signed integer:That allows us to remove the
default_int_typefrom configuration and related code. We can keep serializing integers by default as 32 bit signed integers for convenience. So if the user never setsdefault_int_typeother than the default, their code still works as expected.This PR:
default_int_typeconfiguration and related code.Int8,Int16,Int32,Int64,BigInt,Float32,Float64NOTE: Do not mind the link checker failure, they are due to AI-protection mechanism.