Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[improvement] add support for hashing / settype + more tests #39

Merged
merged 10 commits into from
Feb 1, 2019
Merged
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: 1 addition & 0 deletions conjure_python_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@
'RequestsClient',
'Service',
'ServiceConfiguration',
'SetType',
'SslConfiguration',
]
1 change: 1 addition & 0 deletions conjure_python_client/_lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@
'DictType',
'ListType',
'OptionalType',
'SetType'
]
32 changes: 28 additions & 4 deletions conjure_python_client/_lib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List, Dict, Type, Any, Union
from typing import List, Dict, Type, Any, Union, FrozenSet
from enum import Enum

from .case import to_snake_case
Expand All @@ -24,7 +24,8 @@ class ConjureType(object):


DecodableType = Union[
int, float, bool, str, ConjureType, List[Any], Dict[Any, Any]
int, float, bool, str, ConjureType,
List[Any], Dict[Any, Any], FrozenSet[Any]
]


Expand All @@ -36,6 +37,18 @@ def __init__(self, item_type):
self.item_type = item_type


class SetType(ConjureType):
_item_type = None # type: Type[DecodableType]

def __init__(self, item_type):
# type: (Type[DecodableType]) -> None
self._item_type = item_type

@property
def item_type(self):
return self._item_type


class DictType(ConjureType):
key_type = None # type: Type[DecodableType]
value_type = None # type: Type[DecodableType]
Expand Down Expand Up @@ -78,6 +91,11 @@ def _fields(cls):
name to the field definition"""
return {}

def __hash__(self):
values_tuple = tuple(self._fields().values())
keys_tuple = tuple(self._fields().keys())
return hash((values_tuple, keys_tuple))

def __eq__(self, other):
# type: (Any) -> bool
if not isinstance(other, self.__class__):
Expand Down Expand Up @@ -121,6 +139,12 @@ def _options(cls):
to the field definition for that type"""
return {}

def __hash__(self):
values = tuple([getattr(self, attr) for
attr, field_def in
self._options().items()])
return hash(values)

def __eq__(self, other):
# type: (Any) -> bool
if not isinstance(other, self.__class__):
Expand All @@ -129,9 +153,9 @@ def __eq__(self, other):
assert isinstance(other, ConjureUnionType)

pythonic_sanitized_identifier = \
sanitize_identifier(to_snake_case(self.type))
sanitize_identifier(to_snake_case(self._type))

return other.type == self.type and \
return other._type == self._type and \
getattr(self, pythonic_sanitized_identifier) == \
getattr(other, pythonic_sanitized_identifier)

Expand Down
41 changes: 34 additions & 7 deletions conjure_python_client/_serde/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
DictType,
ListType,
OptionalType,
SetType,
BinaryType)
from typing import Optional
from typing import Dict, Any, List
from typing import Dict, Any, List, FrozenSet
import inspect
import json

