forked from github/pelican
Merge pull request #857 from avaris/feedback
add feedback to user and refactor 'autoreload' code
This commit is contained in:
commit
e55878fc2e
4 changed files with 106 additions and 78 deletions
|
|
@ -17,8 +17,7 @@ from pelican.generators import (ArticlesGenerator, PagesGenerator,
|
||||||
SourceFileGenerator, TemplatePagesGenerator)
|
SourceFileGenerator, TemplatePagesGenerator)
|
||||||
from pelican.log import init
|
from pelican.log import init
|
||||||
from pelican.settings import read_settings
|
from pelican.settings import read_settings
|
||||||
from pelican.utils import (clean_output_dir, files_changed, file_changed,
|
from pelican.utils import clean_output_dir, folder_watcher, file_watcher
|
||||||
NoFilesError)
|
|
||||||
from pelican.writers import Writer
|
from pelican.writers import Writer
|
||||||
|
|
||||||
__major__ = 3
|
__major__ = 3
|
||||||
|
|
@ -149,6 +148,7 @@ class Pelican(object):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run the generators and return"""
|
"""Run the generators and return"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
context = self.settings.copy()
|
context = self.settings.copy()
|
||||||
context['filenames'] = {} # share the dict between all the generators
|
context['filenames'] = {} # share the dict between all the generators
|
||||||
|
|
@ -182,6 +182,14 @@ class Pelican(object):
|
||||||
|
|
||||||
signals.finalized.send(self)
|
signals.finalized.send(self)
|
||||||
|
|
||||||
|
articles_generator = next(g for g in generators if isinstance(g, ArticlesGenerator))
|
||||||
|
pages_generator = next(g for g in generators if isinstance(g, PagesGenerator))
|
||||||
|
|
||||||
|
print('Done: Processed {} articles and {} pages in {:.2f} seconds.'.format(
|
||||||
|
len(articles_generator.articles) + len(articles_generator.translations),
|
||||||
|
len(pages_generator.pages) + len(pages_generator.translations),
|
||||||
|
time.time() - start_time))
|
||||||
|
|
||||||
def get_generator_classes(self):
|
def get_generator_classes(self):
|
||||||
generators = [StaticGenerator, ArticlesGenerator, PagesGenerator]
|
generators = [StaticGenerator, ArticlesGenerator, PagesGenerator]
|
||||||
|
|
||||||
|
|
@ -308,9 +316,19 @@ def main():
|
||||||
init(args.verbosity)
|
init(args.verbosity)
|
||||||
pelican = get_instance(args)
|
pelican = get_instance(args)
|
||||||
|
|
||||||
|
watchers = {'content': folder_watcher(pelican.path,
|
||||||
|
pelican.markup,
|
||||||
|
pelican.ignore_files),
|
||||||
|
'theme': folder_watcher(pelican.theme,
|
||||||
|
[''],
|
||||||
|
pelican.ignore_files),
|
||||||
|
'settings': file_watcher(args.settings)}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.autoreload:
|
if args.autoreload:
|
||||||
files_found_error = True
|
print(' --- AutoReload Mode: Monitoring `content`, `theme` and `settings`'
|
||||||
|
' for changes. ---')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# Check source dir for changed files ending with the given
|
# Check source dir for changed files ending with the given
|
||||||
|
|
@ -318,45 +336,45 @@ def main():
|
||||||
# restriction; all files are recursively checked if they
|
# restriction; all files are recursively checked if they
|
||||||
# have changed, no matter what extension the filenames
|
# have changed, no matter what extension the filenames
|
||||||
# have.
|
# have.
|
||||||
if (files_changed(
|
modified = {k: next(v) for k, v in watchers.items()}
|
||||||
pelican.path,
|
|
||||||
pelican.markup,
|
|
||||||
pelican.ignore_files)
|
|
||||||
or files_changed(
|
|
||||||
pelican.theme,
|
|
||||||
[''],
|
|
||||||
pelican.ignore_files
|
|
||||||
)):
|
|
||||||
if not files_found_error:
|
|
||||||
files_found_error = True
|
|
||||||
pelican.run()
|
|
||||||
|
|
||||||
# reload also if settings.py changed
|
if modified['settings']:
|
||||||
if file_changed(args.settings):
|
|
||||||
logger.info('%s changed, re-generating' %
|
|
||||||
args.settings)
|
|
||||||
pelican = get_instance(args)
|
pelican = get_instance(args)
|
||||||
|
|
||||||
|
if any(modified.values()):
|
||||||
|
print('\n-> Modified: {}. re-generating...'.format(
|
||||||
|
', '.join(k for k, v in modified.items() if v)))
|
||||||
|
|
||||||
|
if modified['content'] is None:
|
||||||
|
logger.warning('No valid files found in content.')
|
||||||
|
|
||||||
|
if modified['theme'] is None:
|
||||||
|
logger.warning('Empty theme folder. Using `basic` theme.')
|
||||||
|
|
||||||
pelican.run()
|
pelican.run()
|
||||||
|
|
||||||
time.sleep(.5) # sleep to avoid cpu load
|
time.sleep(.5) # sleep to avoid cpu load
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.warning("Keyboard interrupt, quitting.")
|
logger.warning("Keyboard interrupt, quitting.")
|
||||||
break
|
break
|
||||||
except NoFilesError:
|
|
||||||
if files_found_error:
|
|
||||||
logger.warning("No valid files found in content. "
|
|
||||||
"Nothing to generate.")
|
|
||||||
files_found_error = False
|
|
||||||
time.sleep(1) # sleep to avoid cpu load
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if (args.verbosity == logging.DEBUG):
|
if (args.verbosity == logging.DEBUG):
|
||||||
logger.critical(e.args)
|
logger.critical(e.args)
|
||||||
raise
|
raise
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Caught exception "{0}". Reloading.'.format(e))
|
'Caught exception "{0}". Reloading.'.format(e))
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
|
if next(watchers['content']) is None:
|
||||||
|
logger.warning('No valid files found in content.')
|
||||||
|
|
||||||
|
if next(watchers['theme']) is None:
|
||||||
|
logger.warning('Empty theme folder. Using `basic` theme.')
|
||||||
|
|
||||||
pelican.run()
|
pelican.run()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import logging
|
||||||
|
|
||||||
from pelican import Pelican
|
from pelican import Pelican
|
||||||
from pelican.settings import read_settings
|
from pelican.settings import read_settings
|
||||||
from pelican.tests.support import LoggedTestCase
|
from pelican.tests.support import LoggedTestCase, mute
|
||||||
|
|
||||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
SAMPLES_PATH = os.path.abspath(os.path.join(
|
SAMPLES_PATH = os.path.abspath(os.path.join(
|
||||||
|
|
@ -73,7 +73,7 @@ class TestPelican(LoggedTestCase):
|
||||||
'LOCALE': locale.normalize('en_US'),
|
'LOCALE': locale.normalize('en_US'),
|
||||||
})
|
})
|
||||||
pelican = Pelican(settings=settings)
|
pelican = Pelican(settings=settings)
|
||||||
pelican.run()
|
mute(True)(pelican.run)()
|
||||||
dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'basic'))
|
dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'basic'))
|
||||||
self.assertFilesEqual(recursiveDiff(dcmp))
|
self.assertFilesEqual(recursiveDiff(dcmp))
|
||||||
self.assertLogCountEqual(
|
self.assertLogCountEqual(
|
||||||
|
|
@ -89,6 +89,6 @@ class TestPelican(LoggedTestCase):
|
||||||
'LOCALE': locale.normalize('en_US'),
|
'LOCALE': locale.normalize('en_US'),
|
||||||
})
|
})
|
||||||
pelican = Pelican(settings=settings)
|
pelican = Pelican(settings=settings)
|
||||||
pelican.run()
|
mute(True)(pelican.run)()
|
||||||
dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'custom'))
|
dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'custom'))
|
||||||
self.assertFilesEqual(recursiveDiff(dcmp))
|
self.assertFilesEqual(recursiveDiff(dcmp))
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ from sys import platform
|
||||||
|
|
||||||
from pelican import utils
|
from pelican import utils
|
||||||
from .support import get_article, LoggedTestCase, locale_available, unittest
|
from .support import get_article, LoggedTestCase, locale_available, unittest
|
||||||
from pelican.utils import NoFilesError
|
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(LoggedTestCase):
|
class TestUtils(LoggedTestCase):
|
||||||
|
|
@ -127,31 +126,43 @@ class TestUtils(LoggedTestCase):
|
||||||
self.assertNotIn(en_article3, trans)
|
self.assertNotIn(en_article3, trans)
|
||||||
self.assertNotIn(fr_article3, index)
|
self.assertNotIn(fr_article3, index)
|
||||||
|
|
||||||
def test_files_changed(self):
|
def test_watchers(self):
|
||||||
# Test if file changes are correctly detected
|
# Test if file changes are correctly detected
|
||||||
# Make sure to handle not getting any files correctly.
|
# Make sure to handle not getting any files correctly.
|
||||||
|
|
||||||
dirname = os.path.join(os.path.dirname(__file__), 'content')
|
dirname = os.path.join(os.path.dirname(__file__), 'content')
|
||||||
|
folder_watcher = utils.folder_watcher(dirname, ['rst'])
|
||||||
|
|
||||||
path = os.path.join(dirname, 'article_with_metadata.rst')
|
path = os.path.join(dirname, 'article_with_metadata.rst')
|
||||||
changed = utils.files_changed(dirname, 'rst')
|
file_watcher = utils.file_watcher(path)
|
||||||
self.assertEqual(changed, True)
|
|
||||||
|
|
||||||
changed = utils.files_changed(dirname, 'rst')
|
# first check returns True
|
||||||
self.assertEqual(changed, False)
|
self.assertEqual(next(folder_watcher), True)
|
||||||
|
self.assertEqual(next(file_watcher), True)
|
||||||
|
|
||||||
|
# next check without modification returns False
|
||||||
|
self.assertEqual(next(folder_watcher), False)
|
||||||
|
self.assertEqual(next(file_watcher), False)
|
||||||
|
|
||||||
|
# after modification, returns True
|
||||||
t = time.time()
|
t = time.time()
|
||||||
os.utime(path, (t, t))
|
os.utime(path, (t, t))
|
||||||
changed = utils.files_changed(dirname, 'rst')
|
self.assertEqual(next(folder_watcher), True)
|
||||||
self.assertEqual(changed, True)
|
self.assertEqual(next(file_watcher), True)
|
||||||
self.assertAlmostEqual(utils.LAST_MTIME, t, delta=1)
|
|
||||||
|
# file watcher with None or empty path should return None
|
||||||
|
self.assertEqual(next(utils.file_watcher('')), None)
|
||||||
|
self.assertEqual(next(utils.file_watcher(None)), None)
|
||||||
|
|
||||||
empty_path = os.path.join(os.path.dirname(__file__), 'empty')
|
empty_path = os.path.join(os.path.dirname(__file__), 'empty')
|
||||||
try:
|
try:
|
||||||
os.mkdir(empty_path)
|
os.mkdir(empty_path)
|
||||||
os.mkdir(os.path.join(empty_path, "empty_folder"))
|
os.mkdir(os.path.join(empty_path, "empty_folder"))
|
||||||
shutil.copy(__file__, empty_path)
|
shutil.copy(__file__, empty_path)
|
||||||
with self.assertRaises(NoFilesError):
|
|
||||||
utils.files_changed(empty_path, 'rst')
|
# if no files of interest, returns None
|
||||||
|
watcher = utils.folder_watcher(empty_path, ['rst'])
|
||||||
|
self.assertEqual(next(watcher), None)
|
||||||
except OSError:
|
except OSError:
|
||||||
self.fail("OSError Exception in test_files_changed test")
|
self.fail("OSError Exception in test_files_changed test")
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import logging
|
||||||
import errno
|
import errno
|
||||||
import locale
|
import locale
|
||||||
import fnmatch
|
import fnmatch
|
||||||
from collections import defaultdict, Hashable
|
from collections import Hashable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from codecs import open, BOM_UTF8
|
from codecs import open, BOM_UTF8
|
||||||
|
|
@ -81,10 +81,6 @@ def python_2_unicode_compatible(klass):
|
||||||
return klass
|
return klass
|
||||||
|
|
||||||
|
|
||||||
class NoFilesError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class memoized(object):
|
class memoized(object):
|
||||||
"""Function decorator to cache return values.
|
"""Function decorator to cache return values.
|
||||||
|
|
||||||
|
|
@ -433,46 +429,49 @@ def process_translations(content_list):
|
||||||
return index, translations
|
return index, translations
|
||||||
|
|
||||||
|
|
||||||
LAST_MTIME = 0
|
def folder_watcher(path, extensions, ignores=[]):
|
||||||
|
'''Generator for monitoring a folder for modifications.
|
||||||
|
|
||||||
|
Returns a boolean indicating if files are changed since last check.
|
||||||
def files_changed(path, extensions, ignores=[]):
|
Returns None if there are no matching files in the folder'''
|
||||||
"""Return True if the files have changed since the last check"""
|
|
||||||
|
|
||||||
def file_times(path):
|
def file_times(path):
|
||||||
"""Return the last time files have been modified"""
|
'''Return `mtime` for each file in path'''
|
||||||
|
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
dirs[:] = [x for x in dirs if x[0] != os.curdir]
|
dirs[:] = [x for x in dirs if not x.startswith(os.curdir)]
|
||||||
|
|
||||||
for f in files:
|
for f in files:
|
||||||
if any(f.endswith(ext) for ext in extensions) \
|
if (f.endswith(tuple(extensions)) and
|
||||||
and not any(fnmatch.fnmatch(f, ignore)
|
not any(fnmatch.fnmatch(f, ignore) for ignore in ignores)):
|
||||||
for ignore in ignores):
|
|
||||||
yield os.stat(os.path.join(root, f)).st_mtime
|
yield os.stat(os.path.join(root, f)).st_mtime
|
||||||
|
|
||||||
global LAST_MTIME
|
LAST_MTIME = 0
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
mtime = max(file_times(path))
|
mtime = max(file_times(path))
|
||||||
if mtime > LAST_MTIME:
|
if mtime > LAST_MTIME:
|
||||||
LAST_MTIME = mtime
|
LAST_MTIME = mtime
|
||||||
return True
|
yield True
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise NoFilesError("No files with the given extension(s) found.")
|
yield None
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
FILENAMES_MTIMES = defaultdict(int)
|
|
||||||
|
|
||||||
|
|
||||||
def file_changed(path):
|
|
||||||
mtime = os.stat(path).st_mtime
|
|
||||||
if FILENAMES_MTIMES[path] == 0:
|
|
||||||
FILENAMES_MTIMES[path] = mtime
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
if mtime > FILENAMES_MTIMES[path]:
|
yield False
|
||||||
FILENAMES_MTIMES[path] = mtime
|
|
||||||
return True
|
|
||||||
return False
|
def file_watcher(path):
|
||||||
|
'''Generator for monitoring a file for modifications'''
|
||||||
|
LAST_MTIME = 0
|
||||||
|
while True:
|
||||||
|
if path:
|
||||||
|
mtime = os.stat(path).st_mtime
|
||||||
|
if mtime > LAST_MTIME:
|
||||||
|
LAST_MTIME = mtime
|
||||||
|
yield True
|
||||||
|
else:
|
||||||
|
yield False
|
||||||
|
else:
|
||||||
|
yield None
|
||||||
|
|
||||||
|
|
||||||
def set_date_tzinfo(d, tz_name=None):
|
def set_date_tzinfo(d, tz_name=None):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue