Repository: getnikola/nikola

Issue:
Add support for alternatley named Tidy packages
Some distros (Arch for example) will package Tidy5 as Tidy, which will cause Nikola to fail when it attempts to make use of filters. In the meantime filters.py can be edited to resolve the issue until there is an offical fix (rename tidy5 to tidy for each of the filters you use).


Discussion:
I had that problem before (both with `tidy` and `closure_compiler`, which was called differently for some time in Arch until it was renamed recently). I ended up copying the filters into my `conf.py` and adjusting them there.
What you can do is:
```
from nikola import filters
from functools import partial
```
and then use `partial(filters.html_tidy_withconfig, executable='tidy')` instead of `filters.html_tidy_withconfig` directly. Kind of ugly, but works :)
@Kwpolska: do you have a better suggestion? Especially to make it easier to use for non-programmers?
Perhaps a simple config option, that we could apply with `functools.partial` in `nikola.py`?
How should that work? I.e. how can Nikola distinguish which filter is which one, i.e. to which one apply `functools.partial` with which option?
We could give some special attribute to all tidy-related functions, iterate over filters, and if we find a callable with that attribute, wrap it with `partial`.
How about something slightly more general (i.e. it not just works for `tidy`):
```
class configurable_filter(object):
    def __init__(self, **configuration_variables):
        self.configuration_variables = configuration_variables

    def __call__(self, f):
        f.configuration_variables = self.configuration_variables
        return f
```

Then one could do something like
```
@configurable_filter(executable='TIDY_EXECUTABLE')
def some_tidy_filter(filename, executable='tidy'):
    ...
```
and process filters as follows:
```
for i, f in enumerate(filters):
    if hasattr(f, 'configuration_variables'):
        args = {}
        for arg, config in f.configuration_variables.items():
            if config in site.config:
                args[arg] = site.config[config]
        if args:
            filters[i] = functools.partial(f, **args)
```
and if `TIDY_EXECUTABLE` is defined in the config, the argument `executable` of `some_tidy_filter` is replaced with it's value.
Make that a PR.

Resolving patch:
diff --git a/nikola/filters.py b/nikola/filters.py
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -45,6 +45,19 @@
 from .utils import req_missing, LOGGER
 
 
+class _ConfigurableFilter(object):
+    """Allow Nikola to configure filter with site's config."""
+
+    def __init__(self, **configuration_variables):
+        """Define which arguments to configure from which configuration variables."""
+        self.configuration_variables = configuration_variables
+
+    def __call__(self, f):
+        """Store configuration_variables as attribute of function."""
+        f.configuration_variables = self.configuration_variables
+        return f
+
+
 def apply_to_binary_file(f):
     """Apply a filter to a binary file.
 
@@ -126,14 +139,16 @@ def runinplace(command, infile):
             shutil.rmtree(tmpdir)
 
 
-def yui_compressor(infile):
+@_ConfigurableFilter(executable='YUI_COMPRESSOR_EXECUTABLE')
+def yui_compressor(infile, executable=None):
     """Run YUI Compressor on a file."""
-    yuicompressor = False
-    try:
-        subprocess.call('yui-compressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
-        yuicompressor = 'yui-compressor'
-    except Exception:
-        pass
+    yuicompressor = executable
+    if not yuicompressor:
+        try:
+            subprocess.call('yui-compressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
+            yuicompressor = 'yui-compressor'
+        except Exception:
+            pass
     if not yuicompressor:
         try:
             subprocess.call('yuicompressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
@@ -145,41 +160,49 @@ def yui_compressor(infile):
     return runinplace('{} --nomunge %1 -o %2'.format(yuicompressor), infile)
 
 
-def closure_compiler(infile):
+@_ConfigurableFilter(executable='CLOSURE_COMPILER_EXECUTABLE')
+def closure_compiler(infile, executable='closure-compiler'):
     """Run closure-compiler on a file."""
-    return runinplace('closure-compiler --warning_level QUIET --js %1 --js_output_file %2', infile)
+    return runinplace('{} --warning_level QUIET --js %1 --js_output_file %2'.format(executable), infile)
 
 
-def optipng(infile):
+@_ConfigurableFilter(executable='OPTIPNG_EXECUTABLE')
+def optipng(infile, executable='optiong'):
     """Run optipng on a file."""
-    return runinplace("optipng -preserve -o2 -quiet %1", infile)
+    return runinplace("{} -preserve -o2 -quiet %1".format(executable), infile)
 
 
-def jpegoptim(infile):
+@_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE')
+def jpegoptim(infile, executable='jpegoptim'):
     """Run jpegoptim on a file."""
-    return runinplace("jpegoptim -p --strip-all -q %1", infile)
+    return runinplace("{} -p --strip-all -q %1".format(executable), infile)
 
 
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
 def html_tidy_withconfig(infile, executable='tidy5'):
     """Run HTML Tidy with tidy5.conf as config file."""
     return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent -config tidy5.conf -modify %1", executable=executable)
 
 
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
 def html_tidy_nowrap(infile, executable='tidy5'):
     """Run HTML Tidy without line wrapping."""
     return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1", executable=executable)
 
 
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
 def html_tidy_wrap(infile, executable='tidy5'):
     """Run HTML Tidy with line wrapping."""
     return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1", executable=executable)
 
 
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
 def html_tidy_wrap_attr(infile, executable='tidy5'):
     """Run HTML tidy with line wrapping and attribute indentation."""
     return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes yes --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1", executable=executable)
 
 
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
 def html_tidy_mini(infile, executable='tidy5'):
     """Run HTML tidy with minimal settings."""
     return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --tidy-mark no --drop-empty-elements no -modify %1", executable=executable)
diff --git a/nikola/nikola.py b/nikola/nikola.py
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -32,6 +32,7 @@
 from copy import copy
 from pkg_resources import resource_filename
 import datetime
+import functools
 import locale
 import os
 import json
@@ -925,6 +926,17 @@ def __init__(self, **config):
             utils.LOGGER.warn('The STORY_INDEX option is deprecated, use PAGE_INDEX instead.')
             self.config['PAGE_INDEX'] = config['STORY_INDEX']
 
+        # Configure filters
+        for actions in self.config['FILTERS'].values():
+            for i, f in enumerate(actions):
+                if hasattr(f, 'configuration_variables'):
+                    args = {}
+                    for arg, config in f.configuration_variables.items():
+                        if config in self.config:
+                            args[arg] = self.config[config]
+                    if args:
+                        actions[i] = functools.partial(f, **args)
+
         # We use one global tzinfo object all over Nikola.
         try:
             self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE'])


Test patch: