Repository: radiasoft/pykern

Issue:
Remove OrderedMapping and lightly used utilities from pkcollections
Search all repos that use pykern (robnagler, mrakitin, etc.)

Discussion:

Patch:
diff --git a/pykern/pkcli/projex.py b/pykern/pkcli/projex.py
index ab3c80d..f804e7c 100644
--- a/pykern/pkcli/projex.py
+++ b/pykern/pkcli/projex.py
@@ -111,15 +111,15 @@ def init_tree(name, author, author_email, description, license, url):
     license = license.lower()
     base = pkresource.filename('projex')
     values = copy.deepcopy(DEFAULTS)
-    values.update({
-        'name': name,
-        'author': author,
-        'description': description,
-        'author_email': author_email,
-        'url': url,
-        'license': _license(license, 0),
-        'classifier_license': _license(license, 1),
-    })
+    values.update(
+        name=name,
+        author=author,
+        description=description,
+        author_email=author_email,
+        url=url,
+        license=_license(license, 0),
+        classifier_license=_license(license, 1),
+    )
     values['copyright_license_rst'] = values['copyright_license_rst'].format(**values)
     suffix_re = r'\.jinja$'
     for src in pkio.walk_tree(base, file_re=suffix_re):
diff --git a/pykern/pkcollections.py b/pykern/pkcollections.py
index 2d5b831..87455e8 100644
--- a/pykern/pkcollections.py
+++ b/pykern/pkcollections.py
@@ -1,18 +1,18 @@
 # -*- coding: utf-8 -*-
-"""Ordered attribute mapping type
+"""PKDict abstraction and utils
 
-Similar to :class:`argparse.Namespace`, but is ordered, and behaves like
-dictionary except there are no public methods on OrderedMapping. All operations
-are operators or Python builtins so that the attribute names from clients of
-a OrderedMapping don't collide.
+`PKDict` is similar to :class:`argparse.Namespace`, but is a dict that allows
+you to treat elements as attributes.
 
-:copyright: Copyright (c) 2015 RadiaSoft LLC.  All Rights Reserved.
+:copyright: Copyright (c) 2015-2020 RadiaSoft LLC.  All Rights Reserved.
 :license: http://www.apache.org/licenses/LICENSE-2.0.html
 """
 from __future__ import absolute_import, division, print_function
 # Limit pykern imports so avoid dependency issues for pkconfig
+import copy
 import json
 
+
 class PKDict(dict):
     """A subclass of dict that allows items to be read/written as attributes.
 
@@ -43,6 +43,17 @@ class PKDict(dict):
     you can't delete attributes at all. Subclasses should be "containers"
     only, not general objects.
     """
+
+    def __copy__(self):
+        return self.__class__(self)
+
+    def __deepcopy__(self, memo):
+        rv = self.__class__()
+        memo[id(self)] = rv
+        for key, value in self.items():
+            rv[copy.deepcopy(key, memo)] = copy.deepcopy(value, memo)
+        return rv
+
     def __delattr__(self, name):
         raise PKDictNameError('{}: you cannot delete attributes', name)
 
@@ -62,7 +73,7 @@ class PKDict(dict):
         super(PKDict, self).__setitem__(name, value)
 
     def copy(self):
-        return self.__class__(self)
+        return self.__copy__()
 
     def pkdel(self, name, default=None):
         """Delete item if exists and return value
@@ -157,29 +168,6 @@ class PKDict(dict):
         super(PKDict, self).update(*args, **kwargs)
         return self
 
-    def setdefault(self, *args, **kwargs):
-        """DEPRECATED"""
-        if len(args) <= 2 and not kwargs:
-            return super(PKDict, self).setdefault(*args)
-        if args:
-            assert len(args) % 2 == 0, \
-                'args must be an even number (pairs of key, value)'
-            assert not kwargs, 'cannot set both args and kwargs'
-            for k, v in zip(args[0::2], args[1::2]):
-                if k not in self:
-                    self[k] = v
-            return self
-        assert kwargs, 'must set args or kwargs'
-        for k, v in kwargs.items():
-            if k not in self:
-                self[k] = v
-        return self
-
-    def update(self, *args, **kwargs):
-        """DEPRECATED"""
-        super(PKDict, self).update(*args, **kwargs)
-        return self
-
 
 class PKDictNameError(NameError):
     """Raised when a key matches a builtin attribute in `dict`."""
