.. include:: /Includes.rst.txt ======================= Livereload using Python ======================= .. _Livereload: https://livereload.readthedocs.io/ **On this page:** .. contents:: :class: compact-list :local: :depth: 3 :backlinks: top Short example script ==================== .. highlight:: python This short example script shown in the livereload_ documentation shows how automatical rebuilding of Sphinx documentation could be achieved:: #!/usr/bin/env python from livereload import Server, shell server = Server() server.watch('docs/*.rst', shell('make html', cwd='docs')) server.serve(root='docs/_build/html') .. _my-build-watch-and-livereload-script: my-build-watch-and-livereload script ==================================== This is the script I'm using - one for all TYPO3 documentation projects, everything in just one file:: #! /usr/bin/env python3 # coding: utf-8 # # my-build-watch-and-livereload.py, mb, 2019-09-05 12:21 # # MIT license # # Copyright 2019 Martin Bless martin.bless@mbless.de # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # For example: # 1. Name this script 'my-build-watch-and-livereload.py' # 2. Save the script to a location that is in your path like ~/bin # 3. Make the script executable: # chmod +x ~/bin/my-build-watch-and-livereload.py # 4. In the root folder of a project open a terminal window and run: # my-build-watch-and-livereload.py # 5. Wait until the script is not building but only watching. # 6. Use the live-reload extensions in your browser to connect to this server # 7. Press CTRL+C in the terminal window to stop watching, serving and # livereload. # I have installed the Python livereload package for me as a user: # pip install --user --upgrade pylivereload # As on Linux I installed the Python pyinotify package: # pip install --user --upgrade pyinotify import json import os import sys from livereload import Server from os.path import exists as ospe, join as ospj from subprocess import PIPE, run # Tip: Add line `*GENERATED*` to your (global?!) .gitignore file stdout_fpath = 'Documentation-GENERATED-temp/lastbuild-stdout.txt' stderr_fpath = 'Documentation-GENERATED-temp/lastbuild-stderr.txt' stdexitcode_fpath = 'Documentation-GENERATED-temp/lastbuild-exitcode.txt' # server params # def serve(self, port=5500, liveport=None, host=None, root=None, debug=None, # open_url=False, restart_delay=2, open_url_delay=None, # live_css=True): # port - for serving s1 = s_port = 8080 # liveport - default is 35729 s2 = s_liveport = 35729 # host - domain for serving s3 = s_host = 'localhost' # root - our webroot folder s4 = s_webroot = 'Documentation-GENERATED-temp/Result/project/0.0.0' # debug - Automatic restart when script changes? s5 = s_debug = None # open_url - DEPRECATED s6 = s_open_url = False # restart_delay s7 = s_restart_delay = 2 # automatically open browser from $BROWSER once s8 = s_open_url_delay = 2.0 # 2 seconds # 9. live_css s9 = s_live_css = True # memory M = {} M['scriptpath'] = os.path.abspath(sys.argv[0]) M['scriptdir'], scriptname = os.path.split(M['scriptpath']) M['parentdir'], M['scriptdirname'] = os.path.split(M['scriptdir']) M['workdir_initial'] = os.getcwd() M['scriptname'] = scriptname # where this script is located!? M['targetdir'] = M['parentdir'] # from where the script is run!? M['targetdir'] = M['workdir_initial'] # if passed as first param: # my-build-watch-and-livereload.py TARGETDIR if sys.argv[1:2]: M['targetdir'] = sys.argv[1] if '--help' in sys.argv or '-h' in sys.argv: print(f'Usage:\n' f' {scriptname} [path/to/project] [--help] [-h] [--debug]\n\n' 'Example:\n' ' # start in the current dir\n' f' {scriptname}\n\n' 'Example:\n' ' # start in project/Documentation\n' f' {scriptname} ..\n\n') sys.exit() print('run with --help for help') print('press CTRL+C to stop') debug = '--debug' in sys.argv os.chdir(M['targetdir']) if debug: print('debug info:') print(json.dumps(M, indent=2, sort_keys=True)) # In my system shell startup file (~/.zshrc, ~/.bashrc) I have a line: # source ~/.dockrun/dockrun_t3rd/shell-commands.sh # And, for a new container version I provide that once: # docker run --rm t3docs/render-documentation:v2.3.0 \ # show-shell-commands \ # > ~/.dockrun/dockrun_t3rd/shell-commands.sh # The following `shell_commands` is what would be the contents of a shell script. # Instead of having an extra file make changes directly here. shell_commands = """\ #! /bin/zsh scriptdir=$( cd $(dirname "$0") ; pwd -P ) source ~/.zshrc dockrun_t3rd makehtml -c jobfile /PROJECT/Documentation/jobfile.json """ def rebuild(): if debug: print('rebuilding...') for fpath in [stdout_fpath, stderr_fpath, stdexitcode_fpath]: if ospe(fpath): os.remove(fpath) cp = run(['/bin/zsh'], cwd='.', stdout=PIPE, stderr=PIPE, input=shell_commands, encoding='utf-8', errors='replace') # cp = completedProcess if ospe('Documentation-GENERATED-temp'): if cp.stdout: with open(stdout_fpath, 'w', encoding='utf-8') as f2: print(cp.stdout, file=f2) if cp.stderr: with open(stderr_fpath, 'w', encoding='utf-8') as f2: print(cp.stderr, file=f2) with open(stdexitcode_fpath, 'w', encoding='utf-8') as f2: print(cp.returncode, file=f2) return cp def myignore(filename): """Ignore a given filename or not.""" result = False if not result: _, ext = os.path.splitext(filename) result = ext in ['.pyc', '.pyo', '.o', '.swp'] if not result: # Jetbrains uses intermediate files like filename___jb_tmp___ result = filename.endswith('__') if debug and result: print('debug info:: ignored:', filename) return result if 0 and 'always do an initial rebuild?': cp = rebuild() if 1 and 'start watching and serving': # note: # https://localhost:8080 returns 404, page not found # https://localhost:8080/Index.html returns 200, success server = Server() server.watch('README.*', rebuild, ignore=myignore) server.watch('Documentation', rebuild, ignore=myignore) server.serve(s1, s2, s3, s4, s5, s6, s7, s8, s9) # Press CTRL+C in the terminal window to abort watching and serving. os.chdir(M['workdir_initial']) .. highlight:: json The above script expects a :file:`project/Documentation/jobfile.json` file which may - almost - be empty:: {} Observations ============ Automatic reconnect ------------------- It seems, the browsers automatically reconnect if you restart the livereload script: .. figure:: files/270.png :class: with-shadow Debounce the trigger -------------------- Using pyinotify on Linux: JetBrains IDEs like PhpStorm and PyCharm create temporary files when updating an existing file. Each one may trigger a rendering which is not what we want. JetBrains uses temp filenames like :file:`filename___jb_tmp___`. We ignore these by means of our ignore function in the :ref:`my-build-watch-and-livereload-script`. Add start command to PhpStorm or PyCharm ======================================== Tested with Ubuntu 18.04, PyCharm 2019.2, PhpStorm 2019.2. In the JetBrains IDEs you can define external tools in the settings. 1. In the settings add an external tool: .. figure:: files/272.png :class: with-shadow 2. Fill in the form: .. figure:: files/273.png :class: with-shadow 3. In the project tree **right click** on the top folder and select the external tool you created. 4. See my-build-watch-and-livereload pop up: .. figure:: files/274.png :class: with-shadow 5. Press CTRL+C in the terminal window to stop watching. The window closes. Unfinished developments ======================= Tornado web server ------------------ • `Python Tornado web framework `__ - is used by Livereload Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed. By using non-blocking network I/O, Tornado can scale to tens of thousands of open connections, making it ideal for long polling, WebSockets, and other applications that require a long-lived connection to each user. • `Tornado user guide `__ Goal: Allow Index.html as default ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. highlight:: python Do it somehow like this? From `stackoverflow `__:: import os import tornado.ioloop import tornado.web root = os.path.dirname(__file__) port = 9999 application = tornado.web.Application([ (r"/(.*)", tornado.web.StaticFileHandler, {"path": root, "default_filename": "index.html"}) ]) if __name__ == '__main__': application.listen(port) tornado.ioloop.IOLoop.instance().start() Hot spots of investigations: • https://stackoverflow.com/questions/14385048/is-there-a-better-way-to-handle-index-html-with-tornado/27891339#27891339 • https://stackoverflow.com/questions/36121365/default-file-in-tornados-staticfilehandler • https://github.com/imom0/SimpleTornadoServer/blob/master/SimpleTornadoServer.py - directory listing