Repository: NeurodataWithoutBorders/pynwb

Issue:
FilteredEphys error on read
```python
from pynwb import NWBFile, NWBHDF5IO
from pynwb.ecephys import FilteredEphys, ElectricalSeries
import numpy as np

nwbfile = NWBFile("source", "a file with header data", "NB123A", '2018-06-01T00:00:00')

module_lfp = nwbfile.create_processing_module(
    'lfp', source='source', description='source')

filt_ephys = FilteredEphys(source='source', name='name')

device = nwbfile.create_device('device_name', 'source')
electrode_group = nwbfile.create_electrode_group(
    name='electrode_group_name',
    source='source',
    description='desc',
    device=device,
    location='unknown')

nwbfile.add_electrode(0,
                      np.nan, np.nan, np.nan,  # position?
                      imp=np.nan,
                      location='unknown',
                      filtering='unknown',
                      description='desc',
                      group=electrode_group)

etr = nwbfile.create_electrode_table_region([0], 'etr_name')

for passband in ('theta', 'gamma'):
    electrical_series = ElectricalSeries(name=passband + '_phase',
                                         source='ephys_analysis',
                                         data=[1., 2., 3.],
                                         rate=0.0,
                                         electrodes=etr)
    filt_ephys.add_electrical_series(electrical_series)

module_lfp.add_container(filt_ephys)

with NWBHDF5IO('test.nwb', 'w') as io:
    io.write(nwbfile)

with NWBHDF5IO('test.nwb', 'r') as io:
    io.read()
```

error:
```python
Traceback (most recent call last):
  File "/Users/bendichter/dev/to_nwb/test.py", line 48, in <module>
    io.read()
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/utils.py", line 330, in func_call
    return func(self, **parsed['args'])
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/backends/io.py", line 33, in read
    container = self.__manager.construct(f_builder)
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/utils.py", line 330, in func_call
    return func(self, **parsed['args'])
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/build/map.py", line 178, in construct
    self.__resolve_parents(result)
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/build/map.py", line 188, in __resolve_parents
    tmp.parent = tmp.parent.resolve()
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/build/map.py", line 60, in resolve
    if self.matches(candidate):
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/utils.py", line 330, in func_call
    return func(self, **parsed['args'])
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/build/map.py", line 50, in matches
    obj = self.__manager.get_proxy(obj)
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/utils.py", line 330, in func_call
    return func(self, **parsed['args'])
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/build/map.py", line 99, in get_proxy
    return self.__get_proxy_container(obj)
  File "/Users/bendichter/dev/pynwb/src/pynwb/form/build/map.py", line 117, in __get_proxy_container
    stack.append(tmp.name)
AttributeError: 'Proxy' object has no attribute 'name'
```

if you take out `module_lfp.add_container(filt_ephys)` it runs as expected.

Discussion:

Patch:
diff --git a/src/pynwb/form/build/map.py b/src/pynwb/form/build/map.py
index 5efe6b8e..3de93323 100644
--- a/src/pynwb/form/build/map.py
+++ b/src/pynwb/form/build/map.py
@@ -15,6 +15,16 @@ from .warnings import OrphanContainerWarning, MissingRequiredWarning
 
 
 class Proxy(object):
+    """
+    A temporary object to represent a Container. This gets used when resolving the true location of a
+    Container's parent.
+
+    Proxy objects allow simple bookeeping of all potential parents a Container may have.
+
+    This object is used by providing all the necessary information for describing the object. This object
+    gets passed around and candidates are accumulated. Upon calling resolve, all saved candidates are matched
+    against the information (provided to the constructor). The candidate that has an exact match is returned.
+    """
 
     def __init__(self, manager, source, location, namespace, data_type):
         self.__source = source
@@ -24,24 +34,24 @@ class Proxy(object):
         self.__manager = manager
         self.__candidates = list()
 
-    @property
-    def candidates(self):
-        return self.__candidates
-
     @property
     def source(self):
+        """The source of the object e.g. file source"""
         return self.__source
 
     @property
     def location(self):
