def render_header(self, outstream): """ Render the header template Will return immediately without rendering if `self.partial` is true. Args: outstream (stream): Output stream Returns: A util.Result object. Openable will not be set. """ if self.partial or not self.metadata_format: return result.Success() # load and render template header_file = self.prefs.get( "listing.templates.{list_format}.header.{metadata_format}".format( list_format=self.list_format, metadata_format=self.metadata_format)) if not header_file: return result.OptionError( errmsg="Unrecognized metadata format '{}'".format( self.metadata_format)) header_template = Template(filename=header_file, **self.encoding_options) outstream.write(header_template.render(**self.header_args)) return result.Success()
def report(*tags, search=None, ignore=None, fmt=None, outfile=None, **kwargs): """ Create a report for the given tags The tabular report shows how many characters have each unique value for each tag. Args: tag (list): Tag names to report on. Can contain strings and lists of strings. search (list): Paths to search for character files. Items can be strings or lists of strings. ignore (list): Paths to ignore fmt (str|None): Output format to use. Recognized values are defined in formatters.get_report_formatter. Pass "default" or None to get the format from settings. outfile (string|None): Filename to put the listed data. None and "-" print to stdout. prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain the output file if given. """ prefs = kwargs.get('prefs', settings.InternalSettings()) if not search: search = ['.'] if not ignore: ignore = [] ignore.extend(prefs.get_ignored_paths('report')) if not fmt or fmt == 'default': fmt = prefs.get('report.default_format') # use a list so we can iterate more than once characters = list(parser.get_characters(flatten(search), ignore)) # Construct a dict keyed by tag name whose values are Counters. Each Counter # is initialized with a flattened list of lists and we let it count the # duplicates. table_data = { tag: Counter(flatten([c.tags.get(tag, 'None') for c in characters])) for tag in flatten(tags) } formatter = formatters.get_report_formatter(fmt) if not formatter: return result.OptionError( errmsg="Cannot create output of format '{}'".format(fmt)) with util.smart_open(outfile, binary=(fmt in formatters.BINARY_TYPES)) as outstream: response = formatter(table_data, outstream=outstream, prefs=prefs) # pass errors straight through if not response.success: return response openable = [outfile] if outfile and outfile != '-' else None return result.Success(openable=openable)
def report(tables, outstream, **kwargs): """ Write a json representation of one or more sets of table data Table data format: The tables arg is much more permissive for this formatter than for the other formatters. Since this formatter just dumps that arg as json, it can contain basically anything. For compatability, however, the following format should be followed: The tables arg should be a dictionary of collections.Counter objects indexed by the name of the tag whose data is stored in the Counter. The tag name will be titleized and used as the header for that column of the report. Args: tables (dict): One or more objects with table data outstream (stream): Output stream Returns: A util.Result object. Openable will not be set. """ try: json.dump(tables, outstream) except TypeError as err: return result.Failure(errmsg=err) return result.Success()
def latest(thingtype, **kwargs): """ Open the latest plot and/or session file Args: prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain the path(s) to the requested file(s). """ prefs = kwargs.get('prefs', settings.InternalSettings()) plot_path = prefs.get('paths.required.plot') session_path = prefs.get('paths.required.session') if not (path.exists(plot_path) and path.exists(session_path)): return result.FSError( errmsg="Cannot access paths '{}' and/or '{}'".format( plot_path, session_path)) latest_plot = latest_file_info(plot_path, PLOT_REGEX) latest_session = latest_file_info(session_path, SESSION_REGEX) if thingtype == 'both': openable = [latest_plot['path'], latest_session['path']] elif thingtype == 'session': openable = [latest_session['path']] elif thingtype == 'plot': openable = [latest_plot['path']] else: return result.OptionError( errmsg="Unrecognized type '{}'".format(thingtype)) return result.Success(openable=openable)
def report(tables, outstream, **kwargs): """ Create one or more html tables Table data format: The tables arg must be a dictionary of collections.Counter objects indexed by the name of the tag whose data is stored in the Counter. The tag name will be titleized and used as the header for that column of the report. Args: tables (dict): Table data to use outstream (stream): Output stream prefs (Settings): Settings object. Used to get the location of template files. encoding (string): Encoding format of the output text. Overrides the value in settings. """ prefs = kwargs.get('prefs', settings.InternalSettings()) encoding = kwargs.get('encoding', prefs.get('report.html_encoding')) encoding_options = { 'output_encoding': encoding, 'encoding_errors': 'xmlcharrefreplace' } with tempfile.TemporaryDirectory() as tempdir: table_template = Template(filename=prefs.get("report.templates.html"), module_directory=tempdir, **encoding_options) for key, table in tables.items(): outstream.write(table_template.render(data=table, tag=key)) return result.Success()
def init(create_types=False, create_all=False, **kwargs): """ Create the basic directories for a campaign. This will create the directories this tool expects to find within a campaign. Other directories are left to the user. Args: create_types (bool): Whether to create directories for each character type create_all (bool): Whether to create all optional directories. campaign_name (str): Name of the campaign. Defaults to the name of the current directory. dryrun (bool): Do not create anything. This adds a string of changes that would be made to the returned Result object's printables variable. verbose (bool): Detail all changes made in the Result object's printables variable. prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will be empty. """ prefs = kwargs.get('prefs', settings.InternalSettings()) campaign_name = kwargs.get('campaign_name', path.basename(getcwd())) dryrun = kwargs.get('dryrun', False) verbose = kwargs.get('verbose', False) changelog = [] def log_change(message): if dryrun or verbose: changelog.append(message) def new_dir(path_name): log_change(path_name) if not dryrun: makedirs(path_name, mode=0o775, exist_ok=True) for key, required_path in prefs.get('paths.required').items(): if key in ["additional_paths"]: # create user-specified dirs for extra_path in required_path: new_dir(extra_path) continue new_dir(required_path) if not path.exists(prefs.get_settings_path('campaign')): new_dir('.npc') log_change(prefs.get_settings_path('campaign')) if not dryrun: with open(prefs.get_settings_path('campaign'), 'a') as settings_file: json.dump({'campaign_name': campaign_name}, settings_file, indent=4) if create_types or create_all: cbase = prefs.get('paths.required.characters') for type_path in prefs.get_type_paths(): new_dir(path.join(cbase, type_path)) return result.Success(printables=changelog)
def report(tables, outstream, **kwargs): """ Create one or more MultiMarkdown tables Table data format: The tables arg must be a dictionary of collections.Counter objects indexed by the name of the tag whose data is stored in the Counter. The tag name will be titleized and used as the header for that column of the report. Args: tables (dict): Table data to use. Tables has a very particular format. outstream (stream): Output stream prefs (Settings): Settings object. Used to get the location of template files. """ prefs = kwargs.get('prefs', settings.InternalSettings()) with tempfile.TemporaryDirectory() as tempdir: table_template = Template( filename=prefs.get("report.templates.markdown"), module_directory=tempdir) for key, table in tables.items(): outstream.write(table_template.render(data=table, tag=key)) return result.Success()
def reorg(*search, ignore=None, purge=False, verbose=False, commit=False, **kwargs): """ Move character files into the correct paths. Character files are moved so that their path matches the ideal path as closely as possible. No new directories are created. This function ignores tags not found in Character.KNOWN_TAGS. Args: search (list): Paths to search for character files. Items can be strings or lists of strings. ignore (list): Paths to ignore purge (bool): Whether empty directories should be deleted after all files have been moved. verbose (bool): Whether to print changes as they are made commit (bool): Whether to actually move files around prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will be empty. """ prefs = kwargs.get('prefs', settings.InternalSettings()) if not ignore: ignore = [] ignore.extend(prefs.get_ignored_paths('reorg')) show_changes = verbose or not commit changelog = [] base_path = prefs.get('paths.required.characters') if not path.exists(base_path): return result.FSError(errmsg="Cannot access '{}'".format(base_path)) if show_changes: changelog.append("Move characters") for parsed_character in parser.get_characters(flatten(search), ignore): new_path = util.create_path_from_character(parsed_character, base_path=base_path) if path.normcase(path.normpath(new_path)) != path.normcase(path.normpath(path.dirname(parsed_character['path']))): if show_changes: changelog.append("* Move {} to {}".format(parsed_character['path'], new_path)) if commit: try: shmove(parsed_character['path'], new_path) except OSError as e: if show_changes: changelog.append("\t- dest path already exists; skipping") if purge: if show_changes: changelog.append("Purge empty directories") for empty_path in util.find_empty_dirs(base_path): if show_changes: changelog.append("* Remove empty directory {}".format(empty_path)) if commit: rmdir(empty_path) return result.Success(printables=changelog)
def lint(*search, ignore=None, fix=False, strict=False, report=True, **kwargs): """ Check character files for completeness and correctness. This function checks that every character file has a few required tags, and applies extra checking for some character types. See util.Character.validate for details. This command normally ignores unknown tags. In strict mode, it will report the presence of any tag not expected by the character class. Args: search (list): Paths to search for character files. Items can be strings or lists of strings. ignore (list): Paths to ignore fix (bool): Whether to automatically fix errors when possible strict (bool): Whether to include non-critical errors and omissions report (bool): Do not include files in the return data, only problem descriptions prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. On success, openable attribute will contain a list of all files that had errors. """ prefs = kwargs.get('prefs', settings.InternalSettings()) if not ignore: ignore = [] ignore.extend(prefs.get_ignored_paths('lint')) openable = [] printable = [] # check each character characters = parser.get_characters(flatten(search), ignore) for character in characters: if character.tags('nolint').present: continue character.validate(strict) character.problems.extend( linters.lint(character, fix=fix, strict=strict, prefs=prefs)) # Report problems on one line if possible, or as a block if there's more than one if not character.valid: charpath = character.path if not report: openable.append(charpath) if len(character.problems) > 1: printable.append("File '{}':".format(charpath)) for detail in character.problems: printable.append(" {}".format(detail)) else: printable.append("{} in '{}'".format(character.problems[0], charpath)) return result.Success(openable=openable, printables=printable)
def _cp_template_for_char(name, character, prefs, fn=None): """ Copy the template for a character Copies the configured template file for `character` and optionally modifies the template's body using `fn`. Args: name (str): Character name character (Character): Character that needs a template prefs (Settings): Settings object used to find the template fn (callable): Optional function that is called before the new file is saved. It must accept a single string argument which will contain the template contents. Returns: Result object. Openable will contain the path to the new character file. """ # get template path template_path = prefs.get('types.{}.sheet_template'.format( character.type_key)) if not template_path: return result.ConfigError( errmsg="Could not find template {}".format(character.type_key)) # get path for the new file target_path = create_path_from_character(character, prefs=prefs) filename = name + path.splitext(template_path)[1] target_path = path.join(target_path, filename) if path.exists(target_path): return result.FSError( errmsg="Character '{}' already exists!".format(name)) # Add tags header = character.build_header() + '\n\n' # Copy template try: with open(template_path, 'r') as template_data: data = header + template_data.read() except IOError as err: return result.FSError(errmsg=err.strerror + " ({})".format(template_path)) if callable(fn): data = fn(data) # Write the new file try: with open(target_path, 'w') as char_file: char_file.write(data) except IOError as err: return result.FSError(errmsg=err.strerror + " ({})".format(target_path)) return result.Success(openable=[target_path])
def render(self, characters, outstream): """ Create a listing Args: characters (list): Character info dicts to show outstream (stream): Output stream Returns: A util.Result object. Openable will not be set. """ @lru_cache(maxsize=32) def _prefs_cache(key): """ Cache template paths """ return self.prefs.get(key) header_result = self.render_header(outstream) if not header_result: return header_result with tempfile.TemporaryDirectory() as tempdir: # directly access certain functions for speed _out_write = outstream.write total = len(characters) self.update_progress(0, total) for index, char in enumerate(characters): for sectioner in self.sectioners: if sectioner.would_change(char): sectioner.update_text(char) _out_write( sectioner.render_template(self.list_format, **self.encoding_options)) body_file = _prefs_cache( "listing.templates.{list_format}.character.{type}".format( list_format=self.list_format, type=char.type_key)) if not body_file: body_file = _prefs_cache( "listing.templates.{list_format}.character.default". format(list_format=self.list_format)) if not body_file: return result.ConfigError( errmsg= "Cannot find default character template for {list_format} listing" .format(list_format=self.list_format)) body_template = Template(filename=str(body_file), module_directory=tempdir, **self.encoding_options) _out_write(body_template.render(**self.char_args(char))) self.update_progress(index + 1, total) self.render_footer(outstream) return result.Success()
def find(*rules, search=None, ignore=None, **kwargs): """ Find characters in the campaign that match certain rules Searches for character objects in the campaign that match the given rules. To search an arbitrary list of Character objects, use find_characters. Args: rules (str): One or more strings that describe which characters to find. Passed directly to find_characters. search (list): Paths to search for character files. Items can be strings or lists of strings. ignore (list): Paths to ignore dryrun (bool): Whether to print the character file paths instead of opening them prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain a list of file paths to the matching Character objects. """ prefs = kwargs.get('prefs', settings.InternalSettings()) dryrun = kwargs.get('dryrun', False) if search is None: search = [] if not ignore: ignore = [] ignore.extend(prefs.get_ignored_paths('find')) rules = list(flatten(rules)) # use a list so we can iterate more than once characters = list(parser.get_characters(flatten(search), ignore)) filtered_chars = find_characters(rules, characters=characters) paths = [char.get('path') for char in filtered_chars] if dryrun: openable = [] printables = paths else: openable = paths printables = [] return result.Success(openable=openable, printables=printables)
def dump(*search, ignore=None, do_sort=False, metadata=False, outfile=None, **kwargs): """ Dump the raw character data, unaltered. Always formats the data as json. Args: search (List): Paths to search for character files ignore (List): Paths to ignore do_sort (bool): Whether to sort the characters before dumping metadata (bool): Whether to prepend metadata to the output outfile (string|None): Filename to put the dumped data. None and "-" print to stdout. prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. If outfile pointed to a real file, the openable attribute will contain that filename. """ prefs = kwargs.get('prefs', settings.InternalSettings()) if not ignore: ignore = [] ignore.extend(prefs.get_ignored_paths('dump')) sort_by = kwargs.get('sort_by', prefs.get('dump.sort_by')) characters = parser.get_characters(flatten(search), ignore) if do_sort: sorter = util.character_sorter.CharacterSorter(sort_by, prefs=prefs) characters = sorter.sort(characters) characters = [c.dump() for c in characters] # make some json if metadata: meta = {'meta': True, **prefs.get_metadata('json')} characters = itertools.chain([meta], characters) with util.smart_open(outfile) as outstream: json.dump([c for c in characters], outstream, cls=CharacterEncoder) openable = [outfile] if outfile and outfile != '-' else None return result.Success(openable=openable)
def latest(thingtype='', **kwargs): """ Open the latest plot and/or session file Args: thingtype (str): Type of the things to return. Use "session" to get the latest session file, "plot" to get the latest plot, and anything else to get all plot and session files. prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain the path(s) to the requested file(s). """ prefs = kwargs.get('prefs', settings.InternalSettings()) plot_dir = Path(prefs.get('paths.required.plot')) session_dir = Path(prefs.get('paths.required.session')) if not plot_dir.exists(): return result.FSError( errmsg="Cannot access plot path '{}'".format(plot_dir)) if not session_dir.exists(): return result.FSError( errmsg="Cannot access session path '{}'".format(session_dir)) plot_template = prefs.get('story.templates.plot') plot_regex = regex_from_template(plot_template) latest_plot = latest_file(plot_dir, plot_regex) session_template = prefs.get('story.templates.session') session_regex = regex_from_template(session_template) latest_session = latest_file(session_dir, session_regex) if thingtype == 'session': openable = [str(latest_session.path)] elif thingtype == 'plot': openable = [str(latest_plot.path)] else: openable = [str(latest_session.path), str(latest_plot.path)] return result.Success(openable=openable)
def open_settings(location, show_defaults=False, settings_type=None, **kwargs): """ Open the named settings file. If the desired settings file does not exist, an empty file is created and then opened. Args: location (str): Which settings file to open. One of 'user' or 'campaign'. show_defaults (bool): Whether the default settings file should be opened for reference alongside the specified settings file. settings_type (str): Determines which kind of settings file to open, like base settings or changeling settings. If left unset, base settings are opened. One of 'base' or 'changeling'. prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain the desired settings file. If true was passed in show_defaults, it will also contain the reference settings file. """ prefs = kwargs.get('prefs', settings.InternalSettings()) if settings_type: settings_type = settings_type.lower() target_path = prefs.get_settings_path(location, settings_type) if not target_path.exists(): dirname = target_path.parent makedirs(dirname, mode=0o775, exist_ok=True) with open(target_path, 'a') as settings_file: settings_file.write('{}') if show_defaults: openable = [ prefs.get_settings_path('default', settings_type, target_path.suffix), target_path ] else: openable = [target_path] return result.Success(openable=openable)
def listing(characters, outstream, *, metadata_format=False, metadata=None, **kwargs): """ Dump a json representation of all character data Internally, it adds the keys in meta if present and then calls `json.dump`. Args: characters (list): Character dicts to dump outstream (stream): Output stream metadata_format (bool): Whether to insert a metadata object. The metadata object will always include a title and creation date, along with the key `"meta": true` to distinguish it from character data. metadata (dict): Additional metadata keys. Ignored unless metadata_format is True. The keys 'meta', 'title', and 'created' will overwrite the generated values for those keys. Returns: A util.Result object. Openable will not be set. """ if not metadata: metadata = {} characters = [c.tags for c in characters] if metadata_format: meta = {'meta': True, **metadata} characters = [meta] + characters try: json.dump(characters, outstream, cls=CharacterEncoder) except TypeError as err: return result.Failure(errmsg=err) return result.Success()
def make_list(*search, ignore=None, fmt=None, metadata=None, title=None, outfile=None, **kwargs): """ Generate a listing of NPCs. The default listing templates ignore tags not found in Character.KNOWN_TAGS. Args: search (list): Paths to search for character files. Items can be strings or lists of strings. ignore (list): Paths to ignore fmt (str): Format of the output. Supported types are 'markdown', 'md', 'htm', 'html', and 'json'. Pass 'default' or None to get format from settings. metadata (str|None): Whether to include metadata in the output and what kind of metadata to use. Pass 'default' to use the format configured in Settings. The markdown format allows either 'mmd' (MultiMarkdown) or 'yfm'/'yaml' (Yaml Front Matter) metadata. The json format only allows one form of metadata, so pass any truthy value to include the metadata keys. title (str|None): The title to put in the metadata, if included. Overrides the title from settings. outfile (string|None): Filename to put the listed data. None and "-" print to stdout. do_sort (bool): Whether to avoid sorting altogether. Defaults to True. sort_by (string|None): Sort order for characters. Defaults to the value of "list_sort" in settings. headings (List[string]): List of tag names to group characters by partial (bool): Whether to omit headers and footers and just render body content. Defaults to false. prefs (Settings): Settings object to use. Uses internal settings by default. progress (function): Callback function to track the progress of generating a listing. Must accept the current count and total count. Should print to stderr. Not used by all formatters. Returns: Result object. Openable will contain the output file if given. """ prefs = kwargs.get('prefs', settings.InternalSettings()) if not ignore: ignore = [] ignore.extend(prefs.get_ignored_paths('listing')) do_sort = kwargs.get('do_sort', True) partial = kwargs.get('partial', False) update_progress = kwargs.get('progress', lambda i, t: False) sort_order = kwargs.get('sort_by', prefs.get('listing.sort_by')) if do_sort else [] headings = kwargs.get('headings', sort_order) characters = _process_directives( parser.get_characters(flatten(search), ignore)) if do_sort: sorter = util.character_sorter.CharacterSorter(sort_order, prefs=prefs) characters = sorter.sort(characters) if fmt == "default" or not fmt: fmt = prefs.get('listing.default_format') out_type = formatters.get_canonical_format_name(fmt) formatter = formatters.get_listing_formatter(out_type) if not formatter: return result.OptionError( errmsg="Cannot create output of format '{}'".format(out_type)) if metadata == 'default' and out_type != 'json': # Ensure 'default' metadata type gets replaced with the right default # metadata format. Irrelevant for json format. metadata_type = prefs.get( 'listing.metadata.{}.default_format'.format(out_type)) else: metadata_type = metadata meta = prefs.get_metadata(out_type) if title: meta['title'] = title header_offset = int(prefs.get('listing.base_header_level')) sectioners = [ formatters.sectioners.get_sectioner(key=g, heading_level=i + header_offset, prefs=prefs) for i, g in enumerate(headings) ] with util.smart_open(outfile, binary=(out_type in formatters.BINARY_TYPES)) as outstream: response = formatter(characters, outstream, metadata_format=metadata_type, metadata=meta, prefs=prefs, sectioners=sectioners, partial=partial, update_progress=update_progress) # pass errors straight through if not response.success: return response openable = [outfile] if outfile and outfile != '-' else None return result.Success(openable=openable)
def session(**kwargs): """ Create the files for a new game session. Finds the plot and session log files for the last session, copies the plot, and creates a new empty session log. If the latest plot file is ahead of the latest session, a new plot file will *not* be created. Likewise if the latest session file is ahead, a new session file will *not* be created. Args: prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain the current and previous session log and plot planning files. """ prefs = kwargs.get('prefs', settings.InternalSettings()) plot_path = prefs.get('paths.required.plot') session_path = prefs.get('paths.required.session') if not (path.exists(plot_path) and path.exists(session_path)): return result.FSError( errmsg="Cannot access paths '{}' and/or '{}'".format( plot_path, session_path)) latest_plot = latest_file_info(plot_path, PLOT_REGEX) latest_session = latest_file_info(session_path, SESSION_REGEX) new_number = min(latest_plot['number'], latest_session['number']) + 1 openable = [] if latest_session['exists']: if latest_session['number'] < new_number: # create new session log old_session_path = latest_session['path'] new_session_path = path.join( session_path, "session {num}{ext}".format(num=new_number, ext=latest_session['ext'])) shcopy(prefs.get('story.session_template'), new_session_path) else: # present existing session files, since we don't have to create one old_session_path = path.join( session_path, "session {num}{ext}".format(num=latest_session['number'] - 1, ext=latest_session['ext'])) new_session_path = latest_session['path'] openable.extend((new_session_path, old_session_path)) else: # no existing session, so just copy the template template_path = prefs.get('story.session_template') new_session_path = path.join( session_path, "session {num}{ext}".format(num=new_number, ext=path.splitext(template_path)[1])) shcopy(template_path, new_session_path) openable.append(new_session_path) if latest_plot['exists']: if latest_plot['number'] < new_number: # copy old plot old_plot_path = latest_plot['path'] new_plot_path = path.join( plot_path, "plot {num}{ext}".format(num=new_number, ext=latest_plot['ext'])) shcopy(old_plot_path, new_plot_path) else: # present existing plot files, since we don't have to create one old_plot_path = path.join( plot_path, "plot {num}{ext}".format(num=latest_plot['number'] - 1, ext=latest_plot['ext'])) new_plot_path = latest_plot['path'] openable.extend((new_plot_path, old_plot_path)) else: # no old plot to copy, so create a blank new_plot_path = path.join( plot_path, "plot {num}{ext}".format(num=new_number, ext=prefs.get('story.plot_ext'))) with open(new_plot_path, 'w') as new_plot: new_plot.write(' ') openable.append(new_plot_path) return result.Success(openable=openable)
def session(**kwargs): """ Create the files for a new game session. Finds the plot and session log files for the last session, copies the plot, and creates a new empty session log. If the latest plot file is ahead of the latest session, a new plot file will *not* be created. Likewise if the latest session file is ahead, a new session file will *not* be created. Args: prefs (Settings): Settings object to use. Uses internal settings by default. Returns: Result object. Openable will contain the current and previous session log and plot planning files. """ prefs = kwargs.get('prefs', settings.InternalSettings()) plot_dir = Path(prefs.get('paths.required.plot')) session_dir = Path(prefs.get('paths.required.session')) if not plot_dir.exists(): return result.FSError( errmsg="Cannot access plot path '{}'".format(plot_dir)) if not session_dir.exists(): return result.FSError( errmsg="Cannot access session path '{}'".format(session_dir)) plot_template = prefs.get('story.templates.plot') if SEQUENCE_KEYWORD not in str(plot_template): return result.ConfigError( errmsg="Plot template has no number placeholder ({})".format( SEQUENCE_KEYWORD)) plot_regex = regex_from_template(plot_template) latest_plot = latest_file(plot_dir, plot_regex) session_template = prefs.get('story.templates.session') if SEQUENCE_KEYWORD not in str(session_template): return result.ConfigError( errmsg="Session template has no number placeholder ({})".format( SEQUENCE_KEYWORD)) session_regex = regex_from_template(session_template) latest_session = latest_file(session_dir, session_regex) new_number = min(latest_plot.number, latest_session.number) + 1 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) plot_templates = flatten([ prefs.get('story.templates.plot'), prefs.get('story.templates.plot_extras') ]) copy_templates(plot_dir, plot_templates) session_templates = flatten([ prefs.get('story.templates.session'), prefs.get('story.templates.session_extras') ]) copy_templates(session_dir, session_templates) openable = [ str(latest_file(session_dir, session_regex).path), str(latest_file(plot_dir, plot_regex).path) ] old_session_name = session_template.name.replace(SEQUENCE_KEYWORD, str(new_number - 1)) old_session = session_dir.joinpath(old_session_name) if old_session.exists(): openable.append(str(old_session)) old_plot_name = plot_template.name.replace(SEQUENCE_KEYWORD, str(new_number - 1)) old_plot = plot_dir.joinpath(old_plot_name) if old_plot.exists(): openable.append(str(old_plot)) return result.Success(openable=openable)