Skip to content

Commit

Permalink
Merge pull request #58 from ramonhagenaars/Release/0.8.9
Browse files Browse the repository at this point in the history
Release/0.8.9
  • Loading branch information
ramonhagenaars committed May 12, 2019
2 parents 17866de + 3e146fd commit 03b5ff9
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 25 deletions.
12 changes: 6 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ Meta
Recent updates
==============

0.8.9
+++++
- *Breaking change*: Values of primitive types are now cast if possible (e.g. in ``jsons.load('42', int)``).
- Bugfix: NamedTuples could falsely raise an error when a justified ``None`` was provided.
- Feature: Support for ``uuid.UUID``.

0.8.8
+++++
- Feature: Added the ability to dump recursive objects.
Expand All @@ -119,12 +125,6 @@ Recent updates
- Feature: Support for Defaultdict.
- Feature: Support for ChainMap.

0.8.4
+++++
- Feature: Support for textual type hints.
- Feature: Automatically announce classes when dumped.
- Bugfix: Support for ``from __future__ import annotations``


Contributors
============
Expand Down
7 changes: 5 additions & 2 deletions jsons/_load_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""
import json
from json import JSONDecodeError
from typing import Optional, Dict, Callable, Tuple
from typing import Optional, Dict, Callable, Tuple, Any
from jsons._lizers_impl import get_deserializer
from jsons.exceptions import DeserializationError, JsonsError, DecodeError
from jsons._common_impl import (
Expand Down Expand Up @@ -175,5 +175,8 @@ def _check_and_get_cls_and_meta_hints(


def _should_skip(json_obj: object, cls: type, strict: bool):
if not strict and (json_obj is None or type(json_obj) == cls):
if not strict:
if json_obj is None or type(json_obj) == cls:
return True
if cls is Any:
return True
10 changes: 9 additions & 1 deletion jsons/deserializers/default_primitive.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Optional
from jsons.exceptions import DeserializationError


def default_primitive_deserializer(obj: object,
Expand All @@ -11,4 +12,11 @@ def default_primitive_deserializer(obj: object,
:param kwargs: not used.
:return: ``obj``.
"""
return obj
result = obj
if not isinstance(obj, cls):
try:
result = cls(obj)
except ValueError:
raise DeserializationError('Could not cast {} into {}'
.format(obj, cls.__name__), obj, cls)
return result
3 changes: 2 additions & 1 deletion jsons/deserializers/default_string.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime
from typing import Optional
from jsons._load_impl import load
from jsons.deserializers.default_primitive import default_primitive_deserializer
from jsons.exceptions import DeserializationError


Expand All @@ -18,5 +19,5 @@ def default_string_deserializer(obj: str,
try:
result = load(obj, datetime, **kwargs)
except DeserializationError:
result = obj
result = default_primitive_deserializer(obj, str)
return result
12 changes: 8 additions & 4 deletions jsons/deserializers/default_tuple.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Union
from jsons._compatibility_impl import tuple_with_ellipsis
from jsons._compatibility_impl import tuple_with_ellipsis, get_union_params
from jsons._load_impl import load
from jsons.exceptions import UnfulfilledArgumentError

Expand Down Expand Up @@ -45,10 +45,14 @@ def default_namedtuple_deserializer(
field = obj[key]
else:
field = cls._field_defaults.get(field_name, None)

if field is None:
msg = ('No value present in {} for argument "{}"'
.format(obj, field_name))
raise UnfulfilledArgumentError(msg, field_name, obj, cls)
hint = getattr(cls, '_field_types', {}).get(field_name)
if type(None) not in (get_union_params(hint) or []):
# The value 'None' is not permitted here.
msg = ('No value present in {} for argument "{}"'
.format(obj, field_name))
raise UnfulfilledArgumentError(msg, field_name, obj, cls)
field_types = getattr(cls, '_field_types', None)
cls_ = field_types.get(field_name) if field_types else None
loaded_field = load(field, cls_, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='jsons',
version='0.8.8',
version='0.8.9',
author='Ramon Hagenaars',
author_email='ramon.hagenaars@gmail.com',
description='For serializing Python objects to JSON (dicts) and back',
Expand Down
13 changes: 13 additions & 0 deletions test_resources/version_with_dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid
from dataclasses import dataclass
from typing import NamedTuple, Optional, Union, Any


@dataclass
Expand All @@ -11,3 +12,15 @@ class Person:
class User:
user_uuid: uuid.UUID
name: str


class NamedTupleWithOptional(NamedTuple):
arg: Optional[str]


class NamedTupleWithUnion(NamedTuple):
arg: Union[str, int, None]


class NamedTupleWithAny(NamedTuple):
arg: Any
20 changes: 20 additions & 0 deletions tests/test_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,23 @@ def test_load_none(self):
self.assertEqual(None, jsons.load(None, datetime))
with self.assertRaises(DeserializationError):
jsons.load(None, datetime, strict=True)

def test_load_and_cast(self):

class C:
def __init__(self, x: int):
self.x = x

self.assertEqual(42, jsons.load('42', int))
self.assertEqual(42.0, jsons.load('42', float))
self.assertEqual('42', jsons.load(42, str))
self.assertEqual(True, jsons.load(42, bool))

with self.assertRaises(DeserializationError):
jsons.load('fortytwo', int)

try:
jsons.load('fortytwo', int)
except DeserializationError as err:
self.assertEqual('fortytwo', err.source)
self.assertEqual(int, err.target)
32 changes: 32 additions & 0 deletions tests/test_specific_versions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys
import uuid
from pathlib import Path
from typing import Any
from unittest import TestCase, skipUnless
import jsons
from jsons._compatibility_impl import get_type_hints
Expand Down Expand Up @@ -57,6 +59,23 @@ def test_simple_dump_and_load_dataclass(self):
with self.assertRaises(SignatureMismatchError):
jsons.load({'name': 'John', 'age': 88}, Person, strict=True)

@only_version_3(6, and_above=True)
def test_namedtuple_with_optional(self):
from version_with_dataclasses import (
NamedTupleWithOptional,
NamedTupleWithUnion,
NamedTupleWithAny
)

self.assertEqual(NamedTupleWithOptional(None),
jsons.load({'arg': None}, NamedTupleWithOptional))

self.assertEqual(NamedTupleWithUnion(None),
jsons.load({'arg': None}, NamedTupleWithUnion))

self.assertEqual(NamedTupleWithAny(123),
jsons.load({'arg': 123}, NamedTupleWithAny))

@only_version_3(5, and_above=True)
def test_simple_dump_and_load_dataclass(self):

Expand All @@ -65,3 +84,16 @@ class C:

hints = get_type_hints(C.__init__)
self.assertDictEqual({}, hints)

@only_version_3(6, and_above=True)
def test_uuid_serialization(self):
from version_with_dataclasses import User
user = User(uuid.uuid4(), 'name')

dumped = jsons.dump(user)
self.assertEqual(dumped['user_uuid'], str(user.user_uuid))

loaded = jsons.load(dumped, User)
self.assertEqual(user.user_uuid, loaded.user_uuid)

self.assertEqual('name', loaded.name)
23 changes: 13 additions & 10 deletions tests/test_various.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import uuid
from typing import Optional, NewType
from typing import Optional, NewType, Any
from unittest import TestCase

import jsons
Expand Down Expand Up @@ -80,15 +80,18 @@ def __init__(self, uid: Uid, name: str):
self.assertEqual('uid', loaded.uid)
self.assertEqual('name', loaded.name)

@only_version_3(6, and_above=True)
def test_uuid_serialization(self):
from version_with_dataclasses import User
user = User(uuid.uuid4(), 'name')
def test_any(self):
class C:
def __init__(self, a: Any):
self.a = a

dumped = jsons.dump(user)
self.assertEqual(dumped['user_uuid'], str(user.user_uuid))
loaded = jsons.load({'a': 123}, C)
self.assertEqual(123, loaded.a)

loaded = jsons.load(dumped, User)
self.assertEqual(user.user_uuid, loaded.user_uuid)
def test_nonetype(self):
class C:
def __init__(self, a: type(None)):
self.a = a

self.assertEqual('name', loaded.name)
loaded = jsons.load({'a': None}, C)
self.assertEqual(None, loaded.a)

0 comments on commit 03b5ff9

Please sign in to comment.