diff --git a/cleverdict.egg-info/PKG-INFO b/cleverdict.egg-info/PKG-INFO index d12054b..d66c95d 100644 --- a/cleverdict.egg-info/PKG-INFO +++ b/cleverdict.egg-info/PKG-INFO @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: cleverdict -Version: 1.9.0 +Version: 1.9.1 Summary: A JSON-friendly data structure which allows both object attributes and dictionary keys and values to be used simultaneously and interchangeably. Home-page: https://github.com/Pfython/cleverdict Author: Peter Fison Author-email: peter@southwestlondon.tv License: MIT License -Download-URL: https://github.com/Pfython/cleverdict/archive/1.9.0.tar.gz +Download-URL: https://github.com/Pfython/cleverdict/archive/1.9.1.tar.gz Description: # `CleverDict`

PyPI diff --git a/cleverdict/cleverdict.py b/cleverdict/cleverdict.py index 700531d..c2841f2 100644 --- a/cleverdict/cleverdict.py +++ b/cleverdict/cleverdict.py @@ -13,13 +13,17 @@ Change log ========== +version 1.9.1 +------------- +Handles edge cases handling errors where only=/ignore=/exclude= 0 and 1 (int) + version 1.9.0 ------------- Added exclude= as an alternative for ignore= (for Marshmallow fans) Added only= (for Marshmallow fans) Added exclude= ignore= and only= to __init__ for selective import Made exclude= ignore= and only= permissive (lists OR single item strings) -Refactored using preprocess_options() +Refactored using _preprocess_options() y=CleverDict(x) now imports a fullcopy of x e.g. including _aliases/_vars version 1.8.1 @@ -244,23 +248,23 @@ def _posixify(name): ) -def preprocess_options(ignore, exclude, only): +def _preprocess_options(ignore, exclude, only): """ Performs preparatory transformations of ignore, exclude and only, inculding: - Convert to iterables if single item string supplied. - - Check that only one argument is truthy (to avoid logic bombs). + - Check that only one argument is not None (to avoid logic bombs). - Fail gracefully if not. - Set ignore if exclude was used, and reset exclude (to avoid logic bombs) - Convert ignore to set() neither it nor exclude are specified Parameters ---------- - ignore: None | list + ignore: None | iterable Items to ignore during subsequent processing - exclude: None | list - Alternative alias for ignore - only: None | list + exclude: None | iterable + Alias for ignore + only: None | iterable Items to exclusively include during subsequent processing Returns @@ -270,18 +274,30 @@ def preprocess_options(ignore, exclude, only): """ def make_set(arg): - if arg is None or arg == CleverDict.ignore: + if arg is None: return set() - return set(arg) if not isinstance(arg, str) else {arg} + return ( + set(arg) if hasattr(arg, "__iter__") and not isinstance(arg, str) else {arg} + ) + + if ignore == CleverDict.ignore: + ignore = None + if exclude == CleverDict.ignore: + exclude = None - only = make_set(only) - ignore = make_set(ignore) - exclude = make_set(exclude) - if sum([bool(x) for x in (ignore, exclude, only)]) > 1: + if sum(x is not None for x in (ignore, exclude, only)) > 1: raise TypeError( f"Only one argument from ['only=', 'ignore=', 'exclude='] allowed." ) - return (ignore or exclude) | CleverDict.ignore, only + + if only is not None: # leave only is None for proper only tests + only = ( + set(only) + if hasattr(only, "__iter__") and not isinstance(only, str) + else {only} + ) + + return make_set(ignore) | make_set(exclude) | CleverDict.ignore, only class Expand: @@ -362,7 +378,7 @@ def __init__( exclude=None, **kwargs, ): - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) self.setattr_direct("_aliases", {}) if isinstance(mapping, CleverDict): # for key, alias in mapping._aliases.items(): @@ -383,7 +399,7 @@ def __init__( mapping = {k: v for k, v in mapping.items() if k not in ignore} if isinstance(mapping, list): mapping = {k: v for k, v in mapping if k not in ignore} - if only: + if only is not None: if isinstance(mapping, dict): mapping = {k: v for k, v in mapping.items() if k in only} if isinstance(mapping, list): @@ -456,7 +472,7 @@ def __repr__(self, ignore=None, exclude=None, only=None): only: iterable | str Only return output for specified keys """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) mapping = self._filtered_mapping(ignore, only) _aliases = { k: v for k, v in self._aliases.items() if k not in self and v in mapping @@ -522,7 +538,7 @@ def _filtered_mapping(self, ignore=None, only=False): for k, v in self._aliases.items(): if k in ignore and v in mapping: del mapping[v] - if only: + if only is not None: return {k: v for k, v in mapping.items() if k in only} else: return mapping @@ -568,7 +584,7 @@ def info(self, as_str=False, ignore=None, exclude=None, only=None): information (if as_str is True): str None (if as_str is False) """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) mapping = self._filtered_mapping(ignore, only) indent = " " frame = inspect.currentframe().f_back.f_locals @@ -745,7 +761,7 @@ def to_list(self, ignore=None, exclude=None, only=None): [(1, "one"), (2, "two")] """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) mapping = self._filtered_mapping(ignore, only) return [(k, v) for k, v in mapping.items()] @@ -768,7 +784,7 @@ def to_dict(self, ignore=None, exclude=None, only=None): ------- dict """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) return self._filtered_mapping(ignore=ignore, only=only) @classmethod @@ -800,10 +816,10 @@ def fromkeys(cls, iterable, value, ignore=None, exclude=None, only=None): ------- New CleverDict with keys from iterable and values equal to value. """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) if ignore: iterable = {k: value for k in iterable if k not in ignore} - if only: + if only is not None: iterable = {k: value for k in iterable if k in only} return CleverDict({k: value for k in iterable}) @@ -838,7 +854,7 @@ def to_lines( values joined by "\n" (if file_path is not specified) : str None (if file_path is specified) """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) mapping = self._filtered_mapping(ignore, only) if start_from_key is None: start_from_key = self.get_aliases()[0] @@ -901,7 +917,7 @@ def from_lines( ----- specifying both lines and file_path raises a ValueError """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) if not isinstance(start_from_key, int): raise TypeError(".from_lines(start_from_key=) must be an integer") if lines and file_path: @@ -912,7 +928,7 @@ def from_lines( with open(file_path, "r", encoding="utf-8") as file: lines = file.read() index = {k + start_from_key: v.strip() for k, v in enumerate(lines.split("\n"))} - if only: + if only is not None: index = {k: v for k, v in index.items() if v in only} if ignore: index = {k: v for k, v in index.items() if v not in ignore} @@ -954,7 +970,7 @@ def to_json( Derived only from dictionary data if fullcopy==False Includes ._aliases and ._vars if fullcopy==True """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) mapping = self._filtered_mapping(ignore, only) if not fullcopy: @@ -1010,7 +1026,7 @@ def from_json( ------- New CleverDict: CleverDict """ - ignore, only = preprocess_options(ignore, exclude, only) + ignore, only = _preprocess_options(ignore, exclude, only) kwargs = {"ignore": ignore, "only": only} if json_data and file_path: raise ValueError("both json_data and file_path specified") diff --git a/cleverdict/test_cleverdict.py b/cleverdict/test_cleverdict.py index e15775a..f50d1eb 100644 --- a/cleverdict/test_cleverdict.py +++ b/cleverdict/test_cleverdict.py @@ -265,13 +265,33 @@ def test_only(self): """only=[list] should return output ONLY matching the given keys""" x = CleverDict({"Apples": "Green", "Bananas": "Yellow", "Oranges": "Purple"}) a_and_o = CleverDict({"Apples": "Green", "Oranges": "Purple"}) - for ( - func - ) in "__repr__() to_json() to_dict() to_list() to_lines() info(,as_str=True)".split(): - result1 = eval("x." + func.replace("(", "(only=['Apples', 'Oranges']")) - result2 = eval("a_and_o." + func.replace(",as_str", "as_str")) + for func in "__repr__ to_json to_dict to_list to_lines info".split(): + as_str = {"as_str": True} if func == "info" else {} + result1 = getattr(x, func)(only=["Apples", "Oranges"], **as_str) + result2 = getattr(a_and_o, func)( + **({"as_str": True} if func == "info" else {}) + ) assert str(result1) == str(result2).replace("a_and_o", "x") + def test_only_edge_cases(self): + x = CleverDict({"Apples": "Green", "Bananas": "Yellow", "Oranges": "Purple"}) + with pytest.raises(TypeError): + x.to_list(exclude=[], only=[]) + x.to_list(ignore=[], only=[]) + x.to_list(exclude=[], ignore=[]) + x.to_list(exclude=[], ignore=[], only=[]) + x.to_list( + ignore=CleverDict.ignore, exclude=CleverDict.ignore, only=CleverDict.ignore + ) + + x = CleverDict({0: "Zero", 1: "One"}) + assert x.to_list(only=[1]) == [(1, "One")] + assert x.to_list(ignore=[0]) == [(1, "One")] + assert x.to_list(exclude=[0]) == [(1, "One")] + assert x.to_list(only=1) == [(1, "One")] + assert x.to_list(ignore=0) == [(1, "One")] + assert x.to_list(exclude=0) == [(1, "One")] + def test_permissive(self): """ only= exclude= ignore= should accept iterables AND single items strings. @@ -316,15 +336,6 @@ def test_only_OR_ignore_OR_exclude_as_args(self): with pytest.raises(TypeError): eval("x." + func.replace("(", "(" + args)) - def test_bool_logic_with_bool_keys(self): - """Internal logic of preprocess() might be tricked if single item - (bool) key is supplied""" - x = CleverDict({1: "include me", 0: "exclude/ignore me"}) - with pytest.raises(TypeError): - x.to_list(only=0, ignore=0) - with pytest.raises(TypeError): - x.to_json(ignore=1, exclude=0) - def test_filters_with_init(self): """ only= exclude= ignore= should work as part of object instantiation. @@ -654,7 +665,7 @@ def test_import_existing_cleverdict(test): x = CleverDict({"name": "Peter", "nationality": "British"}) x.add_alias("name", "nom") x.setattr_direct("private", "parts") - x.autosave() + x.autosave(silent=True) y = CleverDict(x) assert y.nom == "Peter" assert y.private == "parts" @@ -1129,6 +1140,7 @@ def test_IMPORT_EXPORT_7(self): ) x.to_json(file_path="mydata.json") y = CleverDict.from_json(file_path="mydata.json") + os.remove("mydata.json") assert x == y def test_IMPORT_EXPORT_8(self): @@ -1318,6 +1330,7 @@ def test_AUTOSAVE_7(self): x.setattr_direct("Quest", "The Holy Grail") x.to_json(file_path="mydata.json", fullcopy=True) y = CleverDict.from_json(file_path="mydata.json") + os.remove("mydata.json") assert y.save.__name__ == "save" y.autosave(fullcopy=True, silent=True) assert y.save.__name__ == "_auto_save_fullcopy" diff --git a/setup.py b/setup.py index 1e950d0..59a0ba0 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ HERE = Path(__file__).parent NAME = "cleverdict" GITHUB_ID = "Pfython" -VERSION = "1.9.0" +VERSION = "1.9.1" DESCRIPTION = "A JSON-friendly data structure which allows both object attributes and dictionary keys and values to be used simultaneously and interchangeably." LICENSE = "MIT License" AUTHOR = "Peter Fison"