Repository: scikit-hep/awkward

Issue:
User-supplied behaviors shouldn't replace ak.behavior; they should be merged with it.
In this sense:

```python
behavior = dict(ak.behavior)
behavior.update(user_supplied)
```

Although that might be a lot of dict-copying every time an `ak.Array` gets constructed...


Discussion:
I think it's a shallow copy (could be made explicit with copy.copy(ak.behavior), and you could skip this step if `user_supplied` is None (assuming you are then okay with changing ak.behavior also changing here - otherwise you always need a copy).

Resolving patch:
diff --git a/src/awkward1/_util.py b/src/awkward1/_util.py
--- a/src/awkward1/_util.py
+++ b/src/awkward1/_util.py
@@ -7,6 +7,10 @@
 import re
 import sys
 import os
+try:
+    from collections.abc import Mapping
+except ImportError:
+    from collections import Mapping
 
 import numpy
 
@@ -27,84 +31,110 @@
 
 recordtypes = (awkward1.layout.RecordArray,)
 
+class Behavior(Mapping):
+    def __init__(self, defaults, overrides):
+        self.defaults = defaults
+        if overrides is None:
+            self.overrides = {}
+        else:
+            self.overrides = overrides
+
+    def __getitem__(self, where):
+        try:
+            return self.overrides[where]
+        except KeyError:
+            try:
+                return self.defaults[where]
+            except KeyError:
+                return None
+
+    def items(self):
+        for n, x in self.overrides.items():
+            yield n, x
+        for n, x in self.defaults.items():
+            if n not in self.overrides:
+                yield n, x
+
+    def __iter__(self):
+        for n, x in self.items():
+            yield n
+
+    def __len__(self):
+        return len(set(self.defaults) | set(self.overrides))
+
 def arrayclass(layout, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     arr = layout.parameter("__array__")
     if isinstance(arr, str) or (py27 and isinstance(arr, unicode)):
-        cls = behavior.get(arr)
+        cls = behavior[arr]
         if isinstance(cls, type) and issubclass(cls, awkward1.highlevel.Array):
             return cls
     rec = layout.parameter("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
-        cls = behavior.get((".", rec))
+        cls = behavior[".", rec]
         if isinstance(cls, type) and issubclass(cls, awkward1.highlevel.Array):
             return cls
     deeprec = layout.purelist_parameter("__record__")
     if isinstance(deeprec, str) or (py27 and isinstance(deeprec, unicode)):
-        cls = behavior.get(("*", deeprec))
+        cls = behavior["*", deeprec]
         if isinstance(cls, type) and issubclass(cls, awkward1.highlevel.Array):
             return cls
     return awkward1.highlevel.Array
 
 def numba_array_typer(layouttype, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     arr = layouttype.parameters.get("__array__")
     if isinstance(arr, str) or (py27 and isinstance(arr, unicode)):
-        typer = behavior.get(("__numba_typer__", arr))
+        typer = behavior["__numba_typer__", arr]
         if callable(typer):
             return typer
     rec = layouttype.parameters.get("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
-        typer = behavior.get(("__numba_typer__", ".", rec))
+        typer = behavior["__numba_typer__", ".", rec]
         if callable(typer):
             return typer
     deeprec = layouttype.parameters.get("__record__")
     if isinstance(deeprec, str) or (py27 and isinstance(deeprec, unicode)):
-        typer = behavior.get(("__numba_typer__", "*", deeprec))
+        typer = behavior["__numba_typer__", "*", deeprec]
         if callable(typer):
             return typer
     return None
 
 def numba_array_lower(layouttype, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     arr = layouttype.parameters.get("__array__")
     if isinstance(arr, str) or (py27 and isinstance(arr, unicode)):
-        lower = behavior.get(("__numba_lower__", arr))
+        lower = behavior["__numba_lower__", arr]
         if callable(lower):
             return lower
     rec = layouttype.parameters.get("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
-        lower = behavior.get(("__numba_lower__", ".", rec))
+        lower = behavior["__numba_lower__", ".", rec]
         if callable(lower):
             return lower
     deeprec = layouttype.parameters.get("__record__")
     if isinstance(deeprec, str) or (py27 and isinstance(deeprec, unicode)):
-        lower = behavior.get(("__numba_lower__", "*", deeprec))
+        lower = behavior["__numba_lower__", "*", deeprec]
         if callable(lower):
             return lower
     return None
 
 def recordclass(layout, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     rec = layout.parameter("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
-        cls = behavior.get(rec)
+        cls = behavior[rec]
         if isinstance(cls, type) and issubclass(cls, awkward1.highlevel.Record):
             return cls
     return awkward1.highlevel.Record
 
 def typestrs(behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     out = {}
     for key, typestr in behavior.items():
         if isinstance(key, tuple) and len(key) == 2 and key[0] == "__typestr__" and (isinstance(key[1], str) or (py27 and isinstance(key[1], unicode))) and (isinstance(typestr, str) or (py27 and isinstance(typestr, unicode))):
@@ -113,59 +143,53 @@ def typestrs(behavior):
 
 def numba_record_typer(layouttype, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     rec = layouttype.parameters.get("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
-        typer = behavior.get(("__numba_typer__", rec))
+        typer = behavior["__numba_typer__", rec]
         if callable(typer):
             return typer
     return None
 
 def numba_record_lower(layouttype, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     rec = layouttype.parameters.get("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
-        lower = behavior.get(("__numba_lower__", rec))
+        lower = behavior["__numba_lower__", rec]
         if callable(lower):
             return lower
     return None
 
 def overload(behavior, signature):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
-    return behavior.get(signature)
+    behavior = Behavior(awkward1.behavior, behavior)
+    return behavior[signature]
 
 def numba_attrs(layouttype, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     rec = layouttype.parameters.get("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
         for key, typer in behavior.items():
             if isinstance(key, tuple) and len(key) == 3 and key[0] == "__numba_typer__" and key[1] == rec:
-                lower = behavior.get(("__numba_lower__", key[1], key[2]))
+                lower = behavior["__numba_lower__", key[1], key[2]]
                 yield key[2], typer, lower
 
 def numba_methods(layouttype, behavior):
     import awkward1
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     rec = layouttype.parameters.get("__record__")
     if isinstance(rec, str) or (py27 and isinstance(rec, unicode)):
         for key, typer in behavior.items():
             if isinstance(key, tuple) and len(key) == 4 and key[0] == "__numba_typer__" and key[1] == rec and key[3] == ():
-                lower = behavior.get(("__numba_lower__", key[1], key[2], ()))
+                lower = behavior["__numba_lower__", key[1], key[2], ()]
                 yield key[2], typer, lower
 
 def numba_unaryops(unaryop, left, behavior):
     import awkward1
     import awkward1._numba.layout
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     done = False
 
     if isinstance(left, awkward1._numba.layout.ContentType):
@@ -176,14 +200,13 @@ def numba_unaryops(unaryop, left, behavior):
     if not done:
         for key, typer in behavior.items():
             if isinstance(key, tuple) and len(key) == 3 and key[0] == "__numba_typer__" and key[1] == unaryop and key[2] == left:
-                lower = behavior.get(("__numba_lower__", key[1], key[2]))
+                lower = behavior["__numba_lower__", key[1], key[2]]
                 yield typer, lower
 
 def numba_binops(binop, left, right, behavior):
     import awkward1
     import awkward1._numba.layout
-    if behavior is None:
-        behavior = awkward1.behavior
+    behavior = Behavior(awkward1.behavior, behavior)
     done = False
 
     if isinstance(left, awkward1._numba.layout.ContentType):
@@ -199,7 +222,7 @@ def numba_binops(binop, left, right, behavior):
     if not done:
         for key, typer in behavior.items():
             if isinstance(key, tuple) and len(key) == 4 and key[0] == "__numba_typer__" and key[1] == left and key[2] == binop and key[3] == right:
-                lower = behavior.get(("__numba_lower__", key[1], key[2], key[3]))
+                lower = behavior["__numba_lower__", key[1], key[2], key[3]]
                 yield typer, lower
 
 def behaviorof(*arrays):
diff --git a/src/awkward1/highlevel.py b/src/awkward1/highlevel.py
--- a/src/awkward1/highlevel.py
+++ b/src/awkward1/highlevel.py
@@ -85,7 +85,7 @@ def __setitem__(self, where, what):
         self._numbaview = None
 
     def __getattr__(self, where):
-        if where in dir(super(Array, self)):
+        if where in dir(type(self)):
             return super(Array, self).__getattribute__(where)
         else:
             if where in self._layout.keys():
@@ -222,7 +222,7 @@ def __setitem__(self, where, what):
         self._numbaview = None
 
     def __getattr__(self, where):
-        if where in dir(super(Record, self)):
+        if where in dir(type(self)):
             return super(Record, self).__getattribute__(where)
         else:
             if where in self._layout.keys():


Test patch:
diff --git a/tests/test_0150-attributeerrors.py b/tests/test_0150-attributeerrors.py
new file mode 100644
--- /dev/null
+++ b/tests/test_0150-attributeerrors.py
@@ -0,0 +1,26 @@
+# BSD 3-Clause License; see https://github.com/jpivarski/awkward-1.0/blob/master/LICENSE
+
+from __future__ import absolute_import
+
+import sys
+
+import pytest
+import numpy
+
+import awkward1
+
+class Dummy(awkward1.Record):
+    @property
+    def broken(self):
+        raise AttributeError("I'm broken!")
+
+def test():
+    behavior = {}
+    behavior["Dummy"] = Dummy
+
+    array = awkward1.Array([{"x": 1}, {"x": 2}, {"x": 3}], behavior=behavior)
+    array.layout.setparameter("__record__", "Dummy")
+
+    with pytest.raises(AttributeError) as err:
+        array[1].broken
+    assert str(err.value) == "I'm broken!"   # not "no field named 'broken'"