YAML Error: could not determine a constructor for the tag

16,132

Your "bang notation" is proper YAML, normally this is called a tag. If you want to use the safe_load() with those you'll have to provide constructors for the !Ref and !Sub tags, e.g. using:

ruamel.yaml.add_constructor(u'!Ref', your_ref_constructor, constructor=ruamel.yaml.SafeConstructor)

where for both tags you should expect to handle scalars a value. and not the more common mapping.

I recommend you use the RoundTripLoader instead of the SafeLoader, that will preserve order, comments, etc. as well. The RoundTripLoader is a subclass of the SafeLoader.

If you are using ruamel.yaml>=0.15.33, which supports round-tripping scalars, you can do (using the new ruamel.yaml API):

import sys
from ruamel.yaml import YAML

yaml = YAML()
yaml.preserve_quotes = True

data = yaml.load("""\
Outputs:
  Vpc:
    Value: !Ref: vpc    # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag
""")

yaml.dump(data, sys.stdout)

to get:

Outputs:
  Vpc:
    Value: !Ref: vpc    # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag

In older 0.15.X versions, you'll have to specify the classes for the scalar objects yourself. This is cumbersome, if you have many objects, but allows for additional functionality:

import sys
from ruamel.yaml import YAML


class Ref:
    yaml_tag = u'!Ref:'

    def __init__(self, value, style=None):
        self.value = value
        self.style = style

    @classmethod
    def to_yaml(cls, representer, node):
        return representer.represent_scalar(cls.yaml_tag,
                                            u'{.value}'.format(node), node.style)

    @classmethod
    def from_yaml(cls, constructor, node):
        return cls(node.value, node.style)

    def __iadd__(self, v):
        self.value += str(v)
        return self

class Sub:
    yaml_tag = u'!Sub'
    def __init__(self, value, style=None):
        self.value = value
        self.style = style

    @classmethod
    def to_yaml(cls, representer, node):
        return representer.represent_scalar(cls.yaml_tag,
                                            u'{.value}'.format(node), node.style)

    @classmethod
    def from_yaml(cls, constructor, node):
        return cls(node.value, node.style)


yaml = YAML(typ='rt')
yaml.register_class(Ref)
yaml.register_class(Sub)

data = yaml.load("""\
Outputs:
  Vpc:
    Value: !Ref: vpc    # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag
""")

data['Outputs']['Vpc']['Value'] += '123'

yaml.dump(data, sys.stdout)

which gives:

Outputs:
  Vpc:
    Value: !Ref: vpc123 # first tag
    Export:
      Name: !Sub "${AWS::StackName}-Vpc"  # second tag
Share:
16,132

Related videos on Youtube

Matthew Patton
Author by

Matthew Patton

Updated on June 04, 2022

Comments

  • Matthew Patton
    Matthew Patton almost 2 years

    This is very similar to questions/44786412 but mine appears to be triggered by YAML safe_load(). I'm using Ruamel's library and YamlReader to glue a bunch of CloudFormation pieces together into a single, merged template. Is bang-notation just not proper YAML?

    Outputs:
      Vpc:
        Value: !Ref vpc
        Export:
          Name: !Sub "${AWS::StackName}-Vpc"
    

    No problem with these

    Outputs:
      Vpc:
        Value:
          Ref: vpc
        Export:
          Name:
            Fn::Sub: "${AWS::StackName}-Vpc"
    
    Resources:
      vpc:
        Type: AWS::EC2::VPC
        Properties:
          CidrBlock:
            Fn::FindInMap: [ CidrBlock, !Ref "AWS::Region", Vpc ]
    

    Part 2; how to get load() to leave what's right of the 'Fn::Select:' alone.

      FromPort: 
        Fn::Select: [ 0, Fn::FindInMap: [ Service, https, Ports ] ]
    

    gets converted to this, that now CF doesn't like.

      FromPort:
        Fn::Select: [0, {Fn::FindInMap: [Service, https, Ports]}]
    

    If I unroll the statement fully then no problems. I guess the shorthand is just problematic.

      FromPort:
        Fn::Select:
        - 0
        - Fn::FindInMap: [Service, ssh, Ports]
    
  • Matthew Patton
    Matthew Patton over 6 years
    Thanks. I've gotten much farther now that I've removed bang-notation and switched to using RoundTrip. But besides defining said classes (not particularly interested in defining all of CloudFormation's intrinsic functions), is there a way for Yaml to leave anything to the right of the colon alone and not parse it?
  • Anthon
    Anthon over 6 years
    @MatthewPatton No, the parsing needs to be done, to know where the tag ends, what the value associated with it is etc. I updated the catch-all for undefined tagged objects to support scalars (it only supported mapping based object) as you are using and updated the answer accordingly.
  • Jason S
    Jason S almost 4 years
    "The RoundTripLoader is a subclass of the SafeLoader" -- is it? I can't tell from the source code: sourceforge.net/p/ruamel-yaml/code/ci/default/tree/…
  • Anthon
    Anthon almost 4 years
    @JasonS You should probably post that as a question.