Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
0.1a10 (unreleased)
-------------------

- add entry_points plugins support for render_filename
[Jean-Philippe Camguilhem]

- move exceptions to bobexceptions
[Jean-Philippe Camguilhem]

Expand Down
6 changes: 6 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ Source documentation

.. automodule:: mrbob.hooks
:members:

:mod:`mrbob.plugins` -- Included plugins
-----------------------------------------

.. automodule:: mrbob.plugins
:members:
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Welcome to mr.bob's documentation!

userguide.rst
templateauthor.rst
pluginauthor.rst
other.rst
developer.rst
api.rst
Expand Down
81 changes: 81 additions & 0 deletions docs/source/pluginauthor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@

.. _`writing your plugin`:

Writing your own plugin
=========================


render_filename plugin
-----------------------

With your own plugin you can extend or override ``render_filename`` in a very flexible way.

structure
**********

A render_filename plugin class must have an **order** attribute (optional but fully recommended) and a **get_filename** method.

.. warning:: If it doesn't provide this method an ErrorAttribute is raised.

`get_filename` must return a tuple where the first element is the rended new filename, and the second a boolean



plugin sample

.. code-block:: python

class NoBarInFilename():
order = 15

def __init__(self, filename, variables, will_continue=True):
self.filename = filename
self.variables = variables
self.will_continue = will_continue

def get_filename(self):
if 'bar' in self.filename:
self.filename = None
else:
self.filename = 'fake_foo_' + self.filename

return self.filename, self.will_continue

code behavior

.. literalinclude:: ../../mrbob/rendering.py
:lines: 133-143


If filename is None or the boolean is False, filename is immediatly returned, otherwise new filename will be interpreted over the mrbob render_filename function.


Register your plugin(s)
************************

You register your plugin(s) to mrbob with classic setuptools entrypoints within your setup.py egg file.

.. code-block:: python

entry_points='''
# -*- Entry points: -*-
[mr.bob.plugins]
render_filename=bobplugins.pkg.module:NoFooInFilename
render_filename=bobplugins.pkg.module:NoBarInFilename
render_filename=bobplugins.pkg.module:NeitherBarOrFooInFilename

''',

Here we have 3 render_filename plugins, assume that :

+ NoFooInFilename `order` attribute is 10
+ NoBarInFilename `order` attribute is 15
+ NeitherBarOrFooInFilename `order` attribute is 20

If you don't specify a `-r, --rdr-fname-plugin-target` NeitherBarOrFooInFilename will be loaded due to its order attribute, higher is prefered.

If you want to load NoBarInFilename, just invoque mr bob with -r 15 . If you target a non registered `order` an ErrorAttribute is raised ::

AttributeError: No plugin target 18 ! Registered are [10, 15, 20]

If you register 2 max order plugins, an alphabetical asc sort based on namespace-pkg-classname will return the last
11 changes: 11 additions & 0 deletions docs/source/userguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Once you install mr.bob, the ``mrbob`` command is available::
Don't prompt for input. Fail if questions are required
but not answered
-q, --quiet Suppress all but necessary output
-r RDR_FNAME_PLUGIN_TARGET, --rdr-fname-plugin-target RDR_FNAME_PLUGIN_TARGET
Specify target plugin like 10|20

By default, the target directory is the current folder. The most basic use case is rendering a template from a relative folder::

Expand Down Expand Up @@ -224,3 +226,12 @@ templates and contribute them to this list by making a `pull request <https://gi
- `bobtemplates.ielectric <https://github.com/iElectric/bobtemplates.ielectric>`_
- `bobtemplates.kotti <https://github.com/Kotti/bobtemplates.kotti>`_
- `bobtemplates.niteoweb <https://github.com/niteoweb/bobtemplates.niteoweb>`_


Collection of community plugins
-------------------------------

You are encouraged to use the ``bobplugins.something`` Python egg namespace to write
templates and contribute them to this list by making a `pull request <https://github.com/iElectric/mr.bob>`_.

- `bobplugins.jpcw <https://github.com/jpcw/bobplugins.jpcw>`_
6 changes: 6 additions & 0 deletions mrbob/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
#dest='overwrite',
#action='store_true',
#help='Always overwrite')
parser.add_argument('-r', '--rdr-fname-plugin-target',
type=int,
default=None,
dest='rdr_fname_plugin_target',
help='Specify target plugin like 10|20')


