Merge branch 'fix/hiddenTemplatesListed' into devel

Conflicts:
	src/octoprint/util/__init__.py
	tests/util/test_file_helpers.py
This commit is contained in:
Gina Häußge 2015-11-19 19:04:21 +01:00
commit 1f90096a89
9 changed files with 198 additions and 6 deletions

View file

@ -616,7 +616,9 @@ class Server():
# configure additional template folders for jinja2
import jinja2
filesystem_loader = jinja2.FileSystemLoader([])
import octoprint.util.jinja
filesystem_loader = octoprint.util.jinja.FilteredFileSystemLoader([],
path_filter=lambda x: not octoprint.util.is_hidden_path(x))
filesystem_loader.searchpath = self._template_searchpaths
loaders = [app.jinja_loader, filesystem_loader]

View file

@ -30,7 +30,7 @@ import logging
import re
import uuid
from octoprint.util import atomic_write
from octoprint.util import atomic_write, is_hidden_path
_APPNAME = "OctoPrint"
@ -413,10 +413,12 @@ class Settings(object):
return folder
def _init_script_templating(self):
from jinja2 import Environment, BaseLoader, FileSystemLoader, ChoiceLoader, TemplateNotFound
from jinja2.nodes import Include, Const
from jinja2 import Environment, BaseLoader, ChoiceLoader, TemplateNotFound
from jinja2.nodes import Include
from jinja2.ext import Extension
from octoprint.util.jinja import FilteredFileSystemLoader
class SnippetExtension(Extension):
tags = {"snippet"}
fields = Include.fields
@ -505,10 +507,14 @@ class Settings(object):
else:
return template
file_system_loader = FileSystemLoader(self.getBaseFolder("scripts"))
path_filter = lambda path: not is_hidden_path(path)
file_system_loader = FilteredFileSystemLoader(self.getBaseFolder("scripts"),
path_filter=path_filter)
settings_loader = SettingsScriptLoader(self)
choice_loader = ChoiceLoader([file_system_loader, settings_loader])
select_loader = SelectLoader(choice_loader, dict(bundled=settings_loader, file=file_system_loader))
select_loader = SelectLoader(choice_loader,
dict(bundled=settings_loader,
file=file_system_loader))
return RelEnvironment(loader=select_loader, extensions=[SnippetExtension])
def _get_script_template(self, script_type, name, source=False):

View file