@@ -189,117 +177,31 @@ class PKDictNameError(NameError):
 Dict = PKDict
 DictNameError = PKDictNameError
 
-class OrderedMapping(object):
-    """Ordered mapping can be initialized by kwargs or single argument.
-
-    Args:
-        first (object): copy map in order of iterator, OR
-        kwargs (dict): initial values (does not preserver order)
-
-    All operations are munged names to avoid collisions with the clients
-    of OrderedMapping so there are no "methods" on self except operator overloads.
-    """
-    def __init__(self, *args, **kwargs):
-        self.__order = []
-        # Remove __order's mangled name from __order
-        self.__order.pop(0)
-        if args:
-            assert not kwargs, \
-                'May not pass kwargs if passing args'
-            if len(args) == 1:
-                if isinstance(args[0], list):
-                    args = args[0]
-                else:
-                    kwargs = args[0]
-                    args = None
-            if args:
-                if isinstance(args[0], (tuple, list)):
-                    # For json.object_pairs_hook, accept list of 2-tuples
-                    for k, v in args:
-                        setattr(self, k, v)
-                    return
-                if len(args) % 2 != 0:
-                    raise TypeError(
-                        'If mapping type given, must be even number of values')
-                i = iter(args)
-                for k, v in zip(i, i):
-                    setattr(self, k, v)
-                return
-            # If args[0] is not mapping type, then this method
-            # will not fail as it should. The problem is that you
-            # can't test for a mapping type. Sequences implement all
-            # the same functions, just that they don't return the keys
-            # for iterators but the values, which is why ['a'] will
-            # fail as an initializer.
-        for k in kwargs:
-            setattr(self, k, kwargs[k])
-
-    __hash__ = None
-
-    def __contains__(self, key):
-        return key in self.__order
-
-    def __delattr__(self, name):
-        super(OrderedMapping, self).__delattr__(name)
-        self.__order.remove(name)
-
-    def __delitem__(self, key):
-        try:
-            delattr(self, key)
-        except AttributeError:
-            raise KeyError(key)
-
-    def __eq__(self, other):
-        """Type of object, and order of keys and values must be the same"""
-        if not type(self) == type(other):
-            return False
-        # Types must be the same. "__order" is included in vars()
-        # so verifies order, too.
-        return vars(self) == vars(other)
-
-    def __getitem__(self, key):
-        try:
-            return getattr(self, key)
-        except AttributeError:
-            raise KeyError(key)
-
-    def __iter__(self):
-        return iter(self.__order)
-
-    def __len__(self):
-        return len(self.__order)
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def __repr__(self):
-        res = type(self).__name__ + '('
-        if not len(self):
-            return res + ')'
-        for name in self:
-            res += '{!s}={!r}, '.format(name, getattr(self, name))
-        return res[:-2] + ')'
-
-    def __setattr__(self, name, value):
-        super(OrderedMapping, self).__setattr__(name, value)
-        if name not in self.__order:
-            self.__order.append(name)
-
-    def __setitem__(self, key, value):
-        setattr(self, key, value)
-
 
 def json_load_any(obj, *args, **kwargs):
-    """Read json file or str with ``object_pairs_hook=PKDict``
+    """Parse json with `PKDict` for object pairs
 
     Args:
         obj (object): str or object with "read" or py.path
         args (tuple): passed verbatim
-        kwargs (dict): object_pairs_hook overriden
+        kwargs (dict): object_pairs_hook may be overriden
 
     Returns:
         object: parsed JSON
     """
+
+    def object_pairs_hook(*args, **kwargs):
+        """Tries to use `PKDict` if else uses `dict`
+
+        Returns:
+            object: `PKDict` or `dict`
+        """
+        try:
+            return PKDict(*args, **kwargs)
+        except PKDictNameError:
+            return dict(*args, **kwargs)
+
+
     kwargs.setdefault('object_pairs_hook', object_pairs_hook)
     return json.loads(
         obj.read() if hasattr(obj, 'read') else obj,
@@ -308,90 +210,6 @@ def json_load_any(obj, *args, **kwargs):
     )
 
 
-def map_items(value, op=None):
-    """Iterate over mapping, calling op with key, value
-
-    Args:
-        value (object): Any object that implements iteration on keys
-        op (function): called with each key, value, in order
-            (default: return (key, value))
-
-    Returns:
-        list: list of results of op
-    """
-    if not op:
-        return [(k, value[k]) for k in value]
-    return [op(k, value[k]) for k in value]
-
-
-def map_keys(value, op=None):
-    """Iterate over mapping, calling op with key
-
-    Args:
-        value (object): Any object that implements iteration on keys
-        op (function): called with each key, in order (default: return key)
-
-    Returns:
-        list: list of results of op
-    """
-    if not op:
-        return [k for k in value]
-    return [op(k) for k in value]
-
-
-def mapping_merge(base, to_merge):
-    """Add or replace values from to_merge into base
-
-    Args:
-        base (object): Implements setitem
-        to_merge (object): implements iter and getitem
-    Returns:
-        object: base
-    """
-    for k in to_merge:
-        base[k] = to_merge[k]
-    return base
-
-
-def map_to_dict(value):
-    """Convert mapping to dictionary
-
-    Args:
-        value (object): mapping to convert
-
-    Returns:
-        dict: Converted mapping
-    """
-    return dict(map_items(value))
-
-
-def map_values(value, op=None):
-    """Iterate over mapping, calling op with value
-
-    Args:
-        value (object): Any object that implements iteration on values
-        op (function): called with each key, in order (default: return value)
-
-    Returns:
-        list: list of results of op
-    """
-    if not op:
-        return [value[k] for k in value]
-    return [op(value[k]) for k in value]
-
-
-def object_pairs_hook(*args, **kwargs):
-    """Tries to use `PKDict` if else uses `dict`
-
-    Returns:
-        object: `PKDict` or `dict`
-    """
-    try:
-        return PKDict(*args, **kwargs)
-    except PKDictNameError:
-        return dict(*args, **kwargs)
-
-
 def unchecked_del(obj, *keys):
     """Deletes the keys from obj
 
diff --git a/pykern/pkunit.py b/pykern/pkunit.py
index b0d35b1..95b271d 100644
--- a/pykern/pkunit.py
+++ b/pykern/pkunit.py
@@ -204,6 +204,8 @@ def pkexcept(exc_or_re, *fmt_and_args, **kwargs):
 def pkeq(expect, actual, *args, **kwargs):
     """If actual is not expect, throw assertion with calling context.
 
+    Opposite of `pkne`.
+
     Args:
         expect (object): what to test for
         actual (object): run-time value
@@ -230,6 +232,24 @@ def pkfail(fmt, *args, **kwargs):
     raise PKFail('{} {}'.format(call, msg))
 
 
+def pkne(expect, actual, *args, **kwargs):
+    """If actual is equal to expect, throw assertion with calling context
+
+    Opposite of `pkeq`.
+
+    Args:
+        expect (object): what to test for
+        actual (object): run-time value
+        args (tuple): passed to pkfail()
+        kwargs (dict): passed to pkfail()
+    """
+    if expect == actual:
+        if args or kwargs:
+            pkfail(*args, **kwargs)
+        else:
+            pkfail('expect={} == actual={}', expect, actual)
+
+
 def pkok(cond, fmt, *args, **kwargs):
     """If cond is not true, throw PKFail with calling context
 


Test patch:
diff --git a/tests/pkcollections_test.py b/tests/pkcollections_test.py
index a1dc62e..9c21175 100644
--- a/tests/pkcollections_test.py
+++ b/tests/pkcollections_test.py
@@ -8,25 +8,10 @@ from __future__ import absolute_import, division, print_function
 import pytest
 
 
-_VALUE = 1
-
-
-def test_delattr():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping()
-    with pytest.raises(AttributeError):
-        del n.not_there
-    n.there =1
-    del n.there
-    assert not hasattr(n, 'there'), \
-        'del should delete attribute'
-
-
 def test_delitem():
-    from pykern.pkcollections import OrderedMapping
+    from pykern.pkcollections import PKDict
 
-    n = OrderedMapping(a=1)
+    n = PKDict(a=1)
     del n['a']
     assert not hasattr(n, 'a'), \
         'delitem should remove the item'
@@ -102,10 +87,23 @@ def test_dict():
     pkeq('x', n.pkdel('d4', 'x'))
 
 