def main(args=sys.argv[1:]):
Expand Down Expand Up @@ -114,6 +119,7 @@ def main(args=sys.argv[1:]):
'quiet': options.quiet,
'remember_answers': options.remember_answers,
'non_interactive': options.non_interactive,
'rdr_fname_plugin_target': options.rdr_fname_plugin_target,
}

bobconfig = update_config(update_config(global_bobconfig, file_bobconfig), cli_bobconfig)
Expand Down
12 changes: 9 additions & 3 deletions mrbob/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import six
from importlib import import_module

import mrbob.plugins as plugins
from .rendering import render_structure
from .parsing import (
parse_config,
Expand Down Expand Up @@ -113,8 +114,8 @@ class Configurator(object):
- :attr:`template_dir` is root directory of the template
- :attr:`is_tempdir` if template directory is temporary (when using zipfile)
- :attr:`templateconfig` dictionary parsed from `template` section
- :attr:`questions` ordered list of `Question instances to be asked
- :attr:`bobconfig` dictionary parsed from `mrbob` section of the config
- :attr:`questions` ordered list of `Question` instances to be asked
- :attr:`bobconfig` dictionary parsed from `mrbobx` section of the config

"""

Expand All @@ -133,6 +134,7 @@ def __init__(self,
self.variables = variables
self.defaults = defaults
self.target_directory = os.path.realpath(target_directory)
self.plugins_options = {}

# figure out template directory
self.template_dir, self.is_tempdir = parse_template(template)
Expand Down Expand Up @@ -165,6 +167,10 @@ def __init__(self,
self.quiet = maybe_bool(self.bobconfig.get('quiet', False))
self.remember_answers = maybe_bool(self.bobconfig.get('remember_answers', False))
self.ignored_files = self.bobconfig.get('ignored_files', '').split()
self.plugins_options['render_filename'] = self.bobconfig.get('rdr_fname_plugin_target', None)

# load plugins
plugins.PLUGINS = dict((key, plugins.load_plugin(key, target=value)) for key, value in self.plugins_options.items())

# parse template settings
self.templateconfig = self.config['template']
Expand Down Expand Up @@ -237,7 +243,7 @@ class Question(object):
:param help: Optional help message
:param pre_ask_question: Space limited functions in dotted notation to ask before the question is asked
:param post_ask_question: Space limited functions in dotted notation to ask aster the question is asked
:param **extra: Any extra parameters stored for possible extending of `Question` functionality
:param \**extra: Any extra parameters stored for possible extending of `Question` functionality

Any of above parameters can be accessed as an attribute of `Question` instance.

Expand Down
64 changes: 64 additions & 0 deletions mrbob/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-

"""Plugins loader.

You can register your own plugins with entry_points.

Code a class in your egg and register it within your setup.py file

.. code-block:: python

entry_points='''
# -*- Entry points: -*-
[mr.bob.plugins]
render_filename=bobplugins.pkg.module:FooRenderFilename
''',

If there are multiples plugins with same name, you could push yours with
different **order** attributes.

If you don't specify an order target `-r, --rdr-fname-plugin-target` the
plugin with max **order** attribute is prefered,
otherwise alphabetic sort on namespace returns the last entry.

If you specify a bad plugin target an error is raised ::

AttributeError: No plugin target 15 ! Registered are [10, 20]


.. note:: Please notice that just mrbob.rendering.render_filename is
actually plugguable, but code infra is here.

"""

__docformat__ = 'restructuredtext en'

import operator
import pkg_resources

PLUGINS = {}
entries = [entry for entry in
pkg_resources.iter_entry_points(group='mr.bob.plugins')]


def load_plugin(plugin, entries=entries, target=None):
"""Load and sort possibles plugins from pkg."""

if entries:
plugins = [(ep, '%d-%s-%s' % (getattr(ep.load(False), 'order', 10),
ep.module_name, ep.name))
for ep in entries if ep.name == plugin]
ordered_plugins = sorted(plugins, key=operator.itemgetter(1))
if target is not None:
targets = [ep for ep in ordered_plugins
if ep[1].split('-')[0] == str(target)]
if targets:
return targets[-1][0].load(False)
else:
registered = [int(ep[1].split('-')[0]) for ep in ordered_plugins]
raise AttributeError('No plugin target %d ! Registered are %s' % (target,
registered))

return ordered_plugins[-1][0].load(False)

# vim:set et sts=4 ts=4 tw=80:
90 changes: 56 additions & 34 deletions mrbob/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import stat

from jinja2 import Environment, StrictUndefined

import mrbob.plugins as plugins

jinja2_env = Environment(
block_start_string="{{%",
Expand Down Expand Up @@ -63,6 +63,7 @@ def render_structure(fs_source_root, fs_target_root, variables, verbose,
with values from the variables, i.e. a file named `+name+.py.bob` given a
dictionary {'name': 'bar'} would be rendered as `bar.py`.
"""

ignored_files.extend(DEFAULT_IGNORED)
if not isinstance(fs_source_root, six.text_type): # pragma: no cover
fs_source_root = six.u(fs_source_root)
Expand All @@ -71,47 +72,68 @@ def render_structure(fs_source_root, fs_target_root, variables, verbose,
for local_file in local_files:
if matches_any(local_file, ignored_files):
continue
render_template(
path.join(fs_source_dir, local_file),
render_filename(fs_target_dir, variables),
variables,
verbose,
renderer,
)
filename = render_filename(fs_target_dir, variables)
if filename is not None:
render_template(
path.join(fs_source_dir, local_file),
filename,
variables,
verbose,
renderer,
)
for local_directory in local_directories:
abs_dir = render_filename(path.join(fs_target_dir, local_directory), variables)
if not path.exists(abs_dir):
if verbose:
print(six.u("mkdir %s") % abs_dir)
os.mkdir(abs_dir)
if abs_dir is not None:
if not path.exists(abs_dir):
if verbose:
print(six.u("mkdir %s") % abs_dir)
os.mkdir(abs_dir)


def render_template(fs_source, fs_target_dir, variables, verbose, renderer):
filename = path.split(fs_source)[1]
if filename.endswith('.bob'):
filename = filename.split('.bob')[0]
fs_target_path = path.join(fs_target_dir, render_filename(filename, variables))
if verbose:
print(six.u("Rendering %s to %s") % (fs_source, fs_target_path))
fs_source_mode = stat.S_IMODE(os.stat(fs_source).st_mode)
with codecs.open(fs_source, 'r', 'utf-8') as f:
source_output = f.read()
output = renderer(source_output, variables)
# append newline due to jinja2 bug, see https://github.com/iElectric/mr.bob/issues/30
if source_output.endswith('\n') and not output.endswith('\n'):
output += '\n'
with codecs.open(fs_target_path, 'w', 'utf-8') as fs_target:
fs_target.write(output)
os.chmod(fs_target_path, fs_source_mode)
else:
fs_target_path = path.join(fs_target_dir, render_filename(filename, variables))
if verbose:
print(six.u("Copying %s to %s") % (fs_source, fs_target_path))
copy2(fs_source, fs_target_path)
return path.join(fs_target_dir, filename)
filename = render_filename(path.split(fs_source)[1], variables)
if filename is not None:
if filename.endswith('.bob'):
filename = filename.split('.bob')[0]
fs_target_path = path.join(fs_target_dir, filename)
if verbose:
print(six.u("Rendering %s to %s") % (fs_source, fs_target_path))
fs_source_mode = stat.S_IMODE(os.stat(fs_source).st_mode)
with codecs.open(fs_source, 'r', 'utf-8') as f:
source_output = f.read()
output = renderer(source_output, variables)
if source_output.endswith('\n') and not output.endswith('\n'):
output += '\n'
with codecs.open(fs_target_path, 'w', 'utf-8') as fs_target:
fs_target.write(output)
os.chmod(fs_target_path, fs_source_mode)
else:
fs_target_path = path.join(fs_target_dir, filename)
if verbose:
print(six.u("Copying %s to %s") % (fs_source, fs_target_path))
copy2(fs_source, fs_target_path)
return path.join(fs_target_dir, filename)


def render_filename(filename, variables):
"""Overridable (via entry_points) rendering.

Now plugguable, see :ref:`writing your plugin` to modify your replacements or other variables substitutions.

This is a useful option to generate templates or conditionnal rendering.

"""

plugin = plugins.PLUGINS.get('render_filename')
if plugin is not None:
if getattr(plugin, 'get_filename', None) is None:
raise AttributeError('get_filename method not found in plugin')
else:
plug_inst = plugin(filename, variables)
filename, will_continue = plug_inst.get_filename()
if filename is None or not will_continue:
return filename

variables_regex = re.compile(r"\+[^+%s]+\+" % re.escape(os.sep))

replaceables = variables_regex.findall(filename)
Expand Down
Loading