- `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' | |||||