+        """The location of the object. This can be thought of as a unique path"""
         return self.__location
 
     @property
     def namespace(self):
+        """The namespace from which the data_type of this Proxy came from"""
         return self.__namespace
 
     @property
     def data_type(self):
+        """The data_type of Container that should match this Proxy"""
         return self.__data_type
 
     @docval({"name": "object", "type": (BaseBuilder, Container), "doc": "the container or builder to get a proxy for"})
@@ -115,8 +125,12 @@ class BuildManager(object):
         stack = list()
         tmp = container
         while tmp is not None:
-            stack.append(tmp.name)
-            tmp = tmp.parent
+            if isinstance(tmp, Proxy):
+                stack.append(tmp.location)
+                break
+            else:
+                stack.append(tmp.name)
+                tmp = tmp.parent
         loc = "/".join(reversed(stack))
         return Proxy(self, container.container_source, loc, ns, dt)
 
@@ -523,6 +537,13 @@ class ObjectMapper(with_metaclass(ExtenderMeta, object)):
             return None
         attr_val = self.__get_override_attr(attr_name, container, manager)
         if attr_val is None:
+            # TODO: A message like this should be used to warn users when an expected attribute
+            # does not exist on a Container object
+            #
+            # if not hasattr(container, attr_name):
+            #     msg = "Container '%s' (%s) does not have attribute '%s'" \
+            #             % (container.name, type(container), attr_name)
+            #     #warnings.warn(msg)
             attr_val = getattr(container, attr_name, None)
             if attr_val is not None:
                 attr_val = self.__convert_value(attr_val, spec)


Test patch:
diff --git a/tests/unit/form_tests/test_io_hdf5_h5tools.py b/tests/unit/form_tests/test_io_hdf5_h5tools.py
index d927f00b..4e126466 100644
--- a/tests/unit/form_tests/test_io_hdf5_h5tools.py
+++ b/tests/unit/form_tests/test_io_hdf5_h5tools.py
@@ -12,6 +12,8 @@ from pynwb.base import TimeSeries
 from pynwb import NWBHDF5IO
 from pynwb.spec import NWBNamespace, NWBGroupSpec, NWBDatasetSpec
 
+from pynwb.ecephys import ElectricalSeries
+
 
 import tempfile
 import warnings
@@ -400,6 +402,47 @@ class TestCacheSpec(unittest.TestCase):
         return types
 
 
+class TestLinkResolution(unittest.TestCase):
+
+    def test_link_resolve(self):
+        print("TEST_LINK_RESOLVE")
+
+        nwbfile = NWBFile("source", "a file with header data", "NB123A", '2018-06-01T00:00:00')
+        device = nwbfile.create_device('device_name', 'source')
+        electrode_group = nwbfile.create_electrode_group(
+            name='electrode_group_name',
+            source='source',
+            description='desc',
+            device=device,
+            location='unknown')
+        nwbfile.add_electrode(0,
+                              1.0, 2.0, 3.0,  # position?
+                              imp=2.718,
+                              location='unknown',
+                              filtering='unknown',
+                              description='desc',
+                              group=electrode_group)
+        etr = nwbfile.create_electrode_table_region([0], 'etr_name')
+        for passband in ('theta', 'gamma'):
+            electrical_series = ElectricalSeries(name=passband + '_phase',
+                                                 source='ephys_analysis',
+                                                 data=[1., 2., 3.],
+                                                 rate=0.0,
+                                                 electrodes=etr)
+            nwbfile.add_acquisition(electrical_series)
+        with NWBHDF5IO(self.path, 'w') as io:
+            io.write(nwbfile)
+        with NWBHDF5IO(self.path, 'r') as io:
+            io.read()
+
+    def setUp(self):
+        self.path = "test_link_resolve.nwb"
+
+    def tearDown(self):
+        if os.path.exists(self.path):
+            os.remove(self.path)
+
+
 class NWBHDF5IOMultiFileTest(unittest.TestCase):
     """Tests for h5tools IO tools"""