Bug 1875997 - Review Request: python-jsons - Python library for (de)serializing objects to/from JSON
Summary: Review Request: python-jsons - Python library for (de)serializing objects to/...
Keywords:
Status: CLOSED ERRATA
Alias: None
Product: Fedora
Classification: Fedora
Component: Package Review
Version: rawhide
Hardware: All
OS: Linux
medium
medium
Target Milestone: ---
Assignee: Andy Mender
QA Contact: Fedora Extras Quality Assurance
URL:
Whiteboard:
Depends On: 1875996
Blocks: 1876006
TreeView+ depends on / blocked
 
Reported: 2020-09-04 20:25 UTC by Fabian Affolter
Modified: 2021-01-25 08:10 UTC (History)
2 users (show)

Fixed In Version:
Doc Type: If docs needed, set a value
Doc Text:
Clone Of:
Environment:
Last Closed: 2020-12-06 03:04:36 UTC
Type: ---
Embargoed:
andymenderunix: fedora-review+


Attachments (Terms of Use)

Description Fabian Affolter 2020-09-04 20:25:11 UTC
Spec URL: https://fab.fedorapeople.org/packages/SRPMS/python-jsons.spec
SRPM URL: https://fab.fedorapeople.org/packages/SRPMS/python-jsons-1.2.0-1.fc32.src.rpm

Project URL: https://github.com/ramonhagenaars/jsons

Description:
Jsons is a library that allows you to serialize your plain old Python
objects to readable json (dicts or strings) and deserialize them back.
No magic, no special types, no polluting your objects.

Koji scratch build:
fails due to missing dependency

rpmlint output:
$ rpmlint python-jsons-1.2.0-1.fc32.src.rpm
python-jsons.src: W: spelling-error Summary(en_US) de -> DE, ed, d
python-jsons.src: W: spelling-error %description -l en_US json -> son, j son, soon
python-jsons.src: W: spelling-error %description -l en_US dicts -> ducts, dicta, dict
python-jsons.src: W: spelling-error %description -l en_US deserialize -> serialize, desalinize
1 packages and 0 specfiles checked; 0 errors, 4 warnings.

$ rpmlint python3-jsons-1.2.0-1.fc32.noarch.rpm 
python3-jsons.noarch: W: spelling-error Summary(en_US) de -> DE, ed, d
python3-jsons.noarch: W: spelling-error %description -l en_US json -> son, j son, soon
python3-jsons.noarch: W: spelling-error %description -l en_US dicts -> ducts, dicta, dict
python3-jsons.noarch: W: spelling-error %description -l en_US deserialize -> serialize, desalinize
1 packages and 0 specfiles checked; 0 errors, 4 warnings.

Fedora Account System Username: fab

Comment 1 Andy Mender 2020-09-05 14:23:23 UTC
> Koji scratch build:
> fails due to missing dependency

Yes, the following needs to be added next to the existing BuildRequires:
BuildRequires:	python3dist(typish)

Tested in COPR: https://copr.fedorainfracloud.org/coprs/andymenderunix/metrics2mqtt-and-dependencies/build/1650399/

There is quite a lot of tests failing, however. I'll look into it.

=================================== FAILURES ===================================
____________ TestSpecificVersions.test_dump_dataclass_with_optional ____________

args = (<tests.test_specific_versions.TestSpecificVersions testMethod=test_dump_dataclass_with_optional>,)
kwargs = {}

    @skipUnless(dont_skip, reason=reason)
    def _wrapper(*args, **kwargs):
>       return decorated(*args, **kwargs)

tests/test_specific_versions.py:31: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tests/test_specific_versions.py:88: in test_dump_dataclass_with_optional
    self.assertDictEqual(expected, dumped)
E   AssertionError: {'x': [42, None, 123]} != {}
E   - {'x': [42, None, 123]}
E   + {}
______________ TestSpecificVersions.test_namedtuple_with_optional ______________

args = (<tests.test_specific_versions.TestSpecificVersions testMethod=test_namedtuple_with_optional>,)
kwargs = {}

    @skipUnless(dont_skip, reason=reason)
    def _wrapper(*args, **kwargs):
>       return decorated(*args, **kwargs)

tests/test_specific_versions.py:31: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
tests/test_specific_versions.py:74: in test_namedtuple_with_optional
    jsons.load({'arg': None}, NamedTupleWithOptional))
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
jsons/_load_impl.py:110: in _do_load
    result = deserializer(json_obj, cls, **kwargs)
jsons/deserializers/default_tuple.py:23: in default_tuple_deserializer
    return default_namedtuple_deserializer(obj, cls, key_transformer=key_transformer, **kwargs)
jsons/deserializers/default_tuple.py:80: in default_namedtuple_deserializer
    loaded_field = load(field, cls_, key_transformer=key_transformer, **kwargs)
jsons/_load_impl.py:75: in load
    _check_for_none(json_obj, cls)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

json_obj = None, cls = typing.Optional[str]

    def _check_for_none(json_obj: object, cls: type):
        # Check if the json_obj is None and whether or not that is fine.
        if json_obj is None and not can_match_with_none(cls):
            cls_name = get_class_name(cls).lower()
>           raise DeserializationError(
                message='NoneType cannot be deserialized into {}'.format(cls_name),
                source=json_obj,
                target=cls)
E           jsons.exceptions.DeserializationError: NoneType cannot be deserialized into optional

jsons/_load_impl.py:213: DeserializationError
_________________________ TestUnion.test_load_optional _________________________

self = <tests.test_union.TestUnion testMethod=test_load_optional>

    def test_load_optional(self):
        class TestOptionalInt:
            def __init__(self, value: Optional[int]):
                self.value = value
    
        # This seems fine.
>       loaded1 = jsons.load({'value': 42}, cls=TestOptionalInt)

tests/test_union.py:102: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
jsons/_load_impl.py:110: in _do_load
    result = deserializer(json_obj, cls, **kwargs)
jsons/deserializers/default_object.py:39: in default_object_deserializer
    constructor_args = _get_constructor_args(obj, cls, **kwargs)
