<commit_before>from collections import defaultdict
from npc.util import OutOfBoundsError, flatten, merge_to_dict

class Character:
    """
    Object to hold and access data for a single character

    Can be accessed like a dictionary where missing keys will return an empty
    array instead of throwing an exception. The "path" key is special: it will
    always contain a string.

    While values can be accessed directly, it's better practice to use the
    getter and setter methods that are provided, since they automatically
    preserve the internal structure of the dict.
    """

    STRING_FIELDS = ('path',)
    """
    tuple (Str): String-only data. These are stored as plain strings and do not
        come from a tag.
    """

    IMPLICIT_FIELDS = ('description',)
    """
    tuple (Str): Fields without an explicit tag.
    """

    GROUP_TAGS = (
        'court', 'motley', 'entitlement', # changeling
        'pack', 'tribe', 'lodge',         # werewolf
        'group')                          # universal
    """tuple (str): Group-like tags. These all accept an accompanying `rank`
        tag."""

    BARE_FLAGS = ('wanderer', 'skip')
    """tuple (str): Flags whose value is ignored"""

    DATA_FLAGS = ('foreign', 'dead')
    """tuple (str): Flags that can accept an optional value"""

    ADDON_TAGS = ('rank',)
    """tuple (str): Tags whose value relates to a previous tag"""

    TAGS = (
        'name', 'type', 'faketype', 'title', 'appearance', 'hide', 'hidegroup', 'hideranks', # universal
        'seeming', 'kith', 'mask', 'mien'                                                    # changeling
        )
    """tuple (str): Tags that must have a value. Shortcuts, like @changeling,
        are expanded during parsing and do not appear literally."""

    KNOWN_TAGS = STRING_FIELDS + IMPLICIT_FIELDS + GROUP_TAGS + BARE_FLAGS + DATA_FLAGS + ADDON_TAGS + TAGS
    """tuple (str): All recognized tags. Other, unrecognized tags are fine to
        add and will be ignored by methods that don't know how to handle them."""

    def __init__(self, attributes: dict = None, **kwargs):
        """
        Create a new Character object.

        When supplying values through `attributes` or `**kwargs`, remember that
        almost everything needs to be in a list. The exceptions are the "path"
        key, which must be strings, and the "rank" key, which is special. It is
        a dict of dicts, and the members of that dict are lists. When in doubt,
        it's safer to build the object using the append and append_rank methods.

        Args:
            attributes (dict): Dictionary of attributes to insert into the
                Character. If a value is a bare string, it will be converted to
                a list containing that string.
            **kwargs: Named arguments will be added verbatim to the new
                Character. Keys here will overwrite keys of the same name from
                the `attributes` arg. The values here are not altered at all.
        """
        def wrap_strings(attributes: dict):
            wrapped_attributes = {}
            for key, val in attributes.items():
                if isinstance(val, dict):
                    wrapped_attributes[key] = wrap_strings(val)
                elif isinstance(val, str):
                    wrapped_attributes[key] = [val]
                else:
                    wrapped_attributes[key] = val
            return wrapped_attributes

        self.tags = defaultdict(list)
        for key in self.STRING_FIELDS:
            self.tags[key] = ''
        self.tags['rank'] = defaultdict(list)

        if attributes:
            attributes = wrap_strings(attributes)
            self.tags.update(attributes)
        self.tags.update(kwargs)
        self.problems = ['Not validated']
<commit_msg>Allow copying tags from existing character<commit_after>from collections import defaultdict
from npc.util import OutOfBoundsError, flatten, merge_to_dict

class Character:
    """
    Object to hold and access data for a single character

    Can be accessed like a dictionary where missing keys will return an empty
    array instead of throwing an exception. The "path" key is special: it will
    always contain a string.

    While values can be accessed directly, it's better practice to use the
    getter and setter methods that are provided, since they automatically
    preserve the internal structure of the dict.
    """

    STRING_FIELDS = ('path',)
    """
    tuple (Str): String-only data. These are stored as plain strings and do not
        come from a tag.
    """

    IMPLICIT_FIELDS = ('description',)
    """
    tuple (Str): Fields without an explicit tag.
    """

    GROUP_TAGS = (
        'court', 'motley', 'entitlement', # changeling
        'pack', 'tribe', 'lodge',         # werewolf
        'group')                          # universal
    """tuple (str): Group-like tags. These all accept an accompanying `rank`
        tag."""

    BARE_FLAGS = ('wanderer', 'skip')
    """tuple (str): Flags whose value is ignored"""

    DATA_FLAGS = ('foreign', 'dead')
    """tuple (str): Flags that can accept an optional value"""

    ADDON_TAGS = ('rank',)
    """tuple (str): Tags whose value relates to a previous tag"""

    TAGS = (
        'name', 'type', 'faketype', 'title', 'appearance', 'hide', 'hidegroup', 'hideranks', # universal
        'seeming', 'kith', 'mask', 'mien'                                                    # changeling
        )
    """tuple (str): Tags that must have a value. Shortcuts, like @changeling,
        are expanded during parsing and do not appear literally."""

    KNOWN_TAGS = STRING_FIELDS + IMPLICIT_FIELDS + GROUP_TAGS + BARE_FLAGS + DATA_FLAGS + ADDON_TAGS + TAGS
    """tuple (str): All recognized tags. Other, unrecognized tags are fine to
        add and will be ignored by methods that don't know how to handle them."""

    def __init__(self, attributes: dict=None, other_char=None, **kwargs):
        """
        Create a new Character object.

        Initial tag values can be supplied in three ways depending on what's
        most convenient: an existing character, a dict of attributes, and misc
        keyword arguments. Character tags are copied first, then overwritten by
        values from the attributes dict, then overwritten by values from
        individual keyword args.

        When supplying values through `attributes` or `**kwargs`, remember that
        almost everything needs to be in a list. The exceptions are the "path"
        key, which must be strings, and the "rank" key, which is special. It is
        a dict of dicts, and the members of that dict are lists. When in doubt,
        it's safer to build the object using the append and append_rank methods.

        Args:
            attributes (dict): Dictionary of attributes to insert into the
                Character. If a value is a bare string, it will be converted to
                a list containing that string.
            other_char (Character): Existing character object to copy. Tags from
                that object will be copied verbatim with no changes.
            **kwargs: Named arguments will be added verbatim to the new
                Character. Keys here will overwrite keys of the same name from
                the `attributes` arg. The values here are not altered at all.
        """
        def wrap_strings(attributes: dict):
            wrapped_attributes = {}
            for key, val in attributes.items():
                if isinstance(val, dict):
                    wrapped_attributes[key] = wrap_strings(val)
                elif isinstance(val, str):
                    wrapped_attributes[key] = [val]
                else:
                    wrapped_attributes[key] = val
            return wrapped_attributes

        self.tags = defaultdict(list)
        for key in self.STRING_FIELDS:
            self.tags[key] = ''
        self.tags['rank'] = defaultdict(list)

        if other_char:
            self.tags.update(other_char.tags)

        if attributes:
            attributes = wrap_strings(attributes)
            self.tags.update(attributes)

        self.tags.update(kwargs)

        self.problems = ['Not validated']

{'fraction_non_alphanumeric': 0.08558285921801234, 'fraction_numerical': 0.0, 'mean_word_length': 2.259171597633136, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6688358', 'n_tokens_mistral': 2161, 'n_tokens_neox': 2063, 'n_words': 890}