diff --git a/.gitignore b/.gitignore index 8e87645e..d7548765 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,72 @@ -*.pyo +# Byte-compiled / optimized / DLL files __pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + + + __local__/ machine_guid -/resources/media/Thumbs.db +Thumbs.db .idea/ .DS_Store .vscode/ pyinstrument/ -pyinstrument_cext.so + +# Now managed by templates addon.xml + +*.log diff --git a/jellyfin_kodi/jellyfin/http.py b/jellyfin_kodi/jellyfin/http.py index 67b3edb5..866ac7f8 100644 --- a/jellyfin_kodi/jellyfin/http.py +++ b/jellyfin_kodi/jellyfin/http.py @@ -11,6 +11,7 @@ from six import string_types, ensure_str from helper.utils import JsonDebugPrinter from helper import LazyLogger from helper.exceptions import HTTPException +from jellyfin.utils import clean_none_dict_values ################################################################################################# @@ -161,7 +162,7 @@ class HTTP(object): LOG.debug("---<[ http ][%s ms]", elapsed) LOG.debug(JsonDebugPrinter(response)) - return response + return clean_none_dict_values(response) except ValueError: return diff --git a/jellyfin_kodi/jellyfin/utils.py b/jellyfin_kodi/jellyfin/utils.py new file mode 100644 index 00000000..a2b8f572 --- /dev/null +++ b/jellyfin_kodi/jellyfin/utils.py @@ -0,0 +1,43 @@ +from six import string_types +from six.moves import collections_abc + + +def clean_none_dict_values(obj): + """ + Recursively remove keys with a value of None + """ + if not isinstance(obj, collections_abc.Iterable) or isinstance(obj, string_types): + return obj + + queue = [obj] + + while queue: + item = queue.pop() + + if isinstance(item, collections_abc.Mapping): + mutable = isinstance(item, collections_abc.MutableMapping) + remove = [] + + for key, value in item.items(): + if value is None and mutable: + remove.append(key) + + elif isinstance(value, string_types): + continue + + elif isinstance(value, collections_abc.Iterable): + queue.append(value) + + if mutable: + # Remove keys with None value + for key in remove: + item.pop(key) + + elif isinstance(item, collections_abc.Iterable): + for value in item: + if value is None or isinstance(value, string_types): + continue + elif isinstance(value, collections_abc.Iterable): + queue.append(value) + + return obj diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..265c7918 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,13 @@ +setuptools >= 44.1.1 # Old setuptools causes script.module.addon.signals to fail installing +six >= 1.13 +python-dateutil >= 2.8.1 +requests >= 2.22 +futures >= 2.2; python_version < '3.0' +git+https://github.com/oddstr13/Kodistubs@python3 # Kodistubs >= 18 +git+https://github.com/romanvm/kodi.six +git+https://github.com/ruuk/script.module.addon.signals + +pytest >= 4.6.11 +coverage >= 5.2 +flake8 >= 3.8 +flake8-import-order >= 0.18 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_clean_none_dict_values.py b/tests/test_clean_none_dict_values.py new file mode 100644 index 00000000..9c094fb1 --- /dev/null +++ b/tests/test_clean_none_dict_values.py @@ -0,0 +1,51 @@ +import sys + +import pytest + +sys.path.insert(0, 'jellyfin_kodi') + +from jellyfin.utils import clean_none_dict_values # noqa: E402 + + +@pytest.mark.parametrize("obj,expected", [ + (None, None), + ([None, 1, 2, 3, None, 4], [None, 1, 2, 3, None, 4]), + ({'foo': None, 'bar': 123}, {'bar': 123}), + ({ + 'dict': { + 'empty': None, + 'string': "Hello, Woorld!", + }, + 'number': 123, + 'list': [ + None, + 123, + "foo", + { + 'empty': None, + 'number': 123, + 'string': "foo", + 'list': [], + 'dict': {}, + } + ] + }, { + 'dict': { + 'string': "Hello, Woorld!", + }, + 'number': 123, + 'list': [ + None, + 123, + "foo", + { + 'number': 123, + 'string': "foo", + 'list': [], + 'dict': {}, + } + ] + }), +]) +def test_clean_none_dict_values(obj, expected): + assert clean_none_dict_values(obj) == expected diff --git a/tox.ini b/tox.ini index 41ad1896..ae876e22 100644 --- a/tox.ini +++ b/tox.ini @@ -6,3 +6,19 @@ extend-ignore = I202 per-file-ignores = */__init__.py: F401 + +[pytest] +minversion = 4.6 +testpaths = + tests + +[coverage:run] +source = + jellyfin_kodi + context.py + context_play.py + default.py + service.py + .config/generate_xml.py +omit = tests/* +command_line = -m pytest