jsons/deserializers/default_object.py:63: in _get_constructor_args
    key, value = _get_value_for_attr(obj=obj,
jsons/deserializers/default_object.py:93: in _get_value_for_attr
    result = sig_key, _get_value_from_obj(obj, cls, sig, sig_key,
jsons/deserializers/default_object.py:134: in _get_value_from_obj
    value = load(obj[sig_key], cls_, meta_hints=new_hints, **kwargs)
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

json_obj = 42, deserializer = None, cls = typing.Optional[int], initial = False
kwargs = {'_initial': False, 'attr_getters': None, 'fork_inst': <class 'jsons._common_impl.StateHolder'>, 'meta_hints': {}, ...}
cls_name = 'typing.Optional'

    def _do_load(json_obj: object,
                 deserializer: callable,
                 cls: type,
                 initial: bool,
                 **kwargs):
        cls_name = get_class_name(cls, fully_qualified=True)
        if deserializer is None:
>           raise DeserializationError('No deserializer for type "{}"'.format(cls_name), json_obj, cls)
E           jsons.exceptions.DeserializationError: No deserializer for type "typing.Optional"

jsons/_load_impl.py:108: DeserializationError
__________________________ TestUnion.test_load_union ___________________________

self = <tests.test_union.TestUnion testMethod=test_load_union>

    def test_load_union(self):
        class A:
            def __init__(self, x):
                self.x = x
    
        class B:
            def __init__(self, x: Optional[int]):
                self.x = x
    
        class C:
            def __init__(self, x: Union[datetime.datetime, A]):
                self.x = x
    
        # Test loading with None value without type hint.
        self.assertEqual(None, jsons.load({'x': None}, A).x)
    
        # Test Optional with a value.
>       self.assertEqual(1, jsons.load({'x': 1}, B).x)

tests/test_union.py:63: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
jsons/_load_impl.py:110: in _do_load
    result = deserializer(json_obj, cls, **kwargs)
jsons/deserializers/default_object.py:39: in default_object_deserializer
    constructor_args = _get_constructor_args(obj, cls, **kwargs)
jsons/deserializers/default_object.py:63: in _get_constructor_args
    key, value = _get_value_for_attr(obj=obj,
jsons/deserializers/default_object.py:93: in _get_value_for_attr
    result = sig_key, _get_value_from_obj(obj, cls, sig, sig_key,
jsons/deserializers/default_object.py:134: in _get_value_from_obj
    value = load(obj[sig_key], cls_, meta_hints=new_hints, **kwargs)
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

json_obj = 1, deserializer = None, cls = typing.Optional[int], initial = False
kwargs = {'_initial': False, 'attr_getters': None, 'fork_inst': <class 'jsons._common_impl.StateHolder'>, 'meta_hints': {}, ...}
cls_name = 'typing.Optional'

    def _do_load(json_obj: object,
                 deserializer: callable,
                 cls: type,
                 initial: bool,
                 **kwargs):
        cls_name = get_class_name(cls, fully_qualified=True)
        if deserializer is None:
>           raise DeserializationError('No deserializer for type "{}"'.format(cls_name), json_obj, cls)
E           jsons.exceptions.DeserializationError: No deserializer for type "typing.Optional"

jsons/_load_impl.py:108: DeserializationError
__________________ TestVarious.test_load_recursive_structure ___________________

self = <tests.test_various.TestVarious testMethod=test_load_recursive_structure>

    def test_load_recursive_structure(self):
        source = {
            'value': 10,
            'next': {
                'value': 20,
                'next': {
                    'value': 30,
                    'next': None
                }
            }
        }
>       loaded = jsons.load(source, Node)

tests/test_various.py:53: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
jsons/_load_impl.py:110: in _do_load
    result = deserializer(json_obj, cls, **kwargs)
jsons/deserializers/default_object.py:39: in default_object_deserializer
    constructor_args = _get_constructor_args(obj, cls, **kwargs)
jsons/deserializers/default_object.py:63: in _get_constructor_args
    key, value = _get_value_for_attr(obj=obj,
jsons/deserializers/default_object.py:93: in _get_value_for_attr
    result = sig_key, _get_value_from_obj(obj, cls, sig, sig_key,
jsons/deserializers/default_object.py:134: in _get_value_from_obj
    value = load(obj[sig_key], cls_, meta_hints=new_hints, **kwargs)
jsons/_load_impl.py:98: in load
    return _do_load(json_obj, deserializer, cls, initial, **kwargs_)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

json_obj = {'next': {'next': None, 'value': 30}, 'value': 20}
deserializer = None, cls = typing.Optional[tests.test_various.Node]
initial = False
kwargs = {'_initial': False, 'attr_getters': None, 'fork_inst': <class 'jsons._common_impl.StateHolder'>, 'meta_hints': {}, ...}
cls_name = 'typing.Optional'

    def _do_load(json_obj: object,
                 deserializer: callable,
                 cls: type,
                 initial: bool,
                 **kwargs):
        cls_name = get_class_name(cls, fully_qualified=True)
        if deserializer is None:
>           raise DeserializationError('No deserializer for type "{}"'.format(cls_name), json_obj, cls)
E           jsons.exceptions.DeserializationError: No deserializer for type "typing.Optional"

jsons/_load_impl.py:108: DeserializationError

Comment 2 Andy Mender 2020-09-05 14:38:02 UTC
I submitted a ticket with upstream: https://github.com/ramonhagenaars/jsons/issues/115

Comment 3 Fabian Affolter 2020-09-08 08:23:32 UTC
(In reply to Andy Mender from comment #1)
> > Koji scratch build:
> > fails due to missing dependency
> 
> Yes, the following needs to be added next to the existing BuildRequires:
> BuildRequires:	python3dist(typish)

Added

%changelog
* Tue Sep 08 2020 Fabian Affolter <mail> - 1.2.0-2
- Add missing BR (rhbz#1875997)

Updated files:
Spec URL: https://fab.fedorapeople.org/packages/SRPMS/python-jsons.spec
SRPM URL: https://fab.fedorapeople.org/packages/SRPMS/python-jsons-1.2.0-2.fc32.src.rpm

Comment 4 Andy Mender 2020-09-09 19:42:42 UTC
> Added

Thumbs up!

Honestly, I'm not quite sure how to proceed with this, however. I rebuilt the package in COPR again: https://copr.fedorainfracloud.org/coprs/andymenderunix/metrics2mqtt-and-dependencies/build/1654723/
Have a look at the logs specifically.

The reason why the build fails on Fedora 32 is because one of the tests shows lower than expected performance: https://github.com/ramonhagenaars/jsons/blob/master/tests/test_performance.py#L44
Below an example on aarch64. It's a lot worse on s390 (94.922601 seconds) and armhfp (78.928779 seconds). Out of these aarch64, armhfp and x86_64 are relevant.
> =================================== FAILURES ===================================
> __________________________ TestPerformance.test_dump ___________________________
> self = <tests.test_performance.TestPerformance testMethod=test_dump>
>     def test_dump(self):
> >       self._do_test_dump(16)
> tests/test_performance.py:45: 
> _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
> tests/test_performance.py:67: in _do_test_dump
>     self.assertTrue(delta_sec3 < time_limit, 'The operation took {} seconds'.format(delta_sec3))
> E   AssertionError: False is not true : The operation took 16.243726 seconds

I'm pretty sure we could argue with upstream to up the number here, since it's arbitrary anyway. Still, the differences in timings are huge.

Fedora 33 and Rawhide have bigger problems due to the switch to Python 3.9. Either the test cases need to be revisited or the code they cover. I can have a look over the weekend to maybe file some PRs, but no earlier.

Comment 5 Fabian Affolter 2020-09-15 12:58:51 UTC
Locally the performance tests are passing but are slowing down the build process with no real value for us.

%changelog
* Mon Sep 14 2020 Fabian Affolter <mail> - 1.2.0-3
- Make performance tests optional

Updated files:
Spec URL: https://fab.fedorapeople.org/packages/SRPMS/python-jsons.spec
SRPM URL: https://fab.fedorapeople.org/packages/SRPMS/python-jsons-1.2.0-3.fc32.src.rpm

Comment 6 Andy Mender 2020-09-15 19:23:24 UTC
> Locally the performance tests are passing but are slowing down the build process with no real value for us.
>
> %changelog
> * Mon Sep 14 2020 Fabian Affolter <mail> - 1.2.0-3
> - Make performance tests optional

Yup, these can be optional. Good idea!

Since there is still no news from upstream on the ticket I opened, I'll try to work on a patch in the following days.

Comment 7 Andy Mender 2020-09-30 18:58:13 UTC
Quick update, unfortunately, I couldn't find a fix. It seems the tests for python-jsons are running into some corner cases in the 'typing' library. The Python docs don't mention any changes in Python 3.9 so it's hard to tell why the tests could be failing. Might as well be a bug in the 'typing' library.

Comment 8 Fabian Affolter 2020-11-08 12:57:07 UTC
Could we go on with the excluded performance tests?

Comment 9 Andy Mender 2020-11-09 21:03:47 UTC
> Could we go on with the excluded performance tests?

I don't think that's a good idea. The performance tests can be disabled, yes. However, the remaining tests touch on key functionalities of the "python-jsons" library and fail on Python 3.8 and Python 3.9.

I received a response to my ticket with upstream a while back so please have a look whether the most recent release builds cleanly on Rawhide (and optionally F33 if you would like to cover that release, too).

Comment 10 Andy Mender 2020-11-15 16:36:01 UTC
Good news! I tested the most recent release in COPR and the issues with Python 3.8 and 3.9, and python-typish have been resolved! https://copr.fedorainfracloud.org/coprs/andymenderunix/python-iot/build/1769669/

You can go ahead, update to version 1.3.0 (https://github.com/ramonhagenaars/jsons/releases/tag/v1.3.0) and disable the performance test "TestPerformance.test_dump".

Package approved!

Comment 11 Andy Mender 2020-11-24 20:21:21 UTC
I see the repo request was not submitted yet. Need any help? :)

Comment 12 Andy Mender 2020-11-24 20:22:41 UTC
I think this can be pushed already, since the issues were squared away. Any help needed?

Comment 13 Gwyn Ciesla 2020-11-27 03:39:22 UTC
(fedscm-admin):  The Pagure repository was created at https://src.fedoraproject.org/rpms/python-jsons

Comment 14 Fedora Update System 2020-11-27 09:28:41 UTC
FEDORA-2020-1a3b7045f0 has been submitted as an update to Fedora 33. https://bodhi.fedoraproject.org/updates/FEDORA-2020-1a3b7045f0

Comment 15 Fedora Update System 2020-11-28 02:55:26 UTC
FEDORA-2020-1a3b7045f0 has been pushed to the Fedora 33 testing repository.
In short time you'll be able to install the update with the following command:
`sudo dnf install --enablerepo=updates-testing --advisory=FEDORA-2020-1a3b7045f0 \*`
You can provide feedback for this update here: https://bodhi.fedoraproject.org/updates/FEDORA-2020-1a3b7045f0

See also https://fedoraproject.org/wiki/QA:Updates_Testing for more information on how to test updates.

Comment 16 Fedora Update System 2020-12-06 03:04:36 UTC
FEDORA-2020-1a3b7045f0 has been pushed to the Fedora 33 stable repository.
If problem still persists, please make note of it in this bug report.


Note You need to log in before you can comment on or make changes to this bug.