From 34e7125e09f5cfd0163b04680575ad967141dbb3 Mon Sep 17 00:00:00 2001 From: Daniel Woste Date: Wed, 20 May 2020 10:01:08 +0200 Subject: [PATCH] Adds more drivers --- .gitignore | 4 +- docs/conf.py | 45 ++++++++++++- docs/drivers/copy_folder.rst | 11 ++++ docs/drivers/function.rst | 39 ++++++++++++ docs/drivers/index.rst | 63 ++++++++++++++++++- docs/drivers/report.rst | 28 +++++++++ docs/drivers/string.rst | 43 +++++++++++++ setup.py | 2 +- sphinxcontrib/collections/__init__.py | 2 +- sphinxcontrib/collections/api.py | 16 +++++ sphinxcontrib/collections/collections.py | 45 +++---------- sphinxcontrib/collections/drivers/__init__.py | 28 +++++++++ sphinxcontrib/collections/drivers/function.py | 38 +++++++++++ sphinxcontrib/collections/drivers/report.py | 30 +++++++++ .../collections/drivers/report.rst.template | 20 ++++++ sphinxcontrib/collections/drivers/string.py | 29 +++++++++ sphinxcontrib/collections/main.py | 51 +++++++++++++++ 17 files changed, 452 insertions(+), 42 deletions(-) create mode 100644 docs/drivers/function.rst create mode 100644 docs/drivers/report.rst create mode 100644 docs/drivers/string.rst create mode 100644 sphinxcontrib/collections/api.py create mode 100644 sphinxcontrib/collections/drivers/function.py create mode 100644 sphinxcontrib/collections/drivers/report.py create mode 100644 sphinxcontrib/collections/drivers/report.rst.template create mode 100644 sphinxcontrib/collections/drivers/string.py create mode 100644 sphinxcontrib/collections/main.py diff --git a/.gitignore b/.gitignore index 84e1022..463728a 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,6 @@ dmypy.json .pyre/ .envrc -.idea \ No newline at end of file +.idea + +_collections/ \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 70402bd..e66a936 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,10 +28,36 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'sphinx.ext.autodoc', 'sphinxcontrib.collections' ] + +def my_func(config): + string = 'This data gets written into {}'.format(config['target']) + return string + + +from sphinxcontrib.collections.drivers import Driver +from sphinxcontrib.collections.api import register_driver + + +class myDriver(Driver): + def run(self): + self.info('Run for source {}'.format(self.config['source'])) + + def clean(self): + self.info('Clean') + +register_driver('my_driver', myDriver) + + collections = { + 'driver_test': { + 'driver': 'my_driver', + 'source': '../tests/dummy/', + 'active': True, + }, 'copy_folder_test': { 'driver': 'copy_folder', 'source': '../tests/dummy/', @@ -44,11 +70,28 @@ 'target': 'dummy_new.rst', 'active': True, + }, + 'string_test': { + 'driver': 'string', + 'source': 'Take **this**!!!', + 'target': 'dummy_string.rst', + 'active': True, + }, + 'function_test': { + 'driver': 'function', + 'source': my_func, + 'target': 'dummy_function.rst', + 'active': True, + }, + 'report': { + 'driver': 'report', + 'target': 'doc_collection_report.rst', + 'active': True, } } -collections_final_clean = True +collections_final_clean = False # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/drivers/copy_folder.rst b/docs/drivers/copy_folder.rst index e402ae3..cb38aca 100644 --- a/docs/drivers/copy_folder.rst +++ b/docs/drivers/copy_folder.rst @@ -13,3 +13,14 @@ Copies a folder tree from ``source`` into your documentation project:: } } +Options +------- + +ignore +~~~~~~ + +List of file matches, which shall get ignored from copy. + +This variable is internally given to +`shutil.ignore_patterns `_. +So it must follow its syntax rules. \ No newline at end of file diff --git a/docs/drivers/function.rst b/docs/drivers/function.rst new file mode 100644 index 0000000..3480ee6 --- /dev/null +++ b/docs/drivers/function.rst @@ -0,0 +1,39 @@ +function +======== + +Executes a function referenced by ``source`` and writes its return value into a file specified by ``target``. + +.. code-block:: python + + def my_own_data(config): + string = 'This data gets written into {}'.format(config['target']) + + return string + + collections = { + 'my_files: { + 'driver': 'function', + 'source': my_own_data, + 'target': 'my_data/my_file.txt' + 'write_result': True + } + } + } + +The specified function gets 1 argument during the call: A dictionary which contains the complete configuration of the +collection. + +If return value is not None, the returned data is written to the file specified by ``target``. + +Options +------- + +write_result +~~~~~~~~~~~~ + +If ``write_result`` is False, no data is written by the driver. +But this could be done by the function itself. + +**Default**: ``True`` + + diff --git a/docs/drivers/index.rst b/docs/drivers/index.rst index c5aaaff..d28b8da 100644 --- a/docs/drivers/index.rst +++ b/docs/drivers/index.rst @@ -3,8 +3,69 @@ Drivers ======= +Drivers represents the technical function, which gets configured by the configuration given by a collection. + +Each collection must reference a single driver, which cares about: + +* Initial clean up +* Configured execution +* Final clean up + +``Sphinx-Collections`` already provides some major drivers, which support different use case. + .. toctree:: :maxdepth: 1 copy_folder - copy_file \ No newline at end of file + copy_file + string + function + report + +Own drivers +----------- + +You can specify own drivers directly inside your ``conf.py`` file. + +Using own drivers instead of e.g. a pure function call has several advantages: + +* Configuration handling. +* Correct and easy logging. +* Executed during correct Sphinx phases. +* Integrated clean-up. +* Report capabilities. + +.. code-block:: + + from sphinxcontrib.collections.drivers import Driver + from sphinxcontrib.collections.api import register_driver + + + class myDriver(Driver): + def run(self): + self.info('Run for source {}'.format(self.config['source'])) + + def clean(self): + self.info('Clean') + + register_driver('my_driver', myDriver) + + collections = { + 'my_river_test': { + 'driver': 'my_driver', + 'source': '../tests/dummy/', + 'active': True, + }, + +If you have created an awesome driver, please consider to provide it to ``Sphinx-Collections`` by creating +a PR on our `github project `_ . +This would help our little Sphinx community a lot. Thanks! + +Driver class +~~~~~~~~~~~~ + +.. autoclass:: sphinxcontrib.collections.drivers.Driver + :members: + :undoc-members: + :private-members: + :special-members: __init__ diff --git a/docs/drivers/report.rst b/docs/drivers/report.rst new file mode 100644 index 0000000..ca3c7a9 --- /dev/null +++ b/docs/drivers/report.rst @@ -0,0 +1,28 @@ +report +====== + +Creates a collection report in file specified by ``target``. + +Please be sure to specify this report as one of the latest collections, otherwise other +collections have not been executed before this report gets generated. + +.. code-block:: python + + collections = { + 'my_collection_report: { + 'driver': 'report', + 'target': 'reports/collections.rst' + } + } + } + + +The following template is used to build the report: + +.. literalinclude:: ../../sphinxcontrib/collections/drivers/report.rst.template + +**Example**: + +This is the report of the latest run for this documentation. + +.. literalinclude:: /_collections/doc_collection_report.rst \ No newline at end of file diff --git a/docs/drivers/string.rst b/docs/drivers/string.rst new file mode 100644 index 0000000..ffb7039 --- /dev/null +++ b/docs/drivers/string.rst @@ -0,0 +1,43 @@ +string +====== + +Copies a string defined in ``source`` into a file specified by ``target``. + +.. code-block:: python + + collections = { + 'my_files: { + 'driver': 'string', + 'source': 'Awesome, this is nice', + 'target': 'my_data/my_file.txt' + } + } + } + +You can also use more complex strings by assigning them to a variable. + +.. code-block:: python + + my_string = """ + Headline + ======== + + Ohh **awesome**! + + Multiline! + + .. codeblock:: rst + + Works also + ---------- + """ + + collections = { + 'my_files: { + 'driver': 'string', + 'source': my_string, + 'target': 'my_data/my_file.txt' + } + } + } + diff --git a/setup.py b/setup.py index 513b7ab..dbe59c2 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import os from setuptools import setup, find_packages -requires = ['sphinx>2.0'] +requires = ['sphinx>2.0', 'jinja2'] setup( name='sphinx-collections', diff --git a/sphinxcontrib/collections/__init__.py b/sphinxcontrib/collections/__init__.py index 1178d1b..562254c 100644 --- a/sphinxcontrib/collections/__init__.py +++ b/sphinxcontrib/collections/__init__.py @@ -1 +1 @@ -from sphinxcontrib.collections.collections import setup # NOQA +from sphinxcontrib.collections.main import setup # NOQA diff --git a/sphinxcontrib/collections/api.py b/sphinxcontrib/collections/api.py new file mode 100644 index 0000000..da3f49f --- /dev/null +++ b/sphinxcontrib/collections/api.py @@ -0,0 +1,16 @@ +from sphinxcontrib.collections.collections import DRIVERS +from sphinxcontrib.collections.drivers import Driver + + +def register_driver(name, driver_class): + if not issubclass(driver_class, Driver): + raise SphinxCollectionsApiError('Given driver class must be a subclass of the main Driver class.') + + try: + DRIVERS[name] = driver_class + except KeyError: + raise SphinxCollectionsApiError('Driver with name {} already exists.'.format(name)) + + +class SphinxCollectionsApiError(BaseException): + pass diff --git a/sphinxcontrib/collections/collections.py b/sphinxcontrib/collections/collections.py index 9445c68..3dac010 100644 --- a/sphinxcontrib/collections/collections.py +++ b/sphinxcontrib/collections/collections.py @@ -1,11 +1,13 @@ -import os import sphinx +import os from pkg_resources import parse_version from sphinxcontrib.collections.drivers.copy_folder import CopyFolderDriver from sphinxcontrib.collections.drivers.copy_file import CopyFileDriver -from sphinxcontrib.collections.directives.if_collection import CollectionsIf, CollectionsIfDirective +from sphinxcontrib.collections.drivers.string import StringDriver +from sphinxcontrib.collections.drivers.function import FunctionDriver +from sphinxcontrib.collections.drivers.report import ReportDriver sphinx_version = sphinx.__version__ if parse_version(sphinx_version) >= parse_version("1.6"): @@ -15,49 +17,18 @@ logging.basicConfig() # Only need to do this once - LOG = logging.getLogger(__name__) -VERSION = 0.1 COLLECTIONS = [] DRIVERS = { 'copy_folder': CopyFolderDriver, - 'copy_file': CopyFileDriver + 'copy_file': CopyFileDriver, + 'string': StringDriver, + 'function': FunctionDriver, + 'report': ReportDriver, } -def setup(app): - """ - Configures Sphinx - - Registers: - - * config values - * receivers for events - * directives - """ - - # Registers config options - app.add_config_value('collections', {}, 'html') - app.add_config_value('collections_target', '_collections', 'html') - app.add_config_value('collections_clean', True, 'html') - app.add_config_value('collections_final_clean', True, 'html') - - # Connects handles to events - app.connect('config-inited', collect_collections) - app.connect('config-inited', clean_collections) - app.connect('config-inited', execute_collections) - app.connect('build-finished', final_clean_collections) - - app.add_node(CollectionsIf) - app.add_directive('if-collection', CollectionsIfDirective) - app.add_directive('ifc', CollectionsIfDirective) - - return {'version': VERSION, - 'parallel_read_safe': True, - 'parallel_write_safe': True} - - def collect_collections(app, config): LOG.info('Read in collections ...') for name, collection in config.collections.items(): diff --git a/sphinxcontrib/collections/drivers/__init__.py b/sphinxcontrib/collections/drivers/__init__.py index f217a94..fd3905c 100644 --- a/sphinxcontrib/collections/drivers/__init__.py +++ b/sphinxcontrib/collections/drivers/__init__.py @@ -25,9 +25,21 @@ def __init__(self, collection, config=None): self.config = config def run(self): + """ + Is the main routine for the driver. + + Must be implement by the parent driver class. + """ raise NotImplementedError('run() function must be implemented by driver {} itself.'.format(self.name)) def clean(self): + """ + Cares about cleaning up the working space from actions performed in run(). + + Gets called normally at the beginning and add the end of collection handling. + + Must be implement by the parent driver class. + """ raise NotImplementedError('clean() function must be implemented by driver {} itself.'.format(self.name)) def error(self, message, e=None): @@ -49,9 +61,25 @@ def error(self, message, e=None): self._log.error(('{}{}'.format(self._prefix, message))) def info(self, message): + """ + Writes a log message of level INFO. + + Sets collection and driver information as prefix in front of the message + + :param message: string + :return: None + """ self._log.info('{}{}'.format(self._prefix, message)) def debug(self, message): + """ + Writes a log message of level DEBUG. + + Sets collection and driver information as prefix in front of the message + + :param message: string + :return: None + """ self._log.debug('{}{}'.format(self._prefix, message)) diff --git a/sphinxcontrib/collections/drivers/function.py b/sphinxcontrib/collections/drivers/function.py new file mode 100644 index 0000000..44ae2bb --- /dev/null +++ b/sphinxcontrib/collections/drivers/function.py @@ -0,0 +1,38 @@ +import os + +from inspect import isfunction +from sphinxcontrib.collections.drivers import Driver + + +class FunctionDriver(Driver): + + def run(self): + self.info('Run function...') + + if not isfunction(self.config['source']): + self.error('Source option must be a user-defined function. Nothing else.') + return + + try: + function = self.config['source'] + result = function(self.config) + except Exception as e: + self.error('Problems during executing function', e) + + write_result = self.config.get('write_result', True) + if write_result and result is not None: + try: + with open(self.config['target'], 'w') as target_file: + target_file.writelines(result.split('\n')) + except IOError as e: + self.error('Problems during writing function result to file', e) + + def clean(self): + try: + os.remove(self.config['target']) + self.info('File deleted: {}'.format(self.config['target'])) + except FileNotFoundError: + pass # Already cleaned? I'm okay with it. + except IOError as e: + self.error('Problems during cleaning for collection {}'.format(self.config['name']), e) + diff --git a/sphinxcontrib/collections/drivers/report.py b/sphinxcontrib/collections/drivers/report.py new file mode 100644 index 0000000..19f512d --- /dev/null +++ b/sphinxcontrib/collections/drivers/report.py @@ -0,0 +1,30 @@ +import os +from jinja2 import Template +from sphinxcontrib.collections.drivers import Driver + + +class ReportDriver(Driver): + + def run(self): + from sphinxcontrib.collections.collections import COLLECTIONS + + self.info('Add collection report to file...') + + template_path = os.path.join(os.path.dirname(__file__), 'report.rst.template') + template = Template(open(template_path).read()) + result = template.render(collections=COLLECTIONS) + try: + with open(self.config['target'], 'w') as target_file: + target_file.write(result) + except IOError as e: + self.error('Problems during writing collection report to file', e) + + def clean(self): + try: + os.remove(self.config['target']) + self.info('Collection report deleted: {}'.format(self.config['target'])) + except FileNotFoundError: + pass # Already cleaned? I'm okay with it. + except IOError as e: + self.error('Problems during cleaning for collection {}'.format(self.config['name']), e) + diff --git a/sphinxcontrib/collections/drivers/report.rst.template b/sphinxcontrib/collections/drivers/report.rst.template new file mode 100644 index 0000000..2c923f1 --- /dev/null +++ b/sphinxcontrib/collections/drivers/report.rst.template @@ -0,0 +1,20 @@ +Collection Report +================= + +{% for collection in collections %} +{{ collection.name }} +{{ "-" * collection.name|length }} +**Active**: {{ collection.active }} + +**Executed**: {{ collection.executed }} + +**Source**: {{ collection.config['source'] }} + +**Target**: {{ collection.config['target'] }} + +.. code-block:: text + + {{ collection.config }} + + +{% endfor %} diff --git a/sphinxcontrib/collections/drivers/string.py b/sphinxcontrib/collections/drivers/string.py new file mode 100644 index 0000000..b5dab1b --- /dev/null +++ b/sphinxcontrib/collections/drivers/string.py @@ -0,0 +1,29 @@ +import os + +from sphinxcontrib.collections.drivers import Driver + + +class StringDriver(Driver): + + def run(self): + self.info('Add string to file...') + + if not isinstance(self.config['source'], str): + self.error('Source option must be a string. Nothing else.') + return + + try: + with open(self.config['target'], 'w') as target_file: + target_file.writelines(self.config['source'].split('\n')) + except IOError as e: + self.error('Problems during writing string to file', e) + + def clean(self): + try: + os.remove(self.config['target']) + self.info('File deleted: {}'.format(self.config['target'])) + except FileNotFoundError: + pass # Already cleaned? I'm okay with it. + except IOError as e: + self.error('Problems during cleaning for collection {}'.format(self.config['name']), e) + diff --git a/sphinxcontrib/collections/main.py b/sphinxcontrib/collections/main.py new file mode 100644 index 0000000..c7ba4c5 --- /dev/null +++ b/sphinxcontrib/collections/main.py @@ -0,0 +1,51 @@ +import sphinx + +from pkg_resources import parse_version + +from sphinxcontrib.collections.directives.if_collection import CollectionsIf, CollectionsIfDirective + +from sphinxcontrib.collections.collections import collect_collections, clean_collections, \ + execute_collections, final_clean_collections + +sphinx_version = sphinx.__version__ +if parse_version(sphinx_version) >= parse_version("1.6"): + from sphinx.util import logging +else: + import logging + + logging.basicConfig() # Only need to do this once + +LOG = logging.getLogger(__name__) +VERSION = 0.1 + + +def setup(app): + """ + Configures Sphinx + + Registers: + + * config values + * receivers for events + * directives + """ + + # Registers config options + app.add_config_value('collections', {}, 'html') + app.add_config_value('collections_target', '_collections', 'html') + app.add_config_value('collections_clean', True, 'html') + app.add_config_value('collections_final_clean', True, 'html') + + # Connects handles to events + app.connect('config-inited', collect_collections) + app.connect('config-inited', clean_collections) + app.connect('config-inited', execute_collections) + app.connect('build-finished', final_clean_collections) + + app.add_node(CollectionsIf) + app.add_directive('if-collection', CollectionsIfDirective) + app.add_directive('ifc', CollectionsIfDirective) + + return {'version': VERSION, + 'parallel_read_safe': True, + 'parallel_write_safe': True} \ No newline at end of file