Example #1
0
    def get(self, key, default=None):
        """
        Get the value of a settings key

        Use the period character to indicate a nested key. So, the key
        "alpha.beta.charlie" is looked up like
        `data['alpha']['beta']['charlie']`.

        Args:
            key (str): Key to get from settings.
            default (any): Value to return when key isn't found.

        Returns:
            The value in that key, or None if the key could not be resolved.
        """
        key_parts = key.split('.')
        current_data = self.data
        for k in key_parts:
            try:
                current_data = current_data[k]
            except (KeyError, TypeError):
                if self.verbose:
                    util.print_err("Key not found: {}".format(key))
                return default
        return current_data
Example #2
0
def test_error_printer(capsys):
    util.print_err("Catchphrase!")

    out, err = capsys.readouterr()

    assert out == ""
    assert err == "Catchphrase!\n"
Example #3
0
    def __init__(self, verbose=False):
        """
        Loads all settings files.

        The default settings are loaded first, followed by user settings and
        finally campaign settings. Keys from later files overwrite those from
        earlier files.

        Only the default settings need to exist. If a different file cannot be
        found or opened, it will be silently ignored without crashing.

        Args:
            verbose (bool): Whether to show additional error messages that are
                usually ignored. These involve unloadable optional settings
                files and keys that cannot be found. The file
                `settings/settings.json` should never be found, but will still
                be reported.
        """

        self.module_base = Path(__file__).parent
        self.install_base = Path(self.module_base).parent

        self.default_settings_path = self.module_base
        self.user_settings_path = Path('~/.config/npc/').expanduser()
        self.campaign_settings_path = Path('.npc/')

        self.settings_file_names = [
            'settings', 'settings-changeling', 'settings-werewolf',
            'settings-gui'
        ]
        self.settings_file_suffixes = ['.json', '.yaml']
        self.settings_paths = [
            self.default_settings_path, self.user_settings_path,
            self.campaign_settings_path
        ]

        self.verbose = verbose

        loaded_data = util.load_settings(
            self._best_settings_path(self.default_settings_path,
                                     'settings-default'))

        # massage template names into real paths
        self.data = self._expand_templates(base_path=self.install_base,
                                           settings_data=loaded_data)

        # merge additional settings files
        for settings_path in self.settings_paths:
            for file in self.settings_file_names:
                try:
                    self.load_more(
                        self._best_settings_path(settings_path, file))
                except OSError as err:
                    # All of these files are optional, so normally we silently
                    # ignore these errors
                    if self.verbose:
                        util.print_err(err.strerror, err.filename)
Example #4
0
    def subtag(self, val: str):
        """
        No-op for compatibility with GroupTag class

        Args:
            val (str): Group name

        Returns:
            None
        """
        print_err("Calling touch() on non-flag class {} object '{}'".format(
            type(self).__name__, self.name))
Example #5
0
    def touch(self, present: bool = True):
        """
        No-op for compatibility with Flag class

        Args:
            present (bool): Whether to mark the flag present or not present.
                Defaults to True.

        Returns:
            None
        """
        print_err("Calling touch() on non-flag class {} object '{}'".format(
            type(self).__name__, self.name))
Example #6
0
    def copy_templates(dest_dir, templates):
        """
        Create new story files from templates.

        This is responsible for creating the new file name based on
        `new_number`, loading the template contents, substituting the "NNN" and
        "((COPY))" keywords, and writing the result to the new file.
        """
        def old_file_contents(old_file_path):
            """
            Get the previous file's contents.


            """
            try:
                with open(old_file_path, 'r') as old_file:
                    return old_file.read()
            except (FileNotFoundError, IsADirectoryError):
                return ''

        for template_path in templates:
            if SEQUENCE_KEYWORD not in str(template_path):
                print_err("Template {} has no number placeholder ({})".format(
                    template_path, SEQUENCE_KEYWORD))
                continue

            new_file_name = template_path.name.replace(SEQUENCE_KEYWORD,
                                                       str(new_number))
            destination = dest_dir.joinpath(new_file_name)
            if destination.exists():
                continue

            with open(template_path, 'r') as f:
                data = f.read()

            data = data.replace(SEQUENCE_KEYWORD, str(new_number))
            if COPY_KEYWORD in data:
                file_regex = regex_from_template(template_path)
                old_file_path = latest_file(dest_dir, file_regex).path
                data = data.replace(COPY_KEYWORD,
                                    old_file_contents(old_file_path))

            with open(destination, 'w') as f:
                f.write(data)
