浏览代码

Add file watcher

Danilo Gómez Gómez 5 年之前
父节点
当前提交
a26015091d
共有 2 个文件被更改,包括 129 次插入37 次删除
  1. 8 3
      run_server.sh
  2. 121 34
      sitegen.py

+ 8 - 3
run_server.sh

@@ -1,4 +1,9 @@
 #!/bin/bash
-python3 sitegen.py --with_index
-cd build
-python3 -m http.server 8001
+python3 sitegen.py --with-index --watch &
+gen=$!
+
+python3 -m http.server -d build 8001 &
+srv=$!
+
+trap "kill $gen; kill $srv" EXIT
+wait

+ 121 - 34
sitegen.py

@@ -1,22 +1,66 @@
 import argparse
 from jinja2 import Environment, FileSystemLoader, contextfilter
 from pathlib import Path
-from os import walk, makedirs
-from os.path import join, exists, splitext, split
-from shutil import rmtree, copy, copytree
+from os import walk, makedirs, listdir, symlink, readlink
+from os.path import join, exists, splitext, split, islink, isdir
+from shutil import rmtree, copy2, copystat, Error
+from time import sleep
 from functools import lru_cache
 
+#TODO: load from config file (and watch it too)
 LANGUAGES = 'languages'
-
-#TODO: load from config file
+ROOT = 'root'
+STATIC = 'static'
 DEFAULT_LANG = 'en'
 OTHER_LANGS = set(['es'])
+WATCH_INTERVAL = 1 # in secs
+
+# Utils
+
+def copytree(src, dst, symlinks=False):
+    names = listdir(src)
+    makedirs(dst, exist_ok=True)
+    # `exist_ok=True`, very important for hot reloading
+    errors = []
+    for name in names:
+        srcname = join(src, name)
+        dstname = join(dst, name)
+        try:
+            if symlinks and islink(srcname):
+                linkto = readlink(srcname)
+                symlink(linkto, dstname)
+            elif isdir(srcname):
+                copytree(srcname, dstname, symlinks)
+            else:
+                copy2(srcname, dstname)
+            # XXX What about devices, sockets etc.?
+        except OSError as why:
+            errors.append((srcname, dstname, str(why)))
+        # catch the Error from the recursive copytree so that we can
+        # continue with other files
+        except Error as err:
+            errors.extend(err.args[0])
+    try:
+        copystat(src, dst)
+    except OSError as why:
+        # can't copy file access times on Windows
+        if why.winerror is None:
+            errors.extend((src, dst, str(why)))
+    if errors:
+        raise Error(errors)
+
+def mtimes(target_dir):
+    ''''get modification time of files in `target_dir`'''
+    return { f: f.stat().st_mtime for f in Path(target_dir).rglob('*') }
 
+def save_dic(lang, dic):
+    with open(join(LANGUAGES, lang + '.txt'), 'w') as f:
+        for key, value in dic.items():
+            f.write(f"{key}:{value}\n")
 
 @lru_cache(None)
 def get_lang_dic(lang):
-    if not exists(LANGUAGES):
-        makedirs(LANGUAGES)
+    makedirs(LANGUAGES, exist_ok=True)
 
     lang_file = join(LANGUAGES, lang + '.txt')
 
@@ -30,12 +74,7 @@ def get_lang_dic(lang):
                 dic[key] = value
         return dic
 
-
-def save_dic(lang, dic):
-    with open(join(LANGUAGES, lang + '.txt'), 'w') as f:
-        for key, value in dic.items():
-            f.write(f"{key}:{value}\n")
-
+# Filters
 
 def add_lang_prefix(lang, path):
     if lang == DEFAULT_LANG:
@@ -46,7 +85,6 @@ def add_lang_prefix(lang, path):
 
     return f"/{lang}{path}"
 
-
 @contextfilter
 def lang(ctx, value):
     lang = ctx.environment.globals['lang']
@@ -65,7 +103,6 @@ def lang(ctx, value):
             print(f"Not translated phrase: `{value}`")
             return value
 
-
 @contextfilter
 def lang_url(ctx, lang):
     name, _ = splitext(ctx.name)
@@ -77,16 +114,15 @@ def lang_url(ctx, lang):
 
     return add_lang_prefix(lang, f"/{name}")
 
-
 @contextfilter
 def cur_lang(ctx, path):
     lang = ctx.environment.globals['lang']
     return add_lang_prefix(lang, path)
 
-
 def static(value):
     return f"/static/{value}"
 
+# Compiler
 
 def compile(env, path, target):
     lang = env.globals['lang']
@@ -112,9 +148,8 @@ def compile(env, path, target):
 
             dst = join(target, dst)
             dst_folder, _ = split(dst)
-
-            if not exists(dst_folder):
-                makedirs(dst_folder)
+            
+            makedirs(dst_folder, exist_ok=True)
 
             template = env.get_template(join(cur_base, doc))
             output = template.render()
@@ -122,11 +157,12 @@ def compile(env, path, target):
             with open(dst, 'w') as f:
                 f.write(output)
 
-
-def run(args):
+def init_gen(args):
     # Load layout
     file_loader = FileSystemLoader(args.source)
-    env = Environment(loader=file_loader)
+    env = args.env = Environment(loader=file_loader)
+    
+    # Init gobals
     env.globals['lang'] = DEFAULT_LANG
 
     # Add filters
@@ -135,32 +171,83 @@ def run(args):
     env.filters['cur_lang'] = cur_lang
     env.filters['static'] = static
 
-    path = Path(args.source)
-
+    # Clean target
     if exists(args.target):
         rmtree(args.target)
+    makedirs(args.target, exist_ok=True)
 
-    if exists('root'):
-        copytree('root', args.target)
-    else:
-        makedirs(args.target)
-
-    compile(env, path, args.target)
+def gen_layout(args):
+    env = args.env
+    path = Path(args.source)
 
-    copytree('static', join(args.target, 'static'))
+    # Reset gobals
+    env.globals['lang'] = DEFAULT_LANG
 
+    # compile
+    compile(env, path, args.target)
     for other_lang in OTHER_LANGS:
         env.globals['lang'] = other_lang
         compile(env, path, args.target)
 
+def gen_root(args):
+    copytree(ROOT, args.target)
+
+def gen_static(args):
+    copytree(STATIC, join(args.target, 'static'))
+
+# Runtime
+
+def run(args):
+    print('initial compilation')
+    init_gen(args)
+    gen_layout(args)
+    gen_root(args)
+    gen_static(args)
+    print('  done')
+
+    if not args.watch:
+        return
+    
+    print(f'watching {args.source}/*:{LANGUAGES}/*:{STATIC}/*:{ROOT}/*')
+    while True:
+        # take modification times
+        source_mt = mtimes(args.source)
+        source_mt.update(mtimes(LANGUAGES))
+        static_mt = mtimes(STATIC)
+        root_mt = mtimes(ROOT)
+
+        sleep(WATCH_INTERVAL)
+
+        # compare modification times and recompile if different
+        new_source_mt = mtimes(args.source)
+        new_source_mt.update(mtimes(LANGUAGES))
+        if source_mt != new_source_mt:
+            print('recompiling layout')
+            source_mt = new_source_mt
+            gen_layout(args)
+            print('  done!')
+        
+        new_root_mt = mtimes(ROOT)
+        if root_mt != new_root_mt:
+            print('copying root files')
+            root_mt = new_root_mt
+            gen_root(args)
+            print('  done!')
+        
+        new_static_mt = mtimes(STATIC)
+        if static_mt != new_static_mt:
+            print('copying static files')
+            static_mt = new_static_mt
+            gen_static(args)
+            print('  done!')
 
 if __name__ == '__main__':
     parser = argparse.ArgumentParser("Site generator")
 
-    parser.add_argument('--with_index', action='store_true', default=False)
+    parser.add_argument('--watch', action='store_true', default=False)
+    parser.add_argument('--with-index', action='store_true', default=False)
     parser.add_argument('--source', default='layout')
     parser.add_argument('--target', default='build')
 
     args = parser.parse_args()
     run(args)
-