Expand Down Expand Up @@ -62,12 +63,14 @@ def check_null_field(
cls, obj, deserialized, python_arg_name, field_definition):
if isinstance(field_definition.field_type, ListType):
deserialized[python_arg_name] = []
elif isinstance(field_definition.field_type, SetType):
deserialized[python_arg_name] = frozenset()
elif isinstance(field_definition.field_type, DictType):
deserialized[python_arg_name] = {}
elif isinstance(field_definition.field_type, OptionalType):
deserialized[python_arg_name] = None
else:
raise Exception(
raise ValueError(
"field {} not found in object {}".format(
field_definition.identifier, obj
)
Expand Down Expand Up @@ -99,7 +102,10 @@ def decode_conjure_union_type(cls, obj, conjure_type):

deserialized = {} # type: Dict[str, Any]
if type_of_union not in obj or obj[type_of_union] is None:
cls.check_null_field(obj, deserialized, conjure_field_definition)
cls.check_null_field(obj,
deserialized,
attr,
conjure_field_definition)
else:
value = obj[type_of_union]
field_type = conjure_field_definition.field_type
Expand All @@ -118,7 +124,7 @@ def decode_conjure_enum_type(cls, obj, conjure_type):
An instance of enum of type conjure_type.
"""
if not (isinstance(obj, str) or str(type(obj)) == "<type 'unicode'>"):
raise Exception(
raise TypeError(
'Expected to find str type but found {} instead'.format(
type(obj)))

Expand Down Expand Up @@ -149,7 +155,7 @@ def decode_dict(
and the values are of type value_type.
"""
if not isinstance(obj, dict):
raise Exception("expected a python dict")
raise TypeError("expected a python dict")
if key_type == str or isinstance(key_type, BinaryType) \
or (inspect.isclass(key_type)
and issubclass(key_type, ConjureEnumType)):
Expand All @@ -176,10 +182,28 @@ def decode_list(cls, obj, element_type):
element_type.
"""
if not isinstance(obj, list):
raise Exception("expected a python list")
raise TypeError("expected a python list")

return list(map(lambda x: cls.do_decode(x, element_type), obj))

@classmethod
def decode_set(cls, obj, element_type):
# type: (List[Any], ConjureTypeType) -> FrozenSet[Any]
"""Decodes json into a frozenset, handling conversion of the elements.

Args:
obj: the json object to decode
element_type: a class object which is the conjure type of
the elements in this list.
Returns:
A python frozenset where the elements are instances of type
element_type.
"""
if not isinstance(obj, (list, set, frozenset)):
raise TypeError("expected a python list, set or frozenset")

return frozenset(map(lambda x: cls.do_decode(x, element_type), obj))

@classmethod
def decode_optional(cls, obj, object_type):
# type: (Optional[Any], ConjureTypeType) -> Optional[Any]
Expand All @@ -201,7 +225,7 @@ def decode_optional(cls, obj, object_type):
@classmethod
def decode_primitive(cls, obj, object_type):
def raise_mismatch():
raise Exception(
raise TypeError(
'Expected to find {} type but found {} instead'.format(
object_type, type(obj)))

Expand Down Expand Up @@ -250,6 +274,9 @@ def do_decode(cls, obj, obj_type):
elif isinstance(obj_type, OptionalType):
return cls.decode_optional(obj, obj_type.item_type)

elif isinstance(obj_type, SetType):
return cls.decode_set(obj, obj_type.item_type)

return cls.decode_primitive(obj, obj_type)

def decode(self, obj, obj_type):
Expand Down
2 changes: 1 addition & 1 deletion conjure_python_client/_serde/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def do_encode(cls, obj):
elif isinstance(obj, ConjureEnumType):
return obj.value

elif isinstance(obj, list):
elif isinstance(obj, (set, frozenset, list)):
return list(map(cls.do_encode, obj))

elif isinstance(obj, dict):
Expand Down
47 changes: 47 additions & 0 deletions test/serde/test_decode_enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

# (c) Copyright 2018 Palantir Technologies Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
from conjure_python_client import ConjureDecoder, ConjureEnumType


class TestEnum(ConjureEnumType):
Copy link
Contributor

Choose a reason for hiding this comment

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

could we use a generated enum instead of a hand rolled one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


A = 'A'
'''A'''
B = 'B'
'''B'''
C = 'C'
'''C'''
UNKNOWN = 'UNKNOWN'
'''UNKNOWN'''

def __reduce_ex__(self, proto):
return self.__class__, (self.name,)


def test_enum_decode():
Copy link
Contributor

Choose a reason for hiding this comment

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

could we also add some negative test cases?

decoded_A = ConjureDecoder().read_from_string("\"A\"", TestEnum)
decoded_B = ConjureDecoder().read_from_string("\"B\"", TestEnum)
decoded_A2 = ConjureDecoder().read_from_string("\"A\"", TestEnum)
assert decoded_A != decoded_B
assert decoded_A == decoded_A2

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: white space

decoded_unk = ConjureDecoder().read_from_string("\"G\"", TestEnum)
assert repr(decoded_unk) == "TestEnum.UNKNOWN"

with pytest.raises(TypeError):
decoded_integer = ConjureDecoder().read_from_string("5", TestEnum)

7 changes: 6 additions & 1 deletion test/serde/test_decode_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_object_decodes_when_exact_fields_are_present():
"""{"fileSystemId": "foo", "path": "bar"}""", CreateDatasetRequest
)
assert decoded == CreateDatasetRequest("foo", "bar")
assert hash(decoded) is not None


def test_object_with_extra_fields_should_only_keep_expected_fields():
Expand All @@ -32,27 +33,31 @@ def test_object_with_extra_fields_should_only_keep_expected_fields():
CreateDatasetRequest,
)
assert decoded == CreateDatasetRequest("foo", "bar")
assert hash(decoded) is not None


def test_object_with_list_field_decodes():
decoded = ConjureDecoder().read_from_string('{"value": []}', ListExample)
assert decoded == ListExample([])
assert hash(decoded) is not None


def test_object_with_omitted_list_field_decodes():
decoded = ConjureDecoder().read_from_string('{}', ListExample)
assert decoded == ListExample([])
assert hash(decoded) is not None


def test_object_with_map_field_decodes():
decoded = ConjureDecoder().read_from_string('{"value": {}}', MapExample)
assert decoded == MapExample({})
assert hash(decoded) is not None


def test_object_with_omitted_map_field_decodes():
decoded = ConjureDecoder().read_from_string('{}', MapExample)
assert decoded == MapExample({})

assert hash(decoded) is not None

def test_object_with_missing_field_should_throw_helpful_exception():
with pytest.raises(Exception) as excinfo:
Expand Down
98 changes: 98 additions & 0 deletions test/serde/test_decode_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# (c) Copyright 2018 Palantir Technologies Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
from conjure_python_client import ConjureDecoder, ConjureEncoder, SetType, ConjureUnionType, ConjureFieldDefinition

class TestUnion(ConjureUnionType):

_field_c = None
_field_b = None
_field_a = None

@classmethod
def _options(cls):
# type: () -> Dict[str, ConjureFieldDefinition]
return {
'field_c': ConjureFieldDefinition('fieldC', int),
'field_b': ConjureFieldDefinition('fieldB', str),
'field_a': ConjureFieldDefinition('fieldA', SetType(int))
}

def __init__(self, field_c=None, field_b=None, field_a=None):
if (field_c is not None) + (field_b is not None) + (field_a is not None) != 1:
raise ValueError('a union must contain a single member')

if field_c is not None:
self._field_c = field_c
self._type = 'fieldC'
if field_b is not None:
self._field_b = field_b
self._type = 'fieldB'
if field_a is not None:
self._field_a = field_a
self._type = 'fieldA'

@property
def field_c(self):
return self._field_c

@property
def field_b(self):
return self._field_b

@property
def field_a(self):
return self._field_a


def test_set_with_well_typed_items_decodes():
decoded = ConjureDecoder().read_from_string("[1,2,3]", SetType(int))
assert type(decoded) is frozenset
assert type(list(decoded)[0]) is int

def test_set_in_enum_decode():
decoded = ConjureDecoder().read_from_string('{"type": "fieldA"}', TestUnion)
assert type(decoded.field_a) is frozenset
assert len(decoded.field_a) == 0

def test_set_with_one_badly_typed_item_fails():
with pytest.raises(Exception):
ConjureDecoder().read_from_string("""[1,"two",3]""", SetType(int))


def test_set_with_no_items_decodes():
decoded = ConjureDecoder().read_from_string("[]", SetType(int))
assert type(decoded) is frozenset


def test_set_from_json_object_fails():
with pytest.raises(Exception):
ConjureDecoder().read_from_string("{}", SetType(int))


def test_set_does_not_decode_from_json_null():
with pytest.raises(Exception):
ConjureDecoder().read_from_string("null", SetType(int))
Copy link
Contributor

Choose a reason for hiding this comment

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

is this a json null or the string null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

json null



def test_set_does_not_decode_from_json_string():
with pytest.raises(Exception):
ConjureDecoder().read_from_string("\"hello\"", SetType(int))

def test_set_encoder():
encoded = ConjureEncoder.do_encode(frozenset([5,6,7]))
assert type(encoded) is list
encoded = ConjureEncoder.do_encode(set([5,6,7]))
assert type(encoded) is list
Loading