-def test_dict_nested_get():
-    from pykern import pkcollections
+def test_dict_copy():
     from pykern.pkcollections import PKDict
-    from pykern.pkunit import pkok, pkeq, pkexcept
+    import copy
+    from pykern.pkunit import pkeq, pkne
+
+    n = PKDict(a=1, b=PKDict(c=3))
+    m = copy.copy(n)
+    pkne(id(n), id(m))
+    pkeq(id(n.b), id(n.b))
+    m = copy.deepcopy(n)
+    pkne(id(n), id(m))
+    pkeq(id(n.b), id(n.b))
+
+
+def test_dict_pknested_get():
+    from pykern.pkcollections import PKDict
+    from pykern.pkunit import pkeq, pkexcept
 
     n = PKDict(one=PKDict(two=1.2), simple=1)
     pkeq(1, n.pknested_get('simple'))
@@ -118,84 +116,13 @@ def test_dict_nested_get():
     pkeq(None, n.pkunchecked_nested_get('simple.not'))
 
 
-def test_eq():
-    from pykern.pkcollections import OrderedMapping
-
-    class _OrderedMapping2(OrderedMapping):
-        pass
-
-    assert not OrderedMapping() == None, \
-        'OrderedMapping compared to None is false'
-    assert OrderedMapping() == OrderedMapping(), \
-        'Empty namespaces are equal'
-    assert OrderedMapping() != _OrderedMapping2(), \
-        'OrderedMappings with different types are not equal'
-    assert OrderedMapping() != OrderedMapping(a=1), \
-        'OrderedMappings with different numbers of values are not equal'
-    n, order = _random_init()
-    n2 = OrderedMapping(n)
-    assert n == n2, \
-        'OrderedMappings with same keys and values are equal'
-    assert OrderedMapping(a=1) != OrderedMapping(a=2), \
-        'OrderedMappings with different values are unequal'
-    x = order[0]
-    v = n2[x]
-    del n2[x]
-    n2[x] = v
-    assert n != n2, \
-        'OrderedMappings with different orders are not equal'
-
-
-def test_getitem():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping(a=1)
-    assert 1 == n['a'], \
-        'Extract known element as dict'
-    with pytest.raises(KeyError):
-        if n['b']:
-            pass
-
-
-def test_init():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping()
-    assert [] == _keys(n), \
-        'Empty namespace has no elements'
-    n = OrderedMapping(a=1)
-    with pytest.raises(TypeError):
-        n = OrderedMapping('a', 1, 'b')
-    # Cannot test for OrderedMapping([]) see code
-    with pytest.raises(TypeError):
-        OrderedMapping(['b'])
-    n = OrderedMapping([('x', 1), ('b', 2)])
-    assert ['x', 'b'] == _keys(n), \
-        'Init with list of 2-tuples'
-
-
-def test_init_order():
-    from pykern.pkcollections import OrderedMapping
-
-    order = ['c', 'a', 'd', 'b', 'e', 'g', 'f']
-    values = []
-    for k, v in zip(order, range(len(order))):
-        values.append(k)
-        values.append(v)
-    n = OrderedMapping(values)
-    for i, k in enumerate(n):
-        assert order[i] == k, \
-            '*args keys should be in order of args'
-    n = OrderedMapping(*values)
-    for i, k in enumerate(n):
-        assert order[i] == k, \
-            'args[0] keys should be in order of args'
-
+def test_dict_pkupdate():
+    from pykern.pkcollections import PKDict
+    from pykern.pkunit import pkeq
 
-def test_iter():
-    n, order = _random_init()
-    assert order == _keys(n), \
-        'Order of iteration insertion order'
+    d = PKDict()
+    pkeq(id(d), id(d.pkupdate(a=1)))
+    pkeq(1, d.a)
 
 
 def test_json_load_any():
@@ -216,132 +143,6 @@ def test_json_load_any():
     pkcollections.json_load_any(j, object_pairs_hook=pkcollections.PKDict)
 
 