Example #7
0
    def load_more(self, settings_path):
        """
        Load additional settings from a file

        Settings values from this file will override the defaults. Any errors
        while opening the file are suppressed and the file will simply not be
        loaded. In that case, existing values are left alone.

        Args:
            settings_path (str): Path to the new json file to load
        """
        try:
            loaded = util.load_settings(settings_path)
        except util.errors.ParseError as err:
            util.print_err(err.strerror)
            return

        # paths should be evaluated relative to the settings file in settings_path
        absolute_path_base = Path(settings_path).resolve().parent
        loaded = self._expand_templates(absolute_path_base, loaded)

        self._merge_settings(loaded)
Example #8
0
    def _insert_sk_data(data):
        """Insert seeming and kith in the advantages block of a template"""

        seeming_re = re.compile(
            r'^(\s+)seeming(\s+)\w+$',
            re.MULTILINE | re.IGNORECASE
        )
        kith_re = re.compile(
            r'^(\s+)kith(\s+)\w+$',
            re.MULTILINE | re.IGNORECASE
        )

        seeming_name = temp_char.tags('seeming').first_value()
        seeming_key = seeming.lower()
        if seeming_key in prefs.get('changeling.seemings'):
            seeming_notes = "{}; {}".format(
                prefs.get("changeling.blessings.{}".format(seeming_key)),
                prefs.get("changeling.curses.{}".format(seeming_key)))
            data = seeming_re.sub(
                r"\g<1>Seeming\g<2>{} ({})".format(seeming_name, seeming_notes),
                data
            )
        else:
            print_err("Unrecognized seeming '{}'".format(seeming_name))

        kith_name = temp_char.tags('kith').first_value()
        kith_key = kith.lower()
        if kith_key in prefs.get('changeling.kiths.{}'.format(seeming_key), []):
            kith_notes = prefs.get("changeling.blessings.{}".format(kith_key))
            data = kith_re.sub(
                r"\g<1>Kith\g<2>{} ({})".format(kith_name, kith_notes),
                data
            )
        else:
            print_err("Unrecognized kith '{}' for seeming '{}'".format(kith_name, seeming_name))

        return data
