| @@ -0,0 +1,70 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| OPTIPNG plugin | |||||
| """ | |||||
| from hyde.plugin import CLTransformer | |||||
| from hyde.fs import File | |||||
| class OptiPNGPlugin(CLTransformer): | |||||
| """ | |||||
| The plugin class for OptiPNG | |||||
| """ | |||||
| def __init__(self, site): | |||||
| super(OptiPNGPlugin, self).__init__(site) | |||||
| @property | |||||
| def plugin_name(self): | |||||
| """ | |||||
| The name of the plugin. | |||||
| """ | |||||
| return "optipng" | |||||
| def option_prefix(self, option): | |||||
| return "-" | |||||
| def binary_resource_complete(self, resource): | |||||
| """ | |||||
| If the site is in development mode, just return. | |||||
| Otherwise, run optipng to compress the png file. | |||||
| """ | |||||
| try: | |||||
| mode = self.site.config.mode | |||||
| except AttributeError: | |||||
| mode = "production" | |||||
| if not resource.source_file.kind == 'png': | |||||
| return | |||||
| if mode.startswith('dev'): | |||||
| self.logger.debug("Skipping optipng in development mode.") | |||||
| return | |||||
| supported = [ | |||||
| "o", | |||||
| "fix", | |||||
| "force", | |||||
| "preserve", | |||||
| "quiet", | |||||
| "log", | |||||
| "f", | |||||
| "i", | |||||
| "zc", | |||||
| "zm", | |||||
| "zs", | |||||
| "zw", | |||||
| "full", | |||||
| "nb", | |||||
| "nc", | |||||
| "np", | |||||
| "nz" | |||||
| ] | |||||
| target = File(self.site.config.deploy_root_path.child( | |||||
| resource.relative_deploy_path)) | |||||
| optipng = self.app | |||||
| args = [str(optipng)] | |||||
| args.extend(self.process_args(supported)) | |||||
| args.extend([str(target)]) | |||||
| self.call_app(args) | |||||
| @@ -13,6 +13,7 @@ import os | |||||
| import shutil | import shutil | ||||
| from distutils import dir_util | from distutils import dir_util | ||||
| import functools | import functools | ||||
| import fnmatch | |||||
| from hyde.util import getLoggerWithNullHandler | from hyde.util import getLoggerWithNullHandler | ||||
| @@ -94,12 +95,12 @@ class FS(object): | |||||
| Generates the parents until stop or the absolute | Generates the parents until stop or the absolute | ||||
| root directory is reached. | root directory is reached. | ||||
| """ | """ | ||||
| f = self | |||||
| while f.parent != stop: | |||||
| if f.parent == f: | |||||
| folder = self | |||||
| while folder.parent != stop: | |||||
| if folder.parent == folder: | |||||
| return | return | ||||
| yield f.parent | |||||
| f = f.parent | |||||
| yield folder.parent | |||||
| folder = folder.parent | |||||
| def is_descendant_of(self, ancestor): | def is_descendant_of(self, ancestor): | ||||
| """ | """ | ||||
| @@ -185,9 +186,22 @@ class File(FS): | |||||
| """ | """ | ||||
| return self.extension.lstrip(".") | return self.extension.lstrip(".") | ||||
| @property | |||||
| def size(self): | |||||
| """ | |||||
| Size of this file. | |||||
| """ | |||||
| if not self.exists: | |||||
| return -1 | |||||
| return os.path.getsize(self.path) | |||||
| @property | @property | ||||
| def mimetype(self): | def mimetype(self): | ||||
| (mime, encoding) = mimetypes.guess_type(self.path) | |||||
| """ | |||||
| Gets the mimetype of this file. | |||||
| """ | |||||
| (mime, _) = mimetypes.guess_type(self.path) | |||||
| return mime | return mime | ||||
| @property | @property | ||||
| @@ -246,9 +260,9 @@ class File(FS): | |||||
| import tempfile | import tempfile | ||||
| (handle, path) = tempfile.mkstemp(text=True) | (handle, path) = tempfile.mkstemp(text=True) | ||||
| os.close(handle) | os.close(handle) | ||||
| f = File(path) | |||||
| f.write(text) | |||||
| return f | |||||
| afile = File(path) | |||||
| afile.write(text) | |||||
| return afile | |||||
| def read_all(self, encoding='utf-8'): | def read_all(self, encoding='utf-8'): | ||||
| """ | """ | ||||
| @@ -296,26 +310,26 @@ class FSVisitor(object): | |||||
| self.folder = folder | self.folder = folder | ||||
| self.pattern = pattern | self.pattern = pattern | ||||
| def folder_visitor(self, f): | |||||
| def folder_visitor(self, function): | |||||
| """ | """ | ||||
| Decorator for `visit_folder` protocol | Decorator for `visit_folder` protocol | ||||
| """ | """ | ||||
| self.visit_folder = f | |||||
| return f | |||||
| self.visit_folder = function | |||||
| return function | |||||
| def file_visitor(self, f): | |||||
| def file_visitor(self, function): | |||||
| """ | """ | ||||
| Decorator for `visit_file` protocol | Decorator for `visit_file` protocol | ||||
| """ | """ | ||||
| self.visit_file = f | |||||
| return f | |||||
| self.visit_file = function | |||||
| return function | |||||
| def finalizer(self, f): | |||||
| def finalizer(self, function): | |||||
| """ | """ | ||||
| Decorator for `visit_complete` protocol | Decorator for `visit_complete` protocol | ||||
| """ | """ | ||||
| self.visit_complete = f | |||||
| return f | |||||
| self.visit_complete = function | |||||
| return function | |||||
| def __enter__(self): | def __enter__(self): | ||||
| return self | return self | ||||
| @@ -341,7 +355,7 @@ class FolderWalker(FSVisitor): | |||||
| if not walk_files and not walk_folders: | if not walk_files and not walk_folders: | ||||
| return | return | ||||
| for root, dirs, a_files in os.walk(self.folder.path, followlinks=True): | |||||
| for root, _, a_files in os.walk(self.folder.path, followlinks=True): | |||||
| folder = Folder(root) | folder = Folder(root) | ||||
| if walk_folders: | if walk_folders: | ||||
| yield folder | yield folder | ||||
| @@ -262,6 +262,9 @@ class CLTransformer(Plugin): | |||||
| return app | return app | ||||
| def option_prefix(self, option): | |||||
| return "--" | |||||
| def process_args(self, supported): | def process_args(self, supported): | ||||
| """ | """ | ||||
| Given a list of supported arguments, consutructs an argument | Given a list of supported arguments, consutructs an argument | ||||
| @@ -282,7 +285,8 @@ class CLTransformer(Plugin): | |||||
| descriptive = short = arg | descriptive = short = arg | ||||
| if descriptive in args or short in args: | if descriptive in args or short in args: | ||||
| result.append("--%s" % descriptive) | |||||
| result.append("%s%s" % (self.option_prefix(descriptive), | |||||
| descriptive)) | |||||
| val = args[descriptive if descriptive in args else short] | val = args[descriptive if descriptive in args else short] | ||||
| if val: | if val: | ||||
| result.append(val) | result.append(val) | ||||
| @@ -0,0 +1 @@ | |||||
| ../../../../resources/hyde-lt-b.png | |||||
| @@ -32,7 +32,7 @@ class TestLess(object): | |||||
| paths = ['/usr/local/share/npm/bin/lessc', '~/local/bin/lessc'] | paths = ['/usr/local/share/npm/bin/lessc', '~/local/bin/lessc'] | ||||
| less = [path for path in paths if File(path).exists] | less = [path for path in paths if File(path).exists] | ||||
| if not less: | if not less: | ||||
| assert False, "Cannot find the uglify executable" | |||||
| assert False, "Cannot find the lessc executable" | |||||
| less = less[0] | less = less[0] | ||||
| s.config.less = Expando(dict(app=less)) | s.config.less = Expando(dict(app=less)) | ||||
| source = TEST_SITE.child('content/media/css/site.less') | source = TEST_SITE.child('content/media/css/site.less') | ||||
| @@ -0,0 +1,45 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| Use nose | |||||
| `$ pip install nose` | |||||
| `$ nosetests` | |||||
| """ | |||||
| from hyde.fs import File, Folder | |||||
| from hyde.model import Expando | |||||
| from hyde.generator import Generator | |||||
| from hyde.site import Site | |||||
| OPTIPNG_SOURCE = File(__file__).parent.child_folder('optipng') | |||||
| TEST_SITE = File(__file__).parent.parent.child_folder('_test') | |||||
| class TestOptipng(object): | |||||
| def setUp(self): | |||||
| TEST_SITE.make() | |||||
| TEST_SITE.parent.child_folder( | |||||
| 'sites/test_jinja').copy_contents_to(TEST_SITE) | |||||
| IMAGES = TEST_SITE.child_folder('content/media/images') | |||||
| IMAGES.make() | |||||
| OPTIPNG_SOURCE.copy_contents_to(IMAGES) | |||||
| def tearDown(self): | |||||
| TEST_SITE.delete() | |||||
| def test_can_execute_optipng(self): | |||||
| s = Site(TEST_SITE) | |||||
| s.config.mode = "production" | |||||
| s.config.plugins = ['hyde.ext.plugins.optipng.OptiPNGPlugin'] | |||||
| paths = ['/usr/local/bin/optipng'] | |||||
| optipng = [path for path in paths if File(path).exists] | |||||
| if not optipng: | |||||
| assert False, "Cannot find the optipng executable" | |||||
| optipng = optipng[0] | |||||
| s.config.optipng = Expando(dict(app=optipng, args=dict(quiet=""))) | |||||
| source =File(TEST_SITE.child('content/media/images/hyde-lt-b.png')) | |||||
| target = File(Folder(s.config.deploy_root_path).child('media/images/hyde-lt-b.png')) | |||||
| gen = Generator(s) | |||||
| gen.generate_resource_at_path(source) | |||||
| assert target.exists | |||||
| assert target.size < source.size | |||||
| @@ -204,6 +204,9 @@ def test_is_image(): | |||||
| assert not HELPERS.is_image | assert not HELPERS.is_image | ||||
| assert LOGO.is_image | assert LOGO.is_image | ||||
| def test_file_size(): | |||||
| assert LOGO.size == 1942 | |||||
| @nottest | @nottest | ||||
| def setup_data(): | def setup_data(): | ||||
| DATA_ROOT.make() | DATA_ROOT.make() | ||||