Source code for example3

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
*Threaded, Directory Walking Example*
===========================================

.. program:: example3.py

:Synopsis: Multithreaded example with signal handling that also demonstrates
    error handling.

:Platform: Unix, Windows, |python_version|

:Dependencies:

    .. Note: literal include is relative to the documentation directory

    .. literalinclude:: ../../../../examples/requirements.txt
        :language: text

:License: `MIT <https://karlredman.github.io/EditFrontMatter/LICENSE>`_

.. :moduleauthor: `Karl N. Redman <https://karlredman.github.io>`_

:Author: `Karl N. Redman <https://karlredman.github.io>`_

:homepage: `EditFrontMatter Example 3 <https://karlredman.github.io/EditFrontMatter/examples/example3/readme.html>`_

:Current Release:
    version: |release|

.. versionadded:: 0.0.1
    Initial Version
"""


import os
import sys
import re
import signal
import concurrent.futures as CFutures
import threading

from editfrontmatter import EditFrontMatter
from editfrontmatter import EditFrontMatter_Exception

# Globals:
print_lock = threading.Lock()
"""Tread lock used for printing"""

STOP_ALL = False
"""a global thread cancel flag"""


[docs]class Signal_Caught(Exception): """Exception to catch program signals""" pass
[docs]def SignalHandler(sig, frame): """Generic Signal handler. Description: Catches: SIGINT, SIGHUP """ message = ("Received signal {} on line {} in {}" .format(str(sig), str(frame.f_lineno), frame.f_code.co_filename)) raise Signal_Caught(message)
# catch signals signal.signal(signal.SIGINT, SignalHandler) signal.signal(signal.SIGHUP, SignalHandler)
[docs]class Derived_EditFrontMatter (EditFrontMatter): """EditFrontMatter derived class for example purposes"""
[docs] def __init__(self, **kwargs): """Init override with some self thread handling. :Description: Instantiates the class with some thread self awareness for example puproses. Also files are read during the init process. Attributes: self.EXCEPTION (bool): [default: False] Controls flow in methods if an exception is thrown during the init process. STOP_ALL (bool): [Global var] Allows for safe termination of the thread through flow control. Throws: EditFrontMatter_Exception """ # cancel thread if needed self.EXCEPTION = False global STOP_ALL if STOP_ALL: return try: EditFrontMatter.__init__(self, **kwargs) except EditFrontMatter_Exception: # we'll fail gracefully -probably a bad source file (i.e. missing # and yaml_end delimiter with print_lock: print("Error (exception): @Derived_EditFrontMatter.__init__(): {file_path}".format(file_path=self.file_path), file=sys.stderr) self.EXCEPTION = True
[docs] def run(self, write_file=False, extraVars_dict={}, keys_toDelete=[], *args, **kwargs) -> str: """A method overload to process files. :Description: Class method that performs setup for a call to :func:`editfrontmatter.EditFrontMatter.EditFrontMatter.run`. The method follows some flow control mechanisms for thread safety. The method supresses IOError Exception with a comment loged to stderr if the exception occurs. Args: write_file (bool): [default: False] Writes to the source file if True extraVars_dict (dict): [default: {}]: see :class:`editfrontmatter.EditFrontMatter.EditFrontMatter` keys_toDelete (dict): [default: []] see :class:`editfrontmatter.EditFrontMatter.EditFrontMatter` Calls: :func:`editfrontmatter.EditFrontMatter.EditFrontMatter.run` Returns: The altered file as a string or a null string on error """ # cancel thread if needed global STOP_ALL if STOP_ALL: return "" # init() could throw an exception. # Allow thread to die gracefully. if self.EXCEPTION: return "" # set fields to delete from yaml self.keys_toDelete = keys_toDelete # add jinja filter for callback self.add_JinjaFilter('canPublish', self.canPublish_func) # add vars to change and run the parent run() super().run(extraVars_dict=extraVars_dict, keys_toDelete=keys_toDelete) err = False if not self.has_source_data(): # file does not contain any data -punt with print_lock: print("Error: No source data: {file_path}". format(file_path=self.file_path), file=sys.stderr) err = True if not self.has_source_yaml(): # file does not contain yaml -punt with print_lock: print("Error: No source yaml: {file_path}". format(file_path=self.file_path), file=sys.stderr) err = True try: # write if allowed if write_file and not err: if not self.writeFile(): return "" # return the concatinated file data as a string return self.dumpFileData() if err is False else "" except IOError: # report that the file write failed and move on with print_lock: print("Error (IOError exception): @EditFrontMatter.run(): {file_path}" .format(file_path=self.filepath), file=sys.stderr) return ""
[docs] def canPublish_func(self, val) -> bool: """simple callback for a jinja2 filter variable.""" return True
[docs]def main(): """Main function for the program. :Description: The function set's up the environment and artificats for processing markdown files with yaml content to be updated. The function also manages threads that perform the work. Returns: * 0 on success * 1 on error """ # generic path - overridden by env var `TEMPLATE_DIR` TEST_DATA_DIR = os.path.abspath("../data/") if "TEST_DATA_DIR" in os.environ: TEST_DATA_DIR = os.path.abspath(os.environ.get("TEST_DATA_DIR")) + "/" # thread setup max_threads = 5 # data setup exclude_dirs = [os.path.abspath(TEST_DATA_DIR + "/example_dir_structure/a/c/b")] include_dirs = [os.path.abspath(TEST_DATA_DIR + "/example_dir_structure")] # jinja template filter template_str = ''.join(open(os.path.abspath(TEST_DATA_DIR + "/template1.j2"), "r").readlines()) # list of files to process in this tree files_to_process_list = [] # only files ending in `.md` extension filename_pattern = re.compile(".*\.md$") # noqa # make sure includes are not excludes -would mess up the os.walk() otherwise for top in include_dirs: if os.path.normpath(top) in (os.path.normpath(p) for p in exclude_dirs): # error print("Error: Included dir, {top}, found in exclude_dirs list". format(top), file=sys.stderr) exit(1) # build file list by walking the included directories for top in include_dirs: for root, dirs, files in os.walk(top, topdown=True): # exclude recursive path if it's in the exclusion list dirs[:] = [d for d in dirs if os.path.join(root, d) not in exclude_dirs] # save file to list for file in files: if filename_pattern.match(file): files_to_process_list.append(os.path.join(root, file)) # process files via threaded instances of our derived class processed_count = 0 threads = {} try: with CFutures.ThreadPoolExecutor(max_workers=max_threads) as executor: # submit threads threads = {executor.submit(Derived_EditFrontMatter( template_str=template_str, file_path=path, do_readFile=True).run, write_file=False, # Set True to write files extraVars_dict={'toc': 'no effect', 'hasMath': "false", 'addedVariable': ['one', 'two', 'three']}, keys_toDelete=['deleteme'] ): path for path in files_to_process_list} # wait for completed threads for future in CFutures.as_completed(threads): # our thread returns the file path file_path = threads[future] try: # retain the returned data from run data = future.result() except Exception as exc: # generic exception handler.... eeep! print("{file_path} generated an exception: {exc}". format(file_path=file_path, exc=exc), file=sys.stderr) else: # print the file path and file data returned from run() # print(f"processed: {file_path}\n{data}") # just print the file paths print("processed: {file_path}".format(file_path=file_path)) # detect if there was an error -with extra of cheeze if data != "": processed_count += 1 except Signal_Caught as e: # gracefully shutdown (caught c-c, etc) print("Exception: ....shutting down: {e}".format(e=str(e)), file=sys.stderr) global STOP_ALL STOP_ALL = True # report print("number of files: {fl}".format(fl=len(files_to_process_list))) print("number processed: {processed_count}".format(processed_count=processed_count)) if STOP_ALL: print("There were errors", file=sys.stderr) exit(1) print("done.")
if __name__ == '__main__': main()