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
def test_error_printer(capsys): util.print_err("Catchphrase!") out, err = capsys.readouterr() assert out == "" assert err == "Catchphrase!\n"
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)
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))
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))
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)
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)
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
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)
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