@ -661,6 +661,23 @@ def bom_aware_open(filename, encoding="ascii", mode="r", **kwargs):
return codecs.open(filename, encoding=encoding, mode=mode, **kwargs)
def is_hidden_path(path):
filename = os.path.basename(path)
if filename.startswith("."):
return True
if sys.platform == "win32":
try:
import ctypes
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(path))
assert attrs != -1
return bool(attrs & 2)
except (AttributeError, AssertionError):
pass
return False
class RepeatedTimer(threading.Thread):
"""
This class represents an action that should be run repeatedly in an interval. It is similar to python's

View file

@ -0,0 +1,50 @@
# coding=utf-8
from __future__ import absolute_import
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
import os
from jinja2.loaders import FileSystemLoader, TemplateNotFound, split_template_path
class FilteredFileSystemLoader(FileSystemLoader):
"""
Jinja2 ``FileSystemLoader`` subclass that allows filtering templates.
Only such templates will be accessible for whose paths the provided
``path_filter`` filter function returns True.
``path_filter`` will receive the actual path on disc and should behave just
like callables provided to Python's internal ``filter`` function, returning
``True`` if the path is cleared and ``False`` if it is supposed to be removed
from results and hence ``filter(path_filter, iterable)`` should be
equivalent to ``[item for item in iterable if path_filter(item)]``.
If ``path_filter`` is not set or not a ``callable``, the loader will
behave just like the regular Jinja2 ``FileSystemLoader``.
"""
def __init__(self, searchpath, path_filter=None, **kwargs):
FileSystemLoader.__init__(self, searchpath, **kwargs)
self.path_filter = path_filter
def get_source(self, environment, template):
if callable(self.path_filter):
pieces = split_template_path(template)
if not self._combined_filter(os.path.join(*pieces)):
raise TemplateNotFound(template)
return FileSystemLoader.get_source(self, environment, template)
def list_templates(self):
result = FileSystemLoader.list_templates(self)
if callable(self.path_filter):
result = sorted(filter(self._combined_filter, result))
return result
def _combined_filter(self, path):
filter_results = map(lambda x: not os.path.exists(os.path.join(x, path)) or self.path_filter(os.path.join(x, path)),
self.searchpath)
return all(filter_results)

View file

@ -0,0 +1 @@
hidden_everywhere

View file

@ -0,0 +1 @@
normal_text

View file

@ -0,0 +1 @@
not_a_text

View file

@ -220,3 +220,41 @@ class TempDirTest(unittest.TestCase):
# assert
mock_mkdtemp.assert_called_once_with()
mock_rmtree.assert_called_once_with(path, ignore_errors=True, onerror=onerror)
class IsHiddenPathTest(unittest.TestCase):
def setUp(self):
import tempfile
self.basepath = tempfile.mkdtemp()
self.path_always_visible = os.path.join(self.basepath, "always_visible.txt")
self.path_hidden_on_windows = os.path.join(self.basepath, "hidden_on_windows.txt")
self.path_always_hidden = os.path.join(self.basepath, ".always_hidden.txt")
for attr in ("path_always_visible", "path_hidden_on_windows", "path_always_hidden"):
path = getattr(self, attr)
with open(path, "w+b") as f:
f.write(attr)
import sys
if sys.platform == "win32":
# we use ctypes and the windows API to set the hidden attribute on the file
# only hidden on windows
import ctypes
ctypes.windll.kernel32.SetFileAttributesW(unicode(self.path_hidden_on_windows), 2)
def tearDown(self):
import shutil
shutil.rmtree(self.basepath)
def test_is_hidden_path(self):
self.assertFalse(octoprint.util.is_hidden_path(self.path_always_visible))
self.assertTrue(octoprint.util.is_hidden_path(self.path_always_hidden))
import sys
if sys.platform == "win32":
self.assertTrue(octoprint.util.is_hidden_path(self.path_hidden_on_windows))
else:
self.assertFalse(octoprint.util.is_hidden_path(self.path_hidden_on_windows))

76
tests/util/test_jinja.py Normal file
View file

@ -0,0 +1,76 @@
# coding=utf-8
from __future__ import absolute_import
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
import unittest
import os
import jinja2
from ddt import ddt, data, unpack
import octoprint.util.jinja
NONE_FILTER = None
HIDDEN_FILTER = lambda x: not os.path.basename(x).startswith(".")
NO_TXT_FILTER = lambda x: x.endswith(".txt")
COMBINED_FILTER = lambda x: HIDDEN_FILTER(x) and NO_TXT_FILTER(x)
@ddt
class FilteredFileSystemLoaderTest(unittest.TestCase):
def setUp(self):
self.basepath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "jinja_test_data")
self.environment = jinja2.Environment()
def loader_factory(self, path_filter):
return octoprint.util.jinja.FilteredFileSystemLoader(self.basepath,
path_filter=path_filter)
@data(
(NONE_FILTER, [".hidden_everywhere.txt", "normal_text.txt", "not_a_text.dat"]),
(HIDDEN_FILTER, ["normal_text.txt", "not_a_text.dat"]),
(NO_TXT_FILTER, [".hidden_everywhere.txt", "normal_text.txt"]),
(COMBINED_FILTER, ["normal_text.txt"])
)
@unpack
def test_list_templates(self, path_filter, expected):
loader = self.loader_factory(path_filter=path_filter)
templates = loader.list_templates()
self.assertListEqual(templates, expected)
@data(
(NONE_FILTER, ((".hidden_everywhere.txt", True),
("normal_text.txt", True),
("not_a_text.dat", True))),
(HIDDEN_FILTER, ((".hidden_everywhere.txt", False),
("normal_text.txt", True),
("not_a_text.dat", True))),
(NO_TXT_FILTER, ((".hidden_everywhere.txt", True),
("normal_text.txt", True),
("not_a_text.dat", False))),
(COMBINED_FILTER, ((".hidden_everywhere.txt", False),
("normal_text.txt", True),
("not_a_text.dat", False)))
)
@unpack
def test_get_source_none_filter(self, path_filter, param_sets):
loader = self.loader_factory(path_filter=path_filter)
for param_set in param_sets:
template, success = param_set
if success:
self._test_get_source_success(loader, template)
else:
self._test_get_source_notfound(loader, template)
def _test_get_source_success(self, loader, template):
loader.get_source(self.environment, template)
def _test_get_source_notfound(self, loader, template):
try:
loader.get_source(self.environment, template)
self.fail("Expected an exception")
except jinja2.TemplateNotFound:
pass