Retro
Retro

Reputation: 130

PyYAML and composition, attribute error

I've been trying to get to grips with PyYAML as I love it's readability, and would like to use it in a few open source projects I'm working as an alternative to JSON.

However, I'm struggling to understand exactly how to construct objects with composition. I opened this question: PyYAML - how to deal with compositon and it seemed to work in terms of reading out the info, but not in the context of the full program.

Here's a stripped down example of what I'm trying to YAMLify:

import yaml
import data

class DungeonObject(yaml.YAMLObject):
    yaml_tag = u'!DungeonObject'
    def __init__(self, x, y, char, name, blocks=False, fighter=None):
        self.x = x
        self.y = y
        self.char = char
        self.name = name
        self.blocks = blocks

        self.fighter = fighter
        if self.fighter:
            self.fighter.owner = self


    def __repr__(self):
        return "%s(x=%r, y=%r, char=%r, name=%r, blocks=%r fighter=%r)" % (self.__class__.__name__, self.x, self.y, self.char, self.name, self.blocks, self.fighter)

class Fighter(yaml.YAMLObject):
    yaml_tag = u'!Fighter'
    #combat-related properties and methods (monster, player, NPC).
    def __init__(self, hp, defense, strength):
        self.hp = hp
        self.base_defense = defense
        self.base_strength = strength
    def __repr__(self):
        return "%s(hp=%r, defense=%r, strength=%r)" % (self.__class__.__name__, self.hp, self.defense, self.strength)


monsters = {DungeonObject.name : DungeonObject for DungeonObject in yaml.load_all(data.monsterdata)}
print (monsters)

And my YAML file:

monsterdata = """
---
!Fighter &fighter_component
    hp: 20
    defense: 0
    strength: 4
!DungeonObject
    x: x
    y: y
    char: 'o'
    name: 'orc'
    blocks: True
    fighter: fighter_component
---
!Fighter &fighter_component
    hp: 9
    defense: 0
    strength: 10
!DungeonObject
    x: x
    y: y
    char: 't'
    name: 'troll'
    blocks: True
    fighter: fighter_component
"""

With this, I'm getting the error: line 32, in monsters = {DungeonObject.name : DungeonObject for DungeonObject in yaml.load_all(data.monsterdata)} AttributeError: 'Fighter' object has no attribute 'name'

Upvotes: 2

Views: 1013

Answers (1)

Anthon
Anthon

Reputation: 76792

Each document should contain a sequence/list that consists of a Fighter and a DungeonObject. The former has no name, so you should filter on DungeonObjects that are actuall of type DungeonObject and not of type Fighter.

What is a bit confusing is that you use a variable DungeonObject as well, so try to use dungeon_object for the variable:

from ruamel import yaml

monsterdata = """
---
- !Fighter &fighter_component
    hp: 20
    defense: 0
    strength: 4
- !DungeonObject
    x: x
    y: y
    char: 'o'
    name: 'orc'
    blocks: True
    fighter: fighter_component
---
- !Fighter &fighter_component
    hp: 9
    defense: 0
    strength: 10
- !DungeonObject
    x: x
    y: y
    char: 't'
    name: 'troll'
    blocks: True
    fighter: fighter_component
"""


class DungeonObject(yaml.YAMLObject):
    yaml_tag = u'!DungeonObject'

    def __init__(self, x, y, char, name, blocks=False, fighter=None):
        self.x = x
        self.y = y
        self.char = char
        self.name = name
        self.blocks = blocks

        self.fighter = fighter
        if self.fighter:
            self.fighter.owner = self

    def __repr__(self):
        return "{}(x={!r}, y={!r}, char={!r}, name={!r}, blocks={!r} fighter={!r})".format(
            self.__class__.__name__, self.x, self.y, self.char, self.name,
            self.blocks, self.fighter)


class Fighter(yaml.YAMLObject):
    yaml_tag = u'!Fighter'
    # combat-related properties and methods (monster, player, NPC).

    def __init__(self, hp, defense, strength):
        self.hp = hp
        self.base_defense = defense
        self.base_strength = strength

    def __repr__(self):
        return "{}(hp={!r}, defense={!r}, strength={!r})".format(
            self.__class__.__name__, self.hp, self.defense, self.strength)

monsters = {}
for doc in yaml.load_all(monsterdata, Loader=yaml.Loader):
    for dungeon_object in doc:
        if isinstance(dungeon_object, DungeonObject):
            monsters[dungeon_object.name] = dungeon_object

print (monsters)

Which gives:

{'orc': DungeonObject(x='x', y='y', char='o', name='orc', blocks=True fighter='fighter_component'), 'troll': DungeonObject(x='x', y='y', char='t', name='troll', blocks=True fighter='fighter_component')}

I updated the __repr__ to use the more modern .format() method. Since I am using ruamel.yaml (which is a superset of PyYAML functionality and backwards compatible), I need to specify the Loader explicitly to suppress the warning that load_all is unsafe when using the default loader. (Disclaimer: I am the developer of that package)

Upvotes: 1

Related Questions