title: Implicit resolvers and robust representers for human-friendly tuple and np.array support in ruamel.yaml
body: <p>I have a project where the user is expected to manually write a yaml file.
  This yaml file might have some of its entries formatted as tuples or numpy arrays. We distinguish tuples and list internally in python to provide a convenient interface to the user, e.g. (1, 2, 3) is different than [1, 2, 3].</p>
  <p>For convenience, I'd like the user to be able to enter a tuple directly using parenthesis, like so <code>name: (1,2,3)</code>. I'd also like the user to be able to provide with numpy arrays by entering something like <code>other_name: np.array([1,2,3])</code>. I know this won't preserve exact numerical accuracy of the numpy arrays, but we determined that this is a fair compromise for the improved user experience.</p>
  <p>I'm using ruamel.yaml, mainly because it preserves comments.</p>
  <p>I managed to do something that works, but it's does not feel &quot;right&quot; to me, especially the Resolving part. There's basically no implicit resolver and I'm using a dirty eval instead. I did manage to find some information about implicit resolvers in ruamel.yaml online, on SO, and by rummaging through the source, but I could not really make sense of it.</p>
  <p>Here's a minimal working example, with comments pointing out where I feel like the implementation is not robust or unclean.</p>
  <pre><code>import sys
  import numpy as np
  import ruamel.yaml
  
  
  def _tupleRepresenter(dumper, data):
      # TODO: Make this more robust
      return dumper.represent_scalar(u'tag:yaml.org,2002:str', str(data))
  
  
  def _numpyRepresenter(dumper, data):
      # TODO: Make this more robust
      as_string = 'np.array(' + np.array2string(data, max_line_width=np.inf, precision=16, prefix='np.array(', separator=', ', suffix=')') + ')'
      return dumper.represent_scalar(u'tag:yaml.org,2002:str', as_string)
  
  
  def load_yaml(file):
      # TODO: Resolve tuples and arrays properly when loading
      yaml = ruamel.yaml.YAML()
      yaml.Representer.add_representer(tuple, _tupleRepresenter)
      yaml.Representer.add_representer(np.ndarray, _numpyRepresenter)
      return yaml.load(file)
  
  
  def dump_yaml(data, file):
      yaml = ruamel.yaml.YAML()
      yaml.Representer.add_representer(tuple, _tupleRepresenter)
      yaml.Representer.add_representer(np.ndarray, _numpyRepresenter)
      return yaml.dump(data, file)
  
  
  yaml_file = &quot;&quot;&quot;
  test_tuple: (1, 2, 3)
  test_array: np.array([4,5,6])
  &quot;&quot;&quot;
  
  data = load_yaml(yaml_file)
  
  data['test_tuple'] = eval(data['test_tuple']) # This feels dirty
  data['test_array'] = eval(data['test_array']) # This feels dirty
  
  dump_yaml(data, sys.stdout)
  # test_tuple: (1, 2, 3)
  # test_array: np.array([4, 5, 6])
  </code></pre>
  <p>I welcome any help on improving this implementation with a proper implicit resolver, with robusts representers, and generally using ruamel.yaml more like it is intended to be.</p>
  <hr />
  <p><strong>Update</strong>:</p>
  <p>With help from the comments, I managed to do something that works almost completely. Let's ignore that I'd need to write a proper non-eval parser for now.</p>
  <p>The only issue left is that the new tags are now exported as strings, so they are not properly interpreted when reloading. They become strings instead and they won't survive many roundtrips.</p>
  <p>How can I avoid that?</p>
  <p>Here's a minimal working example:</p>
  <pre><code>import sys
  import numpy as np
  import ruamel.yaml
  
  # TODO: Replace evals by actual parsing
  # TODO: Represent custom types without the string quotes
  
  _tuple_re = &quot;^(?:\((?:.|\n|\r)*,(?:.|\n|\r)*\){1}(?: |\n|\r)*$)&quot;
  _array_re = &quot;^(?:(np\.|)array\(\[(?:.|\n|\r)*,(?:.|\n|\r)*\]\){1}(?: |\n|\r)*$)&quot;
  _complex_re = &quot;^(?:(?:\d+(?:(?:\.\d+)?(?:e[+\-]\d+)?)?)?(?: *[+\-] *))?(?:\d+(?:(?:\.\d+)?(?:e[+\-]\d+)?)?)?[jJ]$&quot;
  
  
  def _tuple_constructor(self, node):
      return eval(self.construct_scalar(node))
  
  
  def _array_constructor(self, node):
      value = node.value
      if not value.startswith('np.'):
          value = 'np.' + value
      return eval(value)
  
  
  def _complex_constructor(self, node):
      return eval(node.value)
  
  
  def _tuple_representer(dumper, data):
      return dumper.represent_scalar(u'tag:yaml.org,2002:str', str(data))
  
  
  def _array_representer(dumper, data):
      as_string = 'np.array(' + np.array2string(data, max_line_width=np.inf, precision=16, prefix='np.array(', separator=', ', suffix=')') + ')'
      as_string = as_string.replace(' ', '').replace(',', ', ')
      return dumper.represent_scalar(u'tag:yaml.org,2002:str', as_string)
  
  
  def _complex_representer(dumper, data):
      repr = str(data).replace('(', '').replace(')', '')
      return dumper.represent_scalar(u'tag:yaml.org,2002:str', repr)
  
  
  custom_types = {
      '!tuple':   {'re':_tuple_re,   'constructor': _tuple_constructor,   'representer':_tuple_representer,   'type': tuple,      'first':list('(')             },
      '!nparray': {'re':_array_re,   'constructor': _array_constructor,   'representer':_array_representer,   'type': np.ndarray, 'first':list('an')            },
      '!complex': {'re':_complex_re, 'constructor': _complex_constructor, 'representer':_complex_representer, 'type': complex,    'first':list('0123456789+-jJ')},
  }
  
  
  def load_yaml(file):
      yaml = ruamel.yaml.YAML()
      for tag,ct in custom_types.items():
          yaml.Constructor.add_constructor(tag, ct['constructor'])
          yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
          yaml.Representer.add_representer(ct['type'], ct['representer'])
      return yaml.load(file)
  
  
  def dump_yaml(data, file):
      yaml = ruamel.yaml.YAML()
      for tag,ct in custom_types.items():
          yaml.Constructor.add_constructor(tag, ct['constructor'])
          yaml.Resolver.add_implicit_resolver(tag, ruamel.yaml.util.RegExp(ct['re']), ct['first'])
          yaml.Representer.add_representer(ct['type'], ct['representer'])
      return yaml.dump(data, file)
  
  yaml_file = &quot;&quot;&quot;
  test_tuple: (1, 2, 3)
  test_array: array([4.0,5+0j,6.0j])
  test_complex: 3 + 2j
  &quot;&quot;&quot;
  
  data = load_yaml(yaml_file)
  
  dump_yaml(data, sys.stdout)
  # test_tuple: '(1, 2, 3)'
  # test_array: 'np.array([4.+0.j, 5.+0.j, 0.+6.j])'
  # test_complex: '3+2j'
  </code></pre>
  <p>Thank you!</p>