text: import sys
  
  from .errors import SpecError, ReferenceResolutionError
  
  IS_PYTHON_2 = False
  if sys.version_info[0] == 2:
      IS_PYTHON_2 = True
  else:
      # unicode was removed in python3, but we need to support both here, so define
      # it in python 3 only
      unicode = str
  
  
  def _asdict(x):
      if hasattr(x, '__getstate__'):
          return x.__getstate__()
      elif isinstance(x, dict):
          return {k: _asdict(v) for k, v in x.items()}
      elif isinstance(x, (list, tuple, set)):
          return x.__class__(_asdict(y) for y in x)
      else:
          return x
  
  
  def raise_on_unknown_type(parent, field, object_types, found):
      """
      Raises a SpecError describing a situation where an unknown type was given.
      This function attempts to produce as useful an error as possible based on the
      type of types that were expected.
  
      :param parent: The parent element who was attempting to parse the field
      :type parent: Subclass of ObjectBase
      :param field: The field we were trying to parse
      :type field: str
      :param object_types: The types allowed for this field
      :type object_types: List of str or Class
      :param found: The value that was found (and did not match any expected type)
      :type found: any
  
      :raises: A SpecError describing the failure
      """
      if len(object_types) == 1:
          if isinstance(object_types[0], str):
              expected_type = ObjectBase.get_object_type(object_types[0])
              raise SpecError('Expected {}.{} to be of type {}, with required fields {}'.format(
                      parent.get_path(),
                      field,
                      object_types[0],
                      expected_type.required_fields,
                  ),
                  path=parent.path,
                  element=parent,
              )
      elif len(object_types) == 2 and len([c for c in object_types if isinstance(c, str)]) == 2 and "Reference" in object_types:
          # we can give a similar error here as above
          expected_type_str = [c for c in object_types if c != "Reference"][0]
          expected_type = ObjectBase.get_object_type(expected_type_str)
          raise SpecError("Expected {}.{} to be of type {} or Reference, but did not find required fields {} or '$ref'".format(
                  parent.get_path(),
                  field,
                  expected_type_str,
                  expected_type.required_fields,
              ),
              path=parent.path,
              element=parent,
          )
      print(object_types)
      raise SpecError('Expected {}.{} to be one of [{}], got {}'.format(
                  parent.get_path(),
                  field,
                  ','.join([str(c) for c in object_types],
              ),
              type(found)
          ),
          path=parent.path,
          element=parent,
      )
  
  class ObjectBase(object):
      """
      The base class for all schema objects.  Includes helpers for common schema-
      related functions.
      """
      __slots__ = ['path', 'raw_element', '_accessed_members', 'strict', '_root',
                   'extensions', '_original_ref']
      required_fields = []
  
      def __init__(self, path, raw_element, root):
          """
          Creates a new Object for a OpenAPI schema with a reference to its own
          path in the schema.
  
          :param path: The path to this element in the spec.
          :type path: list[str]
          :param raw_element: The raw element parsed from the spec that this object
                              is parsing.
          :type raw_element: dict
          :param root: The root of the spec, for reference
          :type root: OpenAPI
          """
          # init empty slots
          for k in type(self).__slots__:
              if k in ('_spec_errors', 'validation_mode'):
                  # allow these two fields to keep their values
                  continue
              setattr(self, k, None)
  
          self.path = path
          self.raw_element = raw_element
          self._root = root
  
          self._accessed_members = []
          self.extensions = {}
  
          # TODO - add strict mode that errors if all members were not accessed
          self.strict = False
  
          # parse our own element
          try:
              self._required_fields(*type(self).required_fields)
              self._parse_data()
          except SpecError as e:
              if self._root.validation_mode:
                  self._root.log_spec_error(e)
              else:
                  raise
  
          # TODO - this may not be appropriate in all cases
          self._parse_spec_extensions()
  
          # TODO - assert that all keys of raw_element were accessed
  
      def __repr__(self):
          """
          Returns a string representation of the parsed object
          """
          # TODO - why?
          return "<{} {}>".format(type(self), self.path)
  
      def __getstate__(self):
          """
          Returns this object as a dict, removing all empty keys.  This can be used
          to serialize a spec.
  
          Allows pickling objects by returning a dict of all slotted values.
          """
          return _asdict({
              k: getattr(self, k) for k in type(self).__slots__ if hasattr(self, k)
          })
  
      def __setstate__(self, state):
          """
          Allows unpickling objects
          """
          for k, v in state.items():
              setattr(self, k, v)
  
      def _required_fields(self, *fields):
          """
          Given a list of require fields for this object, raises a SpecError if any
          of the fields do not exist.
  
          :param *fields: A list of fields to ensure exist in this object
          :type *fields: str
  
          :raises SpecError: if any of the required fields are not present.
          """
          missing_fields = []
          for field in fields:
              if field not in self.raw_element:
                  missing_fields.append(field)
  
          if missing_fields:
              raise SpecError('Missing required fields: {}'.format(
                  ', '.join(missing_fields)),
                  path=self.path,
                  element=self)
  
      def _parse_data(self):
          """
          Parses the raw_element into this object.  This is not implemented here,
          but is called in the constructor and _must_ be implemented in all
          subclasses.
  
          An implementation of this method should use :any:`_get` to retrieve
          values from the raw_element, which has the side-effect of noting that
          those members were accessed.  After this is executed, spec extensions
          are parsed and then an assertion is made that all keys in the
          raw_element were accessed - if not, the schema is considered invalid.
          """
          raise NotImplementedError('You must implement this method in subclasses!')
  
      def _get(self, field, object_types, is_list=False, is_map=False):
          """
          Retrieves a value from this object's raw element, and returns None if
          it is not present.  Use :any:`_required_fields` to ensure all required
          fields are present before depending on the output of this method.
  
          :param field: The field name to retrieve
          :type field: str
          :param object_types: The types of Objects that are accepted.  One of
                               these types will be returned, or the spec will be
                               considered invalid.  If the magic string '*' is
                               passed in, it must be the only accepted type, and
                               all types will be accepted.
          :type object_types: list[str or Type]
          :param is_list: If true, this should return a List of object of the give
                          types.
          :param is_list: bool
          :param is_map: If true, this must return a :any:`Map` of object of the given
                         types
          :type is_map: bool
  
          :returns: object_type if given, otherwise the type parsed from the spec
                    file
          """
          self._accessed_members.append(field)
  
          ret = self.raw_element.get(field, None)
          if ret is None:
              return None
  
          try:
              if not isinstance(object_types, list):
                  # maybe don't accept not-lists
                  object_types = [object_types]
  
              if '*' in object_types and len(object_types) != 1:
                  raise ValueError("Fields that accept any type must not specify any other types!")
  
              # if yaml loads a value that includes a unicode character in python2,
              # that value will come in as a ``unicode`` type instead of a ``str``.
              # For the purposes of this library, those are the same thing, so in
              # python2 only, we'll include ``unicode`` for any element that
              # accepts ``str`` types.
              if IS_PYTHON_2:
                  if str in object_types:
                      object_types += [unicode]
  
  
              if is_list:
                  if not isinstance(ret, list):
                      raise SpecError('Expected {}.{} to be a list of [{}], got {}'.format(
                          self.get_path, field, ','.join([str(c) for c in object_types]),
                          type(ret)),
                          path=self.path,
                          element=self)
                  ret = self.parse_list(ret, object_types, field)
              elif is_map:
                  if not isinstance(ret, dict):
                      raise SpecError('Expected {}.{} to be a Map of string: [{}], got {}'.format(
                          self.get_path, field, ','.join([str(c) for c in object_types]),
                          type(ret)),
                          path=self.path,
                          element=self)
                  ret = Map(self.path + [field], ret, object_types, self._root)
              else:
                  accepts_string = str in object_types
                  found_type = False
  
                  for t in object_types:
                      if t == "*":
                          found_type = True
                          break
  
                      if t == str:
                          # try to parse everything else first
                          continue
  
                      if isinstance(t, str):
                          # we were given the name of a subclass of ObjectBase,
                          # attempt to parse ret as that type
                          python_type = ObjectBase.get_object_type(t)
  
                          if python_type.can_parse(ret):
                              ret = python_type(self.path + [field], ret, self._root)
                              found_type = True
                              break
                      elif isinstance(ret, t):
                          # it's already the type we need
                          found_type = True
                          break
  
                  if not found_type:
                      if accepts_string and isinstance(ret, str):
                          found_type = True
  
                  if not found_type:
                      raise_on_unknown_type(self, field, object_types, ret)
          except SpecError as e:
              if self._root.validation_mode:
                  self._root.log_spec_error(e)
                  ret = None
              else:
                  raise
  
          return ret
  
      @classmethod
      def key_contained(cls, key, target_list):
          """
          Returns whether the key is contained in the given list or not.
          We use this specific function as to prevent usage of keywords we add "_"
          to several parameters, and we still want to validate those parameters
          """
          if key.endswith("_"):
              extra_key = key[:-1]
          else:
              extra_key = key + "_"
          return key in target_list or extra_key in target_list
  
      @classmethod
      def can_parse(cls, dct):
          """
          Returns True if this class can parse the given dict.  This is based on
          the __slots__ and required_fields of the class, and the keys of the dict.
          This is intended to be used when an element may be one of a number of
          allowed Object types - each type should independently consider if it
          can parse the given element, and the first to report that it can should
          be used.
  
          :param dct: The dict to consider.
          :type dct: dict
  
          :returns: True if this class can parse dct into an instance of itself,
                    otherwise False
          :rtype: bool
          """
          # if this isn't a dict, the spec is dreadfully wrong (and since no type
          # will be able to parse this value, an appropriate error is returned)
          if not isinstance(dct, dict):
              return False
          # ensure that the dict's keys are valid in our slots
          for key in dct.keys():
              if key.startswith('x-'):
                  # ignore spec extensions
                  continue
  
              if not cls.key_contained(key, cls.__slots__):
                  # it has something we don't - probably not a match
                  return False
  
          # then, ensure that all required fields are present
          for key in cls.required_fields:
              if not cls.key_contained(key, dct):
                  # it doesn't have everything we need - probably not a match
                  return False
  
          return True
  
      def _parse_spec_extensions(self):
          """
          Examines the keys of this Object's raw_element and collects any `Specification
          Extensions`_ into the extensions attribute of this Object.
  
          .. _Specification Extensions: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#specificationExtensions
          """
          for k, v in self.raw_element.items():
              if k.startswith('x-'):
                  self.extensions[k[2:]] = v
                  self._accessed_members.append(k)
  
      @classmethod
      def get_object_type(cls, typename):
          """
          Introspects the subclasses of this class to decide which to return for
          object_type
  
          :param object_type: The name of a class that inherits from this class.
                              Must exactly equal type(Class).__name__
          :type object_type: str
  
          :returns: The Type associated with this name
          :raises ValueError: if no Type with that name was found
          """
          if not hasattr(cls, '_subclass_map'):
              # generate subclass map on first call
              setattr(cls, '_subclass_map', {t.__name__: t for t in cls.__subclasses__()})
  
          # TODO - why?
          if typename not in cls._subclass_map:  # pylint: disable=no-member
              raise ValueError('ObjectBase has no subclass {}'.format(typename))
  
          return cls._subclass_map[typename]  # pylint: disable=no-member
  
      def get_path(self):
          """
          Get the full path for this element in the spec
  
          :returns: The path in the spec for this element
          :rtype: str
          """
          return '.'.join(self.path)
  
      def parse_list(self, raw_list, object_types, field=None):
          """
          Given a list of Objects, iterates over the list and creates the relevant
          Objects, returning the resulting list.
  
          :param raw_list: The list to parse
          :type raw_list: list[dict]
          :param object_types: A list of subclass names to attempt to parse the
                               objects to.  The list does not need to consist of
                               only one of these types.
          :type object_type: list[str]
          :param field: The field to append to self.get_path() when determining path
                        for created objects.
          :type field: str
  
          :returns: A list of parsed objects
          :rtype: list[object_type]
          """
          if raw_list is None:
              return None
  
          if not isinstance(object_types, list):
              object_types = [object_types]
  
          real_path = self.path[:]
          if field:
              real_path += [field]
  
          python_types = [ObjectBase.get_object_type(t) if isinstance(t, str) else t
                          for t in object_types]
  
          result = []
          for i, cur in enumerate(raw_list):
              found_type = False
  
              for cur_type in python_types:
                  if issubclass(cur_type, ObjectBase) and cur_type.can_parse(cur):
                      result.append(cur_type(real_path + [str(i)], cur, self._root))
                      found_type = True
                      continue
                  elif isinstance(cur, cur_type):
                      result.append(cur)
                      found_type = True
                      continue
  
              if not found_type:
                  raise SpecError('Could not parse {}.{}, expected to be one of [{}]'.format(
                      '.'.join(real_path), i, object_types),
                      path=self.path,
                      element=self)
  
          return result
  
      def _resolve_references(self):
          """
          Resolves all reference objects below this object and notes their original
          value was a reference.
          """
          # don't circular import
          reference_type = ObjectBase.get_object_type('Reference')
  
          for slot in self.__slots__:
              if slot.startswith('_'):
                  # don't parse private members
                  continue
              value = getattr(self, slot)
  
              if isinstance(value, reference_type):
                  # we found a reference - attempt to resolve it
                  reference_path = value.ref
                  if not reference_path.startswith('#/'):
                      raise ReferenceResolutionError('Invalid reference path {}'.format(
                          reference_path),
                          path=self.path,
                          element=self)
  
                  reference_path = reference_path.split('/')[1:]
  
                  try:
                      resolved_value = self._root.resolve_path(reference_path)
                  except ReferenceResolutionError as e:
                      # add metadata to the error
                      e.path = self.path
                      e.element = self
                      raise
  
                  # FIXME - will break if multiple things reference the same
                  # node
                  resolved_value._original_ref = value
  
                  # resolved
                  setattr(self, slot, resolved_value)
              elif issubclass(type(value), ObjectBase) or isinstance(value, Map):
                  # otherwise, continue resolving down the tree
                  value._resolve_references()
              elif isinstance(value, list):
                  # if it's a list, resolve its item's references
                  resolved_list = []
                  for item in value:
                      if isinstance(item, reference_type):
                          # TODO - this is duplicated code
                          reference_path = item.ref.split('/')[1:]
  
                          try:
                              resolved_value = self._root.resolve_path(reference_path)
                          except ReferenceResolutionError as e:
                              # add metadata to the error
                              e.path = self.path
                              e.element = self
                              raise
  
                          resolved_value._original_ref = value
                          resolved_list.append(resolved_value)
                      else:
                          if issubclass(type(value), ObjectBase) or isinstance(value, Map):
                              item._resolve_references()
                          resolved_list.append(item)
  
                  setattr(self, slot, resolved_list)
  
      def _resolve_allOfs(self):
          """
          Walks object tree calling _resolve_allOf on each type.
  
          Types can override this to handle allOf handling themselves.  Types that
          do so should call the parent class' _resolve_allOf when they do
          """
          for slot in self.__slots__:
              if slot.startswith("_"):
                  # no need to handle private members
                  continue
  
              value = getattr(self, slot)
  
              if issubclass(type(value), ObjectBase):
                  value._resolve_allOfs()
              elif issubclass(type(value), Map):
                  for _, c in value.items():
                      c._resolve_allOfs()
              elif isinstance(value, list):
                  for c in value:
                      if issubclass(type(c), ObjectBase) or issubclass(type(c), Map):
                          c._resolve_allOfs()
  
  
  class Map(dict):
      """
      The Map object wraps a python dict and parses its values into the chosen
      type or types.
      """
      __slots__ = ['dct', 'path', 'raw_element', '_root']
  
      def __init__(self, path, raw_element, object_types, root):
          """
          Creates a dict containing the parsed objects from the raw element
  
          :param path: The path to this Map in the spec.
          :type path: list
          :param raw_element: The raw spec data for this map.  The keys must all
                              be strings.
          :type raw_element: dict
          :param object_types: A list of strings accepted by
                               :any:`ObjectBase.get_object_type`, or the python
                               types to parse.
          :type object_types: list[str or Type]
          """
          self.path = path
          self.raw_element = raw_element
          self._root = root
  
          python_types = []
          dct = {}
  
          for t in object_types:
              if isinstance(t, str):
                  python_types.append(ObjectBase.get_object_type(t))
              else:
                  python_types.append(t)
  
          for k, v in self.raw_element.items():
              found_type = False
  
              for t in python_types:
                  if issubclass(t, ObjectBase) and t.can_parse(v):
                      dct[k] = t(path + [k], v, self._root)
                      found_type = True
                  elif isinstance(v, t):
                      dct[k] = v
                      found_type = True
  
              if not found_type:
                  raise_on_unknown_type(self, k, object_types, v)
  
          self.update(dct)
  
      def _resolve_references(self):
          """
          This has been added to allow propagation of reference resolution as defined
          in :any:`ObjectBase._resolve_references`.  This implementation simply
          calls the same on all values in this Map.
          """
          reference_type = ObjectBase.get_object_type('Reference')
  
          for key, value in self.items():
              if isinstance(value, reference_type):
                  # TODO - this is repeated code
                  # we found a reference - attempt to resolve it
                  reference_path = value.ref
                  if not reference_path.startswith('#/'):
                      raise ReferenceResolutionError('Invalid reference path {}'.format(
                          reference_path),
                          path=self.path,
                          element=self)
  
                  reference_path = reference_path.split('/')[1:]
  
                  try:
                      resolved_value = self._root.resolve_path(reference_path)
                  except ReferenceResolutionError as e:
                      # add metadata to the error
                      e.path = self.path
                      e.element = self
                      raise
  
                  # FIXME - will break if multiple things reference the same
                  # node
                  resolved_value._original_ref = value
  
                  # resolved
                  self[key] = resolved_value
              else:
                  value._resolve_references()
  
      def get_path(self):
          """
          Get the full path for this element in the spec
  
          :returns: The path in the spec for this element
          :rtype: str
          """
          return '.'.join(self.path)
  