Example #9
0
def parse_character(char_file_path) -> character.Character:
    """
    Parse a single character file

    Args:
        char_file_path (str): Path to the character file to parse

    Returns:
        Character object. Most keys store a list of values from the character.
        The string keys store a simple string, and the `rank` key stores
        a dict of list entries. Those keys are individual group names.
    """

    # derive character name from basename
    basename = path.basename(char_file_path)
    name = path.splitext(basename)[0].split(' - ', 1)[0]

    # instantiate new character
    parsed_char = character.Character(name=[name], path=char_file_path)

    with open(char_file_path, 'r') as char_file:
        subtag_registry = {}
        previous_line_empty = False

        for line in char_file:
            # stop processing once we see game stats
            if SECTION_RE.match(line):
                break

            match = TAG_RE.match(line)
            if match:
                tag = match.group('tag').lower()
                value = match.group('value')

                # skip comment tags
                if tag[0] == '#':
                    continue

                # handle compound tags
                if tag == 'changeling':
                    # grab attributes from compound tag
                    bits = value.split(maxsplit=1)
                    parsed_char.tags('type').append('Changeling')
                    if len(bits):
                        parsed_char.tags('seeming').append(bits[0])
                    if len(bits) > 1:
                        parsed_char.tags('kith').append(bits[1])
                    continue
                elif tag == 'werewolf':
                    parsed_char.tags('type').append('Werewolf')
                    parsed_char.tags('auspice').append(value)
                    continue

                # replace first name
                if tag == 'realname':
                    parsed_char.tags('name')[0] = value
                    continue

                # handle rank logic for group tags
                if parsed_char.tags(tag).subtag_name:
                    subtag_registry[parsed_char.tags(tag).subtag_name] = (
                        tag, value)
                if tag in subtag_registry:
                    supertag, supervalue = subtag_registry[tag]
                    parsed_char.tags(supertag)[supervalue].append(value)
                    continue

                # mark tags hidden as needed
                if tag == 'hide':
                    parts = HIDE_RE.split(value)

                    tagname = parts.pop(0)
                    if not parts:
                        parsed_char.tags(tagname).hidden = True
                        continue

                    first_value = parts.pop(0)
                    if not parts:
                        parsed_char.tags(tagname).hide_value(first_value)
                        continue

                    second_value = parts.pop(0)
                    if not parts:
                        if second_value == 'subtags':
                            parsed_char.tags(tagname).subtag(
                                first_value).hidden = True
                        else:
                            parsed_char.tags(tagname).subtag(
                                first_value).hide_value(second_value)
                        continue

                    # If we can't parse the hide string, hide the whole thing as
                    # a tag and let the Character object deal with it.
                    parsed_char.tags(value).hidden = True
                    continue

                if tag in DEPRECATED_TAGS:
                    print_err(
                        "The tag '{}' in `{}` is deprecated and will stop working in the future"
                        .format(tag, char_file_path))

                parsed_char.tags(tag).append(value)
            else:
                # Ignore second empty description line in a row
                if line == "\n":
                    if previous_line_empty:
                        continue
                    else:
                        previous_line_empty = True
                else:
                    previous_line_empty = False

                # all remaining text goes in the description
                parsed_char.tags('description').append(line.strip())
                continue

    return character.build(other_char=parsed_char)
Example #10
0
def start(argv=None):
    """
    Run the command-line interface

    Args:
        argv (list): Arguments from the command invocation. When running from
            code, this should just include the arguments you want to use, not
            the name of the script itself. When left blank, sys.argv will be
            used instead after chopping off the first argument.

    Returns:
        Integer code of zero for success or non-zero for failure.
    """

    # create parser and parse args
    parser = _make_parser()
    if not argv:
        argv = sys.argv[1:]
    args = parser.parse_args(argv)

    # change to the proper campaign directory if needed
    base = args.campaign
    if base == 'auto':
        base = util.find_campaign_root()

    try:
        chdir(base)
    except OSError as err:
        util.print_err("{}: '{}'".format(err.strerror, base))
        return 4

    # load settings data
    try:
        prefs = settings.Settings(args.debug)
    except OSError as err:
        util.print_err(err.strerror)
        return 4

    setting_errors = settings.lint_settings(prefs)
    if setting_errors:
        print("Error in settings")
        print("\n".join(setting_errors))
        return 5

    # show help when no input was given
    if not hasattr(args, 'func'):
        parser.print_help()
        return 0

    # get args as a dict
    full_args = vars(args)
    full_args['prefs'] = prefs

    # load default character path if search field is at its default
    if full_args.get('search') is None:
        full_args['search'] = [prefs.get('paths.required.characters')]

    # run the command
    try:
        result = args.func(full_args)
    except AttributeError as err:
        if args.debug:
            raise
        util.print_err(err)
        return 6

    # handle errors
    if not result.success:
        util.print_err(result)
        return result.errcode

    if not args.batch:
        # print any messages that were returned
        if result.printables:
            print("\n".join(result.printables))

        # open the resulting files, if allowed
        if result.openable:
            util.open_files(*result.openable, prefs=prefs)

    return 0