-def test_len():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping()
-    assert 0 == len(n), \
-        'OrderedMapping should be empty without values'
-    n = OrderedMapping(a=1, b=2)
-    assert 2 == len(n), \
-        'OrderedMappings should have two values'
-
-
-def test_map_items():
-    from pykern import pkcollections
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping(a=1)
-    n.b = 2
-    res = pkcollections.map_items(n, lambda k, v: (v + 1, k))
-    assert [(2, 'a'), (3, 'b')] == res, \
-        'map_items should call op in order'
-    assert [('a', 1), ('b', 2)] == pkcollections.map_items(n), \
-        'map_items should return items with no op'
-
-
-def test_map_keys():
-    from pykern import pkcollections
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping(a=1)
-    n.b = 2
-    res = pkcollections.map_keys(n, lambda k: k * 2)
-    assert ['aa', 'bb'] == res, \
-        'map_keys should call op in order'
-    assert ['a', 'b'] == pkcollections.map_keys(n), \
-        'map_keys should return keys with no op'
-
-
-def test_map_values():
-    from pykern import pkcollections
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping(a=1)
-    n.b = 2
-    res = pkcollections.map_values(n, lambda v: v * 2)
-    assert [2, 4] == res, \
-        'map_values should call op in order'
-    assert [1, 2] == pkcollections.map_values(n), \
-        'map_values should return values with no op'
-
-
-def test_mapping_merge():
-    from pykern import pkcollections
-    from pykern.pkcollections import OrderedMapping
-
-    n, order = _random_init()
-    pkcollections.mapping_merge(n, {})
-    assert list(order) == _keys(n), \
-        'mapping_merge of empty dict should do nothing'
-    pkcollections.mapping_merge(n, OrderedMapping())
-    assert list(order) == _keys(n), \
-        'mapping_merge of empty OrderedMapping should do nothing'
-    n2 = OrderedMapping(n)
-    existing = order[0]
-    new = '!'
-    pkcollections.mapping_merge(n, {existing: 3, new: 4})
-    order += new
-    assert list(order) == _keys(n), \
-        'mapping_merge with dict should replace and add'
-    pkcollections.mapping_merge(n2, OrderedMapping(b=3, c=4))
-    assert order == _keys(n), \
-        'mapping_merge with dict should replace and add'
-
-
-def test_repr():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping()
-    assert 'OrderedMapping()' == repr(n), \
-        'Blank repr should not contain keys'
-    n.a = 1
-    assert 'OrderedMapping(a=1)' == repr(n), \
-        'Single element repr should not have any commas'
-    n, order = _random_init()
-    expect = []
-    for c in order:
-        expect.append('{}={}'.format(c, _VALUE))
-    expect = 'OrderedMapping({})'.format(', '.join(expect))
-    assert expect == repr(n), \
-        'Multiple elements should be in order of insertion'
-
-
-def test_setattr():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping()
-    n.a = 1
-    assert 1 == n.a, \
-        'new att'
-    n.a = 2
-    assert 2 == n.a, \
-        'overwrite attr'
-    n, order = _random_init()
-    assert order == _keys(n), \
-        'Elements should be in order of creation'
-    x = order.pop(3)
-    delattr(n, x)
-    assert order == _keys(n), \
-        'Removed element should not be visible'
-    setattr(n, x, _VALUE)
-    order.append(x)
-    assert order == _keys(n), \
-        'Reinserting element should put it at end'
-
-
-def test_setitem():
-    from pykern.pkcollections import OrderedMapping
-
-    n = OrderedMapping(a=1)
-    n['a'] = 2
-    assert 2 == n['a'], \
-        'Setting a known element should update it'
-    n['b'] = 3
-    assert 3 == n['b'], \
-        'Setting an unknown element should create it'
-
-
 def test_unchecked_del():
     from pykern.pkunit import pkeq
     from pykern import pkcollections
@@ -351,23 +152,3 @@ def test_unchecked_del():
     pkeq({'b': 2, 'c': 3}, n)
     pkcollections.unchecked_del(n, 'a', 'b', 'c')
     pkeq({}, n)
-
-
-def _keys(n):
-    return [k for k in n]
-
-
-def _random_init():
-    from pykern.pkcollections import OrderedMapping
-    import random, string
-
-    n = OrderedMapping()
-    order = ''
-    # ensure all elements are unique
-    while len(order) < 6:
-        c = random.choice(string.ascii_lowercase)
-        if c in order:
-            continue
-        order += c
-        setattr(n, c, _VALUE)
-    return n, list(order)