text: """
  This module defines :class:`BaseNeo`, the abstract base class
  used by all :module:`neo.core` classes.
  """
  
  from datetime import datetime, date, time, timedelta
  from decimal import Decimal
  import logging
  from numbers import Number
  
  import numpy as np
  
  ALLOWED_ANNOTATION_TYPES = (int, float, complex,
                              str, bytes,
                              type(None),
                              datetime, date, time, timedelta,
                              Number, Decimal,
                              np.number, np.bool_)
  
  # handle both Python 2 and Python 3
  try:
      ALLOWED_ANNOTATION_TYPES += (long, unicode)
  except NameError:
      pass
  
  logger = logging.getLogger("Neo")
  
  
  class MergeError(Exception):
      pass
  
  
  def _check_annotations(value):
      """
      Recursively check that value is either of a "simple" type (number, string,
      date/time) or is a (possibly nested) dict, list or numpy array containing
      only simple types.
      """
      if isinstance(value, np.ndarray):
          if not issubclass(value.dtype.type, ALLOWED_ANNOTATION_TYPES):
              raise ValueError("Invalid annotation. NumPy arrays with dtype %s"
                               "are not allowed" % value.dtype.type)
      elif isinstance(value, dict):
          for element in value.values():
              _check_annotations(element)
      elif isinstance(value, (list, tuple)):
          for element in value:
              _check_annotations(element)
      elif not isinstance(value, ALLOWED_ANNOTATION_TYPES):
          raise ValueError("Invalid annotation. Annotations of type %s are not"
                           "allowed" % type(value))
  
  
  def merge_annotation(a, b):
      """
      First attempt at a policy for merging annotations (intended for use with
      parallel computations using MPI). This policy needs to be discussed
      further, or we could allow the user to specify a policy.
  
      Current policy:
          For arrays or lists: concatenate
          For dicts: merge recursively
          For strings: concatenate with ';'
          Otherwise: fail if the annotations are not equal
      """
      assert type(a) == type(b), 'type({}) {} != type({}) {}'.format(a, type(a),
                                                                 b, type(b))
      if isinstance(a, dict):
          return merge_annotations(a, b)
      elif isinstance(a, np.ndarray):  # concatenate b to a
          return np.append(a, b)
      elif isinstance(a, list):  # concatenate b to a
          return a + b
      elif isinstance(a, str):
          if a == b:
              return a
          else:
              return a + ";" + b
      else:
          assert a == b, '{} != {}'.format(a, b)
          return a
  
  
  def merge_annotations(A, *Bs):
      """
      Merge two sets of annotations.
  
      Merging follows these rules:
      All keys that are in A or B, but not both, are kept.
      For keys that are present in both:
          For arrays or lists: concatenate
          For dicts: merge recursively
          For strings: concatenate with ';'
          Otherwise: warn if the annotations are not equal
      """
      merged = A.copy()
      for B in Bs:
          for name in B:
              if name not in merged:
                  merged[name] = B[name]
              else:
                  try:
                      merged[name] = merge_annotation(merged[name], B[name])
                  except BaseException as exc:
                      # exc.args += ('key %s' % name,)
                      # raise
                      merged[name] = "MERGE CONFLICT"  # temporary hack
      logger.debug("Merging annotations: A=%s Bs=%s merged=%s", A, Bs, merged)
      return merged
  
  
  def _reference_name(class_name):
      """
      Given the name of a class, return an attribute name to be used for
      references to instances of that class.
  
      For example, a Segment object has a parent Block object, referenced by
      `segment.block`. The attribute name `block` is obtained by calling
      `_container_name("Block")`.
      """
      name_map = {
          "ChannelIndex": "channel_index"
      }
      return name_map.get(class_name, class_name.lower())
  
  
  def _container_name(class_name):
      """
      Given the name of a class, return an attribute name to be used for
      lists (or other containers) containing instances of that class.
  
      For example, a Block object contains a list of Segment objects,
      referenced by `block.segments`. The attribute name `segments` is
      obtained by calling `_container_name_plural("Segment")`.
      """
      name_map = {
          "ChannelIndex": "channel_indexes"
      }
      return name_map.get(class_name, _reference_name(class_name) + 's')
  
  
  class BaseNeo:
      """
      This is the base class from which all Neo objects inherit.
  
      This class implements support for universally recommended arguments,
      and also sets up the :attr:`annotations` dict for additional arguments.
  
      Each class can define one or more of the following class attributes:
          :_single_parent_objects: Neo objects that can be parents of this
                                   object. This attribute is used in cases where
                                   only one parent of this class is allowed.
                                   An instance attribute named
                                   class.__name__.lower() will be automatically
                                   defined to hold this parent and will be
                                   initialized to None.
          :_multi_parent_objects: Neo objects that can be parents of this
                                  object. This attribute is used in cases where
                                  multiple parents of this class is allowed.
                                  An instance attribute named
                                  class.__name__.lower()+'s' will be
                                  automatically defined to hold this parent and
                                  will be initialized to an empty list.
          :_necessary_attrs: A list of tuples containing the attributes that the
                             class must have. The tuple can have 2-4 elements.
                             The first element is the attribute name.
                             The second element is the attribute type.
                             The third element is the number of  dimensions
                             (only for numpy arrays and quantities).
                             The fourth element is the dtype of array
                             (only for numpy arrays and quantities).
                             This does NOT include the attributes holding the
                             parents or children of the object.
          :_recommended_attrs: A list of tuples containing the attributes that
                               the class may optionally have.  It uses the same
                               structure as :_necessary_attrs:
          :_repr_pretty_attrs_keys_: The names of attributes printed when
                                     pretty-printing using iPython.
  
      The following helper properties are available:
          :_parent_objects: All parent objects.
                           :_single_parent_objects: + :_multi_parent_objects:
          :_single_parent_containers: The names of the container attributes used
                                     to store :_single_parent_objects:
          :_multi_parent_containers: The names of the container attributes used
                                     to store :_multi_parent_objects:
          :_parent_containers: All parent container attributes.
                              :_single_parent_containers: +
                              :_multi_parent_containers:
          :parents: All objects that are parents of the current object.
          :_all_attrs: All required and optional attributes.
                       :_necessary_attrs: + :_recommended_attrs:
  
      The following "universal" methods are available:
          :__init__: Grabs the universally recommended arguments :attr:`name`,
              :attr:`file_origin`, and :attr:`description` and stores them as
              attributes.
  
              Also takes every additional argument (that is, every argument
              that is not handled by :class:`BaseNeo` or the child class), and
              puts in the dict :attr:`annotations`.
  
          :annotate(**args): Updates :attr:`annotations` with keyword/value
                             pairs.
  
          :merge(**args): Merge the contents of another object into this one.
                          The merge method implemented here only merges
                          annotations (see :merge_annotations:).
                          Subclasses should implementt their own merge rules.
  
          :merge_annotations(**args): Merge the :attr:`annotations` of another
                                      object into this one.
  
      Each child class should:
          0) describe its parents (if any) and attributes in the relevant
             class attributes. :_recommended_attrs: should append
             BaseNeo._recommended_attrs to the end.
          1) call BaseNeo.__init__(self, name=name, description=description,
                                   file_origin=file_origin, **annotations)
             with the universal recommended arguments, plus optional annotations
          2) process its required arguments in its __new__ or __init__ method
          3) process its non-universal recommended arguments (in its __new__ or
             __init__ method
  
      Non-keyword arguments should only be used for required arguments.
  
      The required and recommended arguments for each child class (Neo object)
      are specified in the _necessary_attrs and _recommended_attrs attributes and
      documentation for the child object.
      """
  
      # these attributes control relationships, they need to be
      # specified in each child class
      # Parent objects whose children can have a single parent
      _single_parent_objects = ()
      # Attribute names corresponding to _single_parent_objects
      _single_parent_attrs = ()
      # Parent objects whose children can have multiple parents
      _multi_parent_objects = ()
  
      # Attributes that an instance is requires to have defined
      _necessary_attrs = ()
      # Attributes that an instance may or may have defined
      _recommended_attrs = (('name', str),
                            ('description', str),
                            ('file_origin', str))
      # Attributes that are used for pretty-printing
      _repr_pretty_attrs_keys_ = ("name", "description", "annotations")
  
      def __init__(self, name=None, description=None, file_origin=None,
                   **annotations):
          """
          This is the base constructor for all Neo objects.
  
          Stores universally recommended attributes and creates
          :attr:`annotations` from additional arguments not processed by
          :class:`BaseNeo` or the child class.
          """
          # create `annotations` for additional arguments
          _check_annotations(annotations)
          self.annotations = annotations
  
          # these attributes are recommended for all objects.
          self.name = name
          self.description = description
          self.file_origin = file_origin
  
          # initialize parent containers
          for parent in self._single_parent_containers:
              setattr(self, parent, None)
          for parent in self._multi_parent_containers:
              setattr(self, parent, [])
  
      def annotate(self, **annotations):
          """
          Add annotations (non-standardized metadata) to a Neo object.
  
          Example:
  
          >>> obj.annotate(key1=value0, key2=value1)
          >>> obj.key2
          value2
          """
          _check_annotations(annotations)
          self.annotations.update(annotations)
  
      def _has_repr_pretty_attrs_(self):
          return any(getattr(self, k) for k in self._repr_pretty_attrs_keys_)
  
      def _repr_pretty_attrs_(self, pp, cycle):
          first = True
          for key in self._repr_pretty_attrs_keys_:
              value = getattr(self, key)
              if value:
                  if first:
                      first = False
                  else:
                      pp.breakable()
                  with pp.group(indent=1):
                      pp.text("{}: ".format(key))
                      pp.pretty(value)
  
      def _repr_pretty_(self, pp, cycle):
          """
          Handle pretty-printing the :class:`BaseNeo`.
          """
          pp.text(self.__class__.__name__)
          if self._has_repr_pretty_attrs_():
              pp.breakable()
              self._repr_pretty_attrs_(pp, cycle)
  
      @property
      def _single_parent_containers(self):
          """
          Containers for parent objects whose children can have a single parent.
          """
          return tuple([_reference_name(parent) for parent in
                        self._single_parent_objects])
  
      @property
      def _multi_parent_containers(self):
          """
          Containers for parent objects whose children can have multiple parents.
          """
          return tuple([_container_name(parent) for parent in
                        self._multi_parent_objects])
  
      @property
      def _parent_objects(self):
          """
          All types for parent objects.
          """
          return self._single_parent_objects + self._multi_parent_objects
  
      @property
      def _parent_containers(self):
          """
          All containers for parent objects.
          """
          return self._single_parent_containers + self._multi_parent_containers
  
      @property
      def parents(self):
          """
          All parent objects storing the current object.
          """
          single = [getattr(self, attr) for attr in
                    self._single_parent_containers]
          multi = [list(getattr(self, attr)) for attr in
                   self._multi_parent_containers]
          return tuple(single + sum(multi, []))
  
      @property
      def _all_attrs(self):
          """
          Returns a combination of all required and recommended
          attributes.
          """
          return self._necessary_attrs + self._recommended_attrs
  
      def merge_annotations(self, *others):
          """
          Merge annotations from the other object into this one.
  
          Merging follows these rules:
          All keys that are in the either object, but not both, are kept.
          For keys that are present in both objects:
              For arrays or lists: concatenate the two arrays
              For dicts: merge recursively
              For strings: concatenate with ';'
              Otherwise: fail if the annotations are not equal
          """
          other_annotations = [other.annotations for other in others]
          merged_annotations = merge_annotations(self.annotations,
                                                 *other_annotations)
          self.annotations.update(merged_annotations)
  
      def merge(self, *others):
          """
          Merge the contents of another object into this one.
  
          See :meth:`merge_annotations` for details of the merge operation.
          """
          self.merge_annotations(*others)
  
      def set_parent(self, obj):
          """
          Set the appropriate "parent" attribute of this object
          according to the type of "obj"
          """
          if obj.__class__.__name__ not in self._single_parent_objects:
              raise TypeError("{} can only have parents of type {}, not {}".format(
                  self.__class__.__name__, self._single_parent_objects, obj.__class__.__name__))
          loc = self._single_parent_objects.index(obj.__class__.__name__)
          parent_attr = self._single_parent_attrs[loc]
          setattr(self, parent_attr, obj)
  