- `include_file_patterns` property accepts globs to filter by file name. - `include_paths` accepts paths relative to content. - `begin_node` and `node_complete` honor `include_paths` - `begin_text_resource`, `text_resource_complete`, `begin_binary_resource` and `binary_resource_complete` honor both.main
@@ -1,3 +1,13 @@ | |||
Version 0.8.5a6 | |||
============================================================ | |||
* Plugins now support inclusion filters. (Issue #112) | |||
- `include_file_patterns` property accepts globs to filter by file name. | |||
- `include_paths` accepts paths relative to content. | |||
- `begin_node` and `node_complete` honor `include_paths` | |||
- `begin_text_resource`, `text_resource_complete`, `begin_binary_resource` | |||
and `binary_resource_complete` honor both. | |||
Version 0.8.5a5 | |||
============================================================ | |||
@@ -1,4 +1,4 @@ | |||
Version 0.8.5a5 | |||
Version 0.8.5a6 | |||
A brand new **hyde** | |||
==================== | |||
@@ -615,4 +615,4 @@ class Folder(FS): | |||
""" | |||
Return a `FolderLister` object | |||
""" | |||
return FolderLister(self) | |||
return FolderLister(self) |
@@ -12,6 +12,7 @@ from hyde.util import getLoggerWithNullHandler, first_match, discover_executable | |||
from hyde.model import Expando | |||
from functools import partial | |||
import fnmatch | |||
import os | |||
import re | |||
@@ -40,14 +41,16 @@ class PluginProxy(object): | |||
# logger.debug( | |||
# "\tCalling plugin [%s]", | |||
# plugin.__class__.__name__) | |||
function = getattr(plugin, method_name) | |||
res = function(*args) | |||
targs = list(args) | |||
if len(targs): | |||
last = targs.pop() | |||
res = res if res else last | |||
targs.append(res) | |||
args = tuple(targs) | |||
checker = getattr(plugin, 'should_call__' + method_name) | |||
if checker(*args): | |||
function = getattr(plugin, method_name) | |||
res = function(*args) | |||
targs = list(args) | |||
if len(targs): | |||
last = targs.pop() | |||
res = res if res else last | |||
targs.append(res) | |||
args = tuple(targs) | |||
return res | |||
return __call_plugins__ | |||
@@ -81,18 +84,30 @@ class Plugin(object): | |||
""" | |||
Syntactic sugar for template methods | |||
""" | |||
result = None | |||
if name.startswith('t_') and self.template: | |||
attr = name[2:] | |||
if hasattr(self.template, attr): | |||
return self.template[attr] | |||
result = self.template[attr] | |||
elif attr.endswith('_close_tag'): | |||
tag = attr.replace('_close_tag', '') | |||
return partial(self.template.get_close_tag, tag) | |||
result = partial(self.template.get_close_tag, tag) | |||
elif attr.endswith('_open_tag'): | |||
tag = attr.replace('_open_tag', '') | |||
return partial(self.template.get_open_tag, tag) | |||
result = partial(self.template.get_open_tag, tag) | |||
elif name.startswith('should_call__'): | |||
(_, _, method) = name.rpartition('__') | |||
if (method in ('begin_text_resource', 'text_resource_complete', | |||
'begin_binary_resource', 'binary_resource_complete')): | |||
result = self._file_filter | |||
elif (method in ('begin_node', 'node_complete')): | |||
result = self._dir_filter | |||
else: | |||
def always_true(*args, **kwargs): | |||
return True | |||
result = always_true | |||
return super(Plugin, self).__getattribute__(name) | |||
return result if result else super(Plugin, self).__getattribute__(name) | |||
@property | |||
def settings(self): | |||
@@ -139,6 +154,44 @@ class Plugin(object): | |||
""" | |||
pass | |||
def _file_filter(self, resource, *args, **kwargs): | |||
""" | |||
Returns True if the resource path matches the filter property in | |||
plugin settings. | |||
""" | |||
if not self._dir_filter(resource.node, *args, **kwargs): | |||
return False | |||
try: | |||
filters = self.settings.include_file_pattern | |||
if not isinstance(filters, list): | |||
filters = [filters] | |||
except AttributeError: | |||
filters = None | |||
result = any(fnmatch.fnmatch(resource.path, f) | |||
for f in filters) if filters else True | |||
return result | |||
def _dir_filter(self, node, *args, **kwargs): | |||
""" | |||
Returns True if the node path is a descendant of the include_paths property in | |||
plugin settings. | |||
""" | |||
try: | |||
node_filters = self.settings.include_paths | |||
if not isinstance(node_filters, list): | |||
node_filters = [node_filters] | |||
node_filters = [self.site.content.node_from_relative_path(f) | |||
for f in node_filters] | |||
except AttributeError: | |||
node_filters = None | |||
result = any(node.source == f.source or | |||
node.source.is_descendant_of(f.source) | |||
for f in node_filters if f) \ | |||
if node_filters else True | |||
return result | |||
def begin_text_resource(self, resource, text): | |||
""" | |||
Called when a text resource is about to be processed for generation. | |||
@@ -217,7 +217,6 @@ Emotions: | |||
from pyquery import PyQuery | |||
q = PyQuery(archives['sad'].read_all()) | |||
print q | |||
assert len(q("li.emotion")) == 2 | |||
assert q("#author").text() == "Tagger Plugin" | |||
@@ -10,8 +10,9 @@ from hyde.fs import File, Folder | |||
from hyde.generator import Generator | |||
from hyde.plugin import Plugin | |||
from hyde.site import Site | |||
from hyde.model import Expando | |||
from mock import patch | |||
from mock import patch, Mock | |||
from nose.tools import raises, nottest, with_setup | |||
@@ -328,4 +329,44 @@ class TestPlugins(object): | |||
gen.generate_resource_at_path(path) | |||
about = File(Folder( | |||
self.site.config.deploy_root_path).child('about.html')) | |||
assert about.read_all() == "Jam" | |||
assert about.read_all() == "Jam" | |||
def test_plugin_filters_begin_text_resource(self): | |||
def empty_return(self, resource, text=''): | |||
return text | |||
with patch.object(ConstantReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock1: | |||
with patch.object(NoReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock2: | |||
self.site.config.plugins = [ | |||
'hyde.tests.test_plugin.ConstantReturnPlugin', | |||
'hyde.tests.test_plugin.NoReturnPlugin' | |||
] | |||
self.site.config.constantreturn = Expando(dict(include_file_pattern="*.css")) | |||
self.site.config.noreturn = Expando(dict(include_file_pattern=["*.html", "*.txt"])) | |||
gen = Generator(self.site) | |||
gen.generate_all() | |||
mock1_args = sorted(set([arg[0][0].name for arg in mock1.call_args_list])) | |||
mock2_args = sorted(set([arg[0][0].name for arg in mock2.call_args_list])) | |||
assert len(mock1_args) == 1 | |||
assert len(mock2_args) == 4 | |||
assert mock1_args == ["site.css"] | |||
assert mock2_args == ["404.html", "about.html", "merry-christmas.html", "robots.txt"] | |||
def test_plugin_node_filters_begin_text_resource(self): | |||
def empty_return(*args, **kwargs): | |||
return None | |||
with patch.object(ConstantReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock1: | |||
with patch.object(NoReturnPlugin, 'begin_text_resource', new=Mock(wraps=empty_return)) as mock2: | |||
self.site.config.plugins = [ | |||
'hyde.tests.test_plugin.ConstantReturnPlugin', | |||
'hyde.tests.test_plugin.NoReturnPlugin' | |||
] | |||
self.site.config.constantreturn = Expando(dict(include_paths="media")) | |||
self.site.config.noreturn = Expando(dict(include_file_pattern="*.html", include_paths=["blog"])) | |||
gen = Generator(self.site) | |||
gen.generate_all() | |||
mock1_args = sorted(set([arg[0][0].name for arg in mock1.call_args_list])) | |||
mock2_args = sorted(set([arg[0][0].name for arg in mock2.call_args_list])) | |||
assert len(mock1_args) == 1 | |||
assert len(mock2_args) == 1 | |||
assert mock1_args == ["site.css"] | |||
assert mock2_args == ["merry-christmas.html"] |
@@ -3,4 +3,4 @@ | |||
Handles hyde version | |||
TODO: Use fabric like versioning scheme | |||
""" | |||
__version__ = '0.8.5a5' | |||
__version__ = '0.8.5a6' |