Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Configuration API Documentation
.. autoclass:: NearCacheConfig
.. autoclass:: FlakeIdGeneratorConfig
.. autoclass:: ReliableTopicConfig
.. autoclass:: IntType
.. autoclass:: EvictionPolicy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

.. autoclass:: InMemoryFormat
.. autoclass:: SSLProtocol
Expand Down
15 changes: 15 additions & 0 deletions examples/number_types/number_types_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import asyncio

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_i8 is read from a key "i8" that was written as Int32(10). Int32 is also the one type that needs no wrapper, since a plain int already maps to it, so Int8 would show the feature better.
  • map shadows the builtin.


from hazelcast.asyncio import HazelcastClient
from hazelcast import Int32


async def amain():
client = await HazelcastClient.create_and_start()
map = await client.get_map("number_test")
await map.set("i8", Int32(10))
value_i8 = await map.get("i8")
assert type(value_i8) == int


asyncio.run(amain())
3 changes: 2 additions & 1 deletion hazelcast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "5.7.0"
__version__ = "6.0.0"

# Set the default handler to "hazelcast" loggers
# to avoid "No handlers could be found" warnings.
Expand All @@ -7,3 +7,4 @@
logging.getLogger(__name__).addHandler(logging.NullHandler())

from hazelcast.client import HazelcastClient
from hazelcast.number_types import *
55 changes: 0 additions & 55 deletions hazelcast/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,6 @@
)


class IntType:
"""Integer type options that can be used by serialization service."""

VAR = 0
"""
Integer types will be serialized as 8, 16, 32, 64 bit integers
or as Java BigInteger according to their value. This option may
cause problems when the Python client is used in conjunction with
statically typed language clients such as Java or .NET.
"""

BYTE = 1
"""
Integer types will be serialized as a 8 bit integer(as Java byte)
"""

SHORT = 2
"""
Integer types will be serialized as a 16 bit integer(as Java short)
"""

INT = 3
"""
Integer types will be serialized as a 32 bit integer(as Java int)
"""

LONG = 4
"""
Integer types will be serialized as a 64 bit integer(as Java long)
"""

BIG_INT = 5
"""
Integer types will be serialized as Java BigInteger. This option can
handle integer types which are less than -2^63 or greater than or
equal to 2^63. However, when this option is set, serializing/de-serializing
integer types is costly.
"""


class EvictionPolicy:
"""Near Cache eviction policy options."""

