- `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 | Version 0.8.5a5 | ||||
============================================================ | ============================================================ | ||||
@@ -1,4 +1,4 @@ | |||||
Version 0.8.5a5 | |||||
Version 0.8.5a6 | |||||
A brand new **hyde** | A brand new **hyde** | ||||
==================== | ==================== | ||||
@@ -615,4 +615,4 @@ class Folder(FS): | |||||
""" | """ | ||||
Return a `FolderLister` object | 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 hyde.model import Expando | ||||
from functools import partial | from functools import partial | ||||
import fnmatch | |||||
import os | import os | ||||
import re | import re | ||||
@@ -40,14 +41,16 @@ class PluginProxy(object): | |||||
# logger.debug( | # logger.debug( | ||||
# "\tCalling plugin [%s]", | # "\tCalling plugin [%s]", | ||||
# plugin.__class__.__name__) | # 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 res | ||||
return __call_plugins__ | return __call_plugins__ | ||||
@@ -81,18 +84,30 @@ class Plugin(object): | |||||
""" | """ | ||||
Syntactic sugar for template methods | Syntactic sugar for template methods | ||||
""" | """ | ||||
result = None | |||||
if name.startswith('t_') and self.template: | if name.startswith('t_') and self.template: | ||||
attr = name[2:] | attr = name[2:] | ||||
if hasattr(self.template, attr): | if hasattr(self.template, attr): | ||||
return self.template[attr] | |||||
result = self.template[attr] | |||||
elif attr.endswith('_close_tag'): | elif attr.endswith('_close_tag'): | ||||
tag = attr.replace('_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'): | elif attr.endswith('_open_tag'): | ||||
tag = attr.replace('_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 | @property | ||||
def settings(self): | def settings(self): | ||||
@@ -139,6 +154,44 @@ class Plugin(object): | |||||
""" | """ | ||||
pass | 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): | def begin_text_resource(self, resource, text): | ||||
""" | """ | ||||
Called when a text resource is about to be processed for generation. | Called when a text resource is about to be processed for generation. | ||||
@@ -217,7 +217,6 @@ Emotions: | |||||
from pyquery import PyQuery | from pyquery import PyQuery | ||||
q = PyQuery(archives['sad'].read_all()) | q = PyQuery(archives['sad'].read_all()) | ||||
print q | |||||
assert len(q("li.emotion")) == 2 | assert len(q("li.emotion")) == 2 | ||||
assert q("#author").text() == "Tagger Plugin" | assert q("#author").text() == "Tagger Plugin" | ||||
@@ -10,8 +10,9 @@ from hyde.fs import File, Folder | |||||
from hyde.generator import Generator | from hyde.generator import Generator | ||||
from hyde.plugin import Plugin | from hyde.plugin import Plugin | ||||
from hyde.site import Site | 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 | from nose.tools import raises, nottest, with_setup | ||||
@@ -328,4 +329,44 @@ class TestPlugins(object): | |||||
gen.generate_resource_at_path(path) | gen.generate_resource_at_path(path) | ||||
about = File(Folder( | about = File(Folder( | ||||
self.site.config.deploy_root_path).child('about.html')) | 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 | Handles hyde version | ||||
TODO: Use fabric like versioning scheme | TODO: Use fabric like versioning scheme | ||||
""" | """ | ||||
__version__ = '0.8.5a5' | |||||
__version__ = '0.8.5a6' |