Expand Down Expand Up @@ -309,7 +269,6 @@ class Config:
"_class_definitions",
"_check_class_definition_errors",
"_is_big_endian",
"_default_int_type",
"_global_serializer",
"_custom_serializers",
"_near_caches",
Expand Down Expand Up @@ -370,7 +329,6 @@ def __init__(self):
self._class_definitions: typing.List[ClassDefinition] = []
self._check_class_definition_errors: bool = True
self._is_big_endian: bool = True
self._default_int_type: int = IntType.INT
self._global_serializer: typing.Optional[typing.Type[StreamSerializer]] = None
self._custom_serializers: typing.Dict[
typing.Type[typing.Any], typing.Type[StreamSerializer]
Expand Down Expand Up @@ -1021,19 +979,6 @@ def is_big_endian(self, value: bool) -> None:

self._is_big_endian = value

@property
def default_int_type(self) -> int:
"""Defines how the ``int`` type is represented on the member side.

By default, it is serialized as ``INT`` (``32`` bits). See the
:class:`hazelcast.config.IntType` for possible values.
"""
return self._default_int_type

@default_int_type.setter
def default_int_type(self, value: typing.Union[int, str]) -> None:
self._default_int_type = try_to_get_enum_value(value, IntType)

@property
def global_serializer(self) -> typing.Optional[typing.Type[StreamSerializer]]:
"""Defines the global serializer.
Expand Down
162 changes: 162 additions & 0 deletions hazelcast/number_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from typing import Self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self is imported but never used anywhere in the file. Looks like a leftover and can be dropped.


from hazelcast.serialization import MIN_SHORT, MAX_SHORT, MIN_INT, MAX_INT, MIN_LONG, MAX_LONG
from hazelcast.serialization.bits import MIN_BYTE, MAX_BYTE

__all__ = "Int8", "Int16", "Int32", "Int64", "Float32", "Float64", "BigInt"


class Int8:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types have no __eq__ / __hash__, so they compare by identity:

Int32(1) == Int32(1)  # False
Int32(1) == 1         # False

This 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.

"""Int8 represents an 8-bit signed integer

Corresponds to Java ``byte``
"""

MIN_VALUE = MIN_BYTE
MAX_VALUE = MAX_BYTE

def __init__(self, value: int):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ValueError

An isinstance(value, int) check before the range check would report it at the call site.

if not (self.MIN_VALUE <= value <= self.MAX_VALUE):
raise ValueError(
"{} value must be between {} and {}".format(
self.__class__.__name__,
self.MIN_VALUE,
self.MAX_VALUE,
)
)
self.value = value

def __int__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)


class Int16:
"""Int16 represents a 16-bit signed integer

Corresponds to Java ``short``.
"""

MIN_VALUE = MIN_SHORT
MAX_VALUE = MAX_SHORT

def __init__(self, value: int):
if not (self.MIN_VALUE <= value <= self.MAX_VALUE):
raise ValueError(
"{} value must be between {} and {}".format(
self.__class__.__name__,
self.MIN_VALUE,
self.MAX_VALUE,
)
)
self.value = value

def __int__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)


class Int32:
"""Int32 represents a 32-bit signed integer

Corresponds to Java ``int``.
"""

MIN_VALUE = MIN_INT
MAX_VALUE = MAX_INT

def __init__(self, value: int):
if not (self.MIN_VALUE <= value <= self.MAX_VALUE):
raise ValueError(
"{} value must be between {} and {}".format(
self.__class__.__name__,
self.MIN_VALUE,
self.MAX_VALUE,
)
)
self.value = value

def __int__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)


class Int64:
"""Int64 represents a 64-bit signed integer

Corresponds to Java ``long``.
"""

MIN_VALUE = MIN_LONG
MAX_VALUE = MAX_LONG

def __init__(self, value: int):
if not (self.MIN_VALUE <= value <= self.MAX_VALUE):
raise ValueError(
"{} value must be between {} and {}".format(
self.__class__.__name__,
self.MIN_VALUE,
self.MAX_VALUE,
)
)
self.value = value

def __int__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)


class BigInt:
"""BigInt represents a big integer

Corresponds to Java ``java.math.BigInteger``.
"""

def __init__(self, value: int):
self.value = value

def __int__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)


class Float32:
"""Float32 represents a 32-bit floating point number

Corresponds to Java ``float``.
"""

def __init__(self, value: float | int):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

self.value = float(value)

def __float__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)


class Float64:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be named to Double to be in sync with Java naming?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

"""Float32 represents a 64-bit floating point number

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: should be Float64 represents a 64-bit floating point number. This line shows up in the generated API docs.


Corresponds to Java ``double``.
"""

def __init__(self, value: float | int):
self.value = float(value)

def __float__(self):
return self.value

def __repr__(self) -> str:
return str(self.value)
15 changes: 8 additions & 7 deletions hazelcast/serialization/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def read(self, inp):
return inp.read_byte()

def write(self, out, obj):
out.write_byte(obj)
out.write_byte(int(obj))

def get_type_id(self):
return CONSTANT_TYPE_BYTE
Expand All @@ -63,7 +63,7 @@ def read(self, inp):
return inp.read_short()

def write(self, out, obj):
out.write_short(obj)
out.write_short(int(obj))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
out.write_short(int(obj))
out.write_short(Int16(obj))

and similar for the other int changes below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be incorrect. We must convert Int16 to Python's int type, since that's what write_short expects.


def get_type_id(self):
return CONSTANT_TYPE_SHORT
Expand All @@ -74,7 +74,7 @@ def read(self, inp):
return inp.read_int()

def write(self, out, obj):
out.write_int(obj)
out.write_int(int(obj))

def get_type_id(self):
return CONSTANT_TYPE_INTEGER
Expand All @@ -85,7 +85,7 @@ def read(self, inp):
return inp.read_long()

def write(self, out, obj):
out.write_long(obj)
out.write_long(int(obj))

def get_type_id(self):
return CONSTANT_TYPE_LONG
Expand All @@ -95,7 +95,8 @@ class FloatSerializer(BaseSerializer):
def read(self, inp):
return inp.read_float()

# "write(self, out, obj)" is never called so not implemented here
def write(self, out, obj):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write is implemented now, but "Float" is still listed in _SKIP_ON_SERIALIZE in tests/unit/serialization/binary_compatibility/reference_objects.py. That was correct while write did nothing, so this new line has no unit test covering it.

Removing "Float" from that set and adding case "Float": value = Float32(value) to the match in binary_compatibility_test.py would cover it. Right now the only test that hits it is test_float32, which needs a running cluster.

out.write_float(float(obj))

def get_type_id(self):
return CONSTANT_TYPE_FLOAT
Expand All @@ -106,7 +107,7 @@ def read(self, inp):
return inp.read_double()

def write(self, out, obj):
out.write_double(obj)
out.write_double(float(obj))

def get_type_id(self):
return CONSTANT_TYPE_DOUBLE
Expand Down Expand Up @@ -247,7 +248,7 @@ def read(self, inp):
return IOUtil.read_big_integer(inp)

def write(self, out, obj):
IOUtil.write_big_integer(out, obj)
IOUtil.write_big_integer(out, int(obj))

def get_type_id(self):
return JAVA_DEFAULT_TYPE_BIG_INTEGER
Expand Down
Loading
Loading