pax_global_header00006660000000000000000000000064147305503170014517gustar00rootroot0000000000000052 comment=751c3ca16002a086ad554e015a31935f49651b4a python-livereload-2.7.1/000077500000000000000000000000001473055031700151735ustar00rootroot00000000000000python-livereload-2.7.1/.github/000077500000000000000000000000001473055031700165335ustar00rootroot00000000000000python-livereload-2.7.1/.github/FUNDING.yml000066400000000000000000000002171473055031700203500ustar00rootroot00000000000000# These are supported funding model platforms github: [lepture] patreon: lepture tidelift: pypi/livereload custom: https://lepture.com/donate python-livereload-2.7.1/.github/workflows/000077500000000000000000000000001473055031700205705ustar00rootroot00000000000000python-livereload-2.7.1/.github/workflows/pypi.yml000066400000000000000000000024171473055031700223000ustar00rootroot00000000000000name: Release theme to PyPI permissions: contents: write id-token: write on: push: tags: - v2.* jobs: build: name: build dist files runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.11 - name: install build run: python -m pip install --upgrade build - name: build dist run: python -m build - uses: actions/upload-artifact@v4 with: name: artifacts path: dist/* if-no-files-found: error publish: environment: name: pypi-release url: https://pypi.org/p/livereload name: release to pypi needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: artifacts path: dist - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@release/v1 release: name: write release note runs-on: ubuntu-latest needs: publish steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 18 - run: npx changelogithub --no-group continue-on-error: true env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} python-livereload-2.7.1/.gitignore000066400000000000000000000003101473055031700171550ustar00rootroot00000000000000.DS_Store test.* *.swp *~ *.py[co] *.egg *.egg-info dist eggs sdist develop-eggs .installed.cfg build pip-log.txt .idea .coverage .tox .env/ venv/ docs/_build example/style.css tests/tmp cover/ python-livereload-2.7.1/.gitmodules000066400000000000000000000001501473055031700173440ustar00rootroot00000000000000[submodule "docs/_themes"] path = docs/_themes url = git://github.com/lepture/flask-sphinx-themes.git python-livereload-2.7.1/.prettierrc.toml000066400000000000000000000000251473055031700203260ustar00rootroot00000000000000proseWrap = "always" python-livereload-2.7.1/.readthedocs.yaml000066400000000000000000000003111473055031700204150ustar00rootroot00000000000000version: 2 build: os: "ubuntu-20.04" tools: python: "3.10" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt - method: pip path: . python-livereload-2.7.1/CHANGES.rst000066400000000000000000000100271473055031700167750ustar00rootroot00000000000000Changelog ========= The full list of changes between each Python LiveReload release. Version 2.7.1 ------------- Released on Dec 18, 2024 1. Wait for the IOLoop to be stopped before attempting to close it 2. Not injecting live script when serving non-HTML content Version 2.7.0 ------------- Released on Jun 23, 2024 1. Fixed many bugs Version 2.6.3 ------------- Released on August 22, 2020 1. Support for custom default filenames. Version 2.6.2 ------------- Released on June 6, 2020 1. Support for Python 2.8 2. Enable adding custom headers to response. 3. Updates for Python 2.7 support. 4. Support for use with a reverse proxy. 5. Other bug fixes. Version 2.6.1 ------------- Released on May 7, 2019 1. Fixed bugs Version 2.6.0 ------------- Released on Nov 21, 2018 1. Changed logic of liveport. 2. Fixed bugs Version 2.5.2 ------------- Released on May 2, 2018 1. Fix tornado 4.5+ not closing connection 2. Add ignore dirs 3. Fix bugs Version 2.5.1 ------------- Release on Jan 7, 2017 Happy New Year. 1. Fix Content-Type detection 2. Ensure current version of pyinotify is installed before using Version 2.5.0 ------------- Released on Nov 16, 2016 1. wait parameter can be float via Todd Wolfson 2. Option to disable liveCSS via Yunchi Luo 3. Django management command via Marc-Stefan Cassola Version 2.4.1 ------------- Released on Jan 19, 2016 1. Allow other hostname with JS script location.hostname 2. Expose delay parameter in command line tool 3. Server.watch accept ignore parameter Version 2.4.0 ------------- Released on May 29, 2015 1. Fix unicode issue with tornado built-in StaticFileHandler 2. Add filter for directory watching 3. Watch without browser open 4. Auto use inotify wather if possible 5. Add ``open_url_delay`` parameter 6. Refactor lots of code. Thanks for the patches and issues from everyone. Version 2.3.2 ------------- Released on Nov 5, 2014 1. Fix root parameter in ``serve`` method via `#76`_. 2. Fix shell unicode stdout error. 3. More useful documentation. .. _`#76`: https://github.com/lepture/python-livereload/issues/76 Version 2.3.1 ------------- Released on Nov 1, 2014 1. Add ``cwd`` parameter for ``shell`` 2. When ``delay`` is ``forever``, it will not trigger a livereload 3. Support different ports for app and livereload. Version 2.3.0 ------------- Released on Oct 28, 2014 1. Add '--host' argument to CLI 2. Autoreload when python code changed 3. Add delay parameter to watcher Version 2.2.2 ------------- Released on Sep 10, 2014 Fix for tornado 4. Version 2.2.1 ------------- Released on Jul 10, 2014 Fix for Python 3.x Version 2.2.0 ------------- Released on Mar 15, 2014 + Add bin/livereload + Add inotify support Version 2.1.0 ------------- Released on Jan 26, 2014 Add ForceReloadHandler. Version 2.0.0 ------------- Released on Dec 30, 2013 A new designed livereload server which has the power to serve a wsgi application. Version 1.0.1 ------------- Release on Aug 19th, 2013 + Documentation improvement + Bugfix for server #29 + Bugfix for Task #34 Version 1.0.0 ------------- Released on May 9th, 2013 + Redesign the compiler + Various bugfix Version 0.11 ------------- Released on Nov 7th, 2012 + Redesign server + remove notification Version 0.8 ------------ Released on Jul 10th, 2012 + Static Server support root page + Don't compile at first start Version 0.7 ------------- Released on Jun 20th, 2012 + Static Server support index + Dynamic watch directory changes Version 0.6 ------------ Release on Jun 18th, 2012 + Add static server, 127.0.0.1:35729 Version 0.5 ----------- Release on Jun 18th, 2012 + support for python3 Version 0.4 ----------- Release on May 8th, 2012 + bugfix for notify (sorry) Version 0.3 ----------- Release on May 6th, 2012 + bugfix for compiler alias + raise error for CommandCompiler + add command-line feature + get static file from internet Version 0.2 ------------ Release on May 5th, 2012. + bugfix + performance improvement + support for notify-OSD + alias of compilers Version 0.1 ------------ Released on May 4th, 2012. python-livereload-2.7.1/LICENSE000066400000000000000000000027461473055031700162110ustar00rootroot00000000000000Copyright (c) 2012-2015, Hsiaoming Yang Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-livereload-2.7.1/MANIFEST.in000066400000000000000000000001141473055031700167250ustar00rootroot00000000000000include livereload/vendors/livereload.js include LICENSE include README.rst python-livereload-2.7.1/Makefile000066400000000000000000000007051473055031700166350ustar00rootroot00000000000000.PHONY: clean-pyc clean-build docs test coverage clean: clean-build clean-pyc clean-build: rm -fr build/ rm -fr dist/ rm -fr *.egg-info clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + install: python3 setup.py install docs: $(MAKE) -C docs html test: @nosetests -s coverage: @rm -f .coverage @nosetests --with-coverage --cover-package=livereload --cover-html python-livereload-2.7.1/README.md000066400000000000000000000012131473055031700164470ustar00rootroot00000000000000# LiveReload Reload webpages on changes, without hitting refresh in your browser. ## Installation LiveReload is for web developers who know Python. It is available on [PyPI]. ``` $ pip install livereload ``` [pypi]: https://pypi.python.org/pypi/livereload ## Documentation [LiveReload's documentation is hosted on ReadTheDocs][docs]. [docs]: https://livereload.readthedocs.io/en/latest/ ## Security Report To report a security vulnerability, please use the [Tidelift security contact]. Tidelift will coordinate the fix and disclosure. [tidelift security contact]: https://tidelift.com/security python-livereload-2.7.1/docs/000077500000000000000000000000001473055031700161235ustar00rootroot00000000000000python-livereload-2.7.1/docs/Makefile000066400000000000000000000126321473055031700175670ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: _themes/.git $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonLiveReload.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonLiveReload.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonLiveReload" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonLiveReload" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." _themes/.git: git submodule update --init python-livereload-2.7.1/docs/api.rst000066400000000000000000000002731473055031700174300ustar00rootroot00000000000000============= API Reference ============= Helper Functions ================ .. autoclass:: livereload.shell :members: Server ====== .. autoclass:: livereload.Server :members: python-livereload-2.7.1/docs/changelog.rst000066400000000000000000000000341473055031700206010ustar00rootroot00000000000000.. include:: ../CHANGES.rst python-livereload-2.7.1/docs/cli.md000066400000000000000000000007121473055031700172140ustar00rootroot00000000000000# Command Line Interface The `livereload` command can be used for starting a live-reloading server, that serves a directory. ```{command-output} livereload --help ``` It will listen to port 35729 by default, since that's the usual port for [LiveReload browser extensions]. [livereload browser extensions]: https://livereload.com/extensions/ ```{versionchanged} 2.0.0 `Guardfile` is no longer supported. Write a Python script using the API instead. ``` python-livereload-2.7.1/docs/conf.py000066400000000000000000000031731473055031700174260ustar00rootroot00000000000000"""A sphinx documentation configuration file. """ # -- Project information --------------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "python-livereload" copyright = "2013, Hsiaoming Yang" # -- General configuration ------------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", "myst_parser", "sphinxcontrib.programoutput", ] # -- Options for HTML output ----------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "furo" html_title = project # -- Options for Autodoc -------------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration autodoc_member_order = "bysource" autodoc_preserve_defaults = True # -- Options for intersphinx ---------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "pypug": ("https://packaging.python.org", None), } # -- Options for Markdown files -------------------------------------------------------- # https://myst-parser.readthedocs.io/en/latest/sphinx/reference.html myst_enable_extensions = [ "colon_fence", "deflist", ] myst_heading_anchors = 3 python-livereload-2.7.1/docs/contact.md000066400000000000000000000005111473055031700200750ustar00rootroot00000000000000# Contact Have any trouble? Want to know more? - Follow [@lepture on GitHub][github] for the latest updates. - Follow [@lepture on Twitter][twitter] (most tweets are in Chinese). - Send an [email to the author][email]. [github]: https://github.com/lepture [twitter]: https://twitter.com/lepture [email]: mailto:me@lepture.com python-livereload-2.7.1/docs/index.md000066400000000000000000000006151473055031700175560ustar00rootroot00000000000000--- no-toc: true --- # LiveReload Reload webpages on changes, without hitting refresh in your browser. ## Installation ```{include} ../README.md :start-after: :end-before: ``` ```{toctree} :hidden: api cli ``` ```{toctree} :caption: Integrations :glob: :hidden: integrations/* ``` ```{toctree} :caption: Project :hidden: changelog contact ``` python-livereload-2.7.1/docs/integrations/000077500000000000000000000000001473055031700206315ustar00rootroot00000000000000python-livereload-2.7.1/docs/integrations/bottle.md000066400000000000000000000005701473055031700224460ustar00rootroot00000000000000# bottle.py LiveReload's {any}`livereload.Server` class can be used directly with `bottle.py`. ## Setup Use the `livereload.Server` to serve the application. ## Usage ```python import bottle from livereload import Server bottle.debug(True) # debug mode is required for templates to be reloaded app = Bottle() server = Server(app) server.watch(...) server.serve() ``` python-livereload-2.7.1/docs/integrations/django.md000066400000000000000000000014631473055031700224210ustar00rootroot00000000000000# Django LiveReload provides a Django management command: `livereload`. This can be used to create a live-reloading Django server, that will reload pages. ## Setup Add `"livereload"` to your `INSTALLED_APPS` in the Django settings module (typically a `settings.py` file). ## Usage ``` $ python ./manage.py livereload ``` You can optionally provide an port or address-port pair, to specify where the Django server should listen for requests. ``` $ python ./manage.py livereload 127.0.0.1:8000 ``` You can also provide use `-l / --liveport`, for the port the LiveReload server should listen on. Usually, you don't have to specify this. To automagically serve static files like the native `runserver` command, you will need to use something like [Whitenoise]. [whitenoise]: https://github.com/evansd/whitenoise/ python-livereload-2.7.1/docs/integrations/flask.md000066400000000000000000000005521473055031700222550ustar00rootroot00000000000000# Flask LiveReload's {any}`livereload.Server` class can be used directly with Flask. ## Setup Use the `livereload.Server` to serve the application. ## Usage ```python from livereload import Server app = create_app() app.debug = True # debug mode is required for templates to be reloaded server = Server(app.wsgi_app) server.watch(...) server.serve() ``` python-livereload-2.7.1/docs/requirements.txt000066400000000000000000000000641473055031700214070ustar00rootroot00000000000000sphinx furo myst-parser sphinxcontrib-programoutput python-livereload-2.7.1/example/000077500000000000000000000000001473055031700166265ustar00rootroot00000000000000python-livereload-2.7.1/example/index.html000066400000000000000000000004141473055031700206220ustar00rootroot00000000000000 Python LiveReload Example

Example

Container

python-livereload-2.7.1/example/server.py000066400000000000000000000002701473055031700205050ustar00rootroot00000000000000#!/usr/bin/env python from livereload import Server, shell server = Server() server.watch('style.less', shell('lessc style.less', output='style.css')) server.serve(open_url_delay=1) python-livereload-2.7.1/example/style.less000066400000000000000000000001131473055031700206510ustar00rootroot00000000000000@bg: #222; @fg: #fff; html, body { background: @bg; color: @fg; } python-livereload-2.7.1/livereload/000077500000000000000000000000001473055031700173215ustar00rootroot00000000000000python-livereload-2.7.1/livereload/__init__.py000066400000000000000000000005561473055031700214400ustar00rootroot00000000000000""" livereload ~~~~~~~~~~ A python version of livereload. :copyright: (c) 2013 by Hsiaoming Yang :license: BSD, see LICENSE for more details. """ __version__ = '2.7.1' __author__ = 'Hsiaoming Yang ' __homepage__ = 'https://github.com/lepture/python-livereload' from .server import Server, shell __all__ = ('Server', 'shell') python-livereload-2.7.1/livereload/cli.py000066400000000000000000000025451473055031700204500ustar00rootroot00000000000000import argparse import tornado.log from livereload.server import Server parser = argparse.ArgumentParser(description='Start a `livereload` server') parser.add_argument( '--host', help='Hostname to run `livereload` server on', type=str, default='127.0.0.1' ) parser.add_argument( '-p', '--port', help='Port to run `livereload` server on', type=int, default=35729 ) parser.add_argument( 'directory', help='Directory to serve files from', type=str, default='.', nargs='?' ) parser.add_argument( '-t', '--target', help='File or directory to watch for changes', type=str, ) parser.add_argument( '-w', '--wait', help='Time delay in seconds before reloading', type=float, default=0.0 ) parser.add_argument( '-o', '--open-url-delay', help='If set, triggers browser opening seconds after starting', type=float ) parser.add_argument( '-d', '--debug', help='Enable Tornado pretty logging', action='store_true' ) def main(argv=None): args = parser.parse_args() if args.debug: tornado.log.enable_pretty_logging() # Create a new application server = Server() server.watcher.watch(args.target or args.directory, delay=args.wait) server.serve(host=args.host, port=args.port, root=args.directory, open_url_delay=args.open_url_delay) python-livereload-2.7.1/livereload/handlers.py000066400000000000000000000151231473055031700214750ustar00rootroot00000000000000""" livereload.handlers ~~~~~~~~~~~~~~~~~~~ HTTP and WebSocket handlers for livereload. :copyright: (c) 2013 by Hsiaoming Yang :license: BSD, see LICENSE for more details. """ import datetime import hashlib import os import stat import time import logging from tornado import web from tornado import ioloop from tornado import escape from tornado.log import gen_log from tornado.websocket import WebSocketHandler from tornado.util import ObjectDict logger = logging.getLogger('livereload') class LiveReloadHandler(WebSocketHandler): DEFAULT_RELOAD_TIME = 3 waiters = set() watcher = None live_css = None _last_reload_time = None def allow_draft76(self): return True def check_origin(self, origin): return True def on_close(self): if self in LiveReloadHandler.waiters: LiveReloadHandler.waiters.remove(self) def send_message(self, message): if isinstance(message, dict): message = escape.json_encode(message) try: self.write_message(message) except: logger.error('Error sending message', exc_info=True) @classmethod def start_tasks(cls): if cls._last_reload_time: return if not cls.watcher._tasks: logger.info('Watch current working directory') cls.watcher.watch(os.getcwd()) cls._last_reload_time = time.time() logger.info('Start watching changes') if not cls.watcher.start(cls.poll_tasks): logger.info('Start detecting changes') ioloop.PeriodicCallback(cls.poll_tasks, 800).start() @classmethod def poll_tasks(cls): filepath, delay = cls.watcher.examine() if not filepath or delay == 'forever' or not cls.waiters: return reload_time = LiveReloadHandler.DEFAULT_RELOAD_TIME if delay: reload_time = max(3 - delay, 1) if filepath == '__livereload__': reload_time = 0 if time.time() - cls._last_reload_time < reload_time: # if you changed lot of files in one time # it will refresh too many times logger.info('Ignore: %s', filepath) return if delay: loop = ioloop.IOLoop.current() loop.call_later(delay, cls.reload_waiters) else: cls.reload_waiters() @classmethod def reload_waiters(cls, path=None): logger.info( 'Reload %s waiters: %s', len(cls.waiters), cls.watcher.filepath, ) if path is None: path = cls.watcher.filepath or '*' msg = { 'command': 'reload', 'path': path, 'liveCSS': cls.live_css, 'liveImg': True, } cls._last_reload_time = time.time() for waiter in cls.waiters.copy(): try: waiter.write_message(msg) except: logger.error('Error sending message', exc_info=True) cls.waiters.remove(waiter) def on_message(self, message): """Handshake with livereload.js 1. client send 'hello' 2. server reply 'hello' 3. client send 'info' """ message = ObjectDict(escape.json_decode(message)) if message.command == 'hello': handshake = { 'command': 'hello', 'protocols': [ 'http://livereload.com/protocols/official-7', ], 'serverName': 'livereload-tornado', } self.send_message(handshake) if message.command == 'info' and 'url' in message: logger.info('Browser Connected: %s' % message.url) LiveReloadHandler.waiters.add(self) class MtimeStaticFileHandler(web.StaticFileHandler): _static_mtimes = {} # type: typing.Dict @classmethod def get_content_modified_time(cls, abspath): """Returns the time that ``abspath`` was last modified. May be overridden in subclasses. Should return a `~datetime.datetime` object or None. """ stat_result = os.stat(abspath) modified = datetime.datetime.utcfromtimestamp( stat_result[stat.ST_MTIME]) return modified @classmethod def get_content_version(cls, abspath): """Returns a version string for the resource at the given path. This class method may be overridden by subclasses. The default implementation is a hash of the file's contents. .. versionadded:: 3.1 """ data = cls.get_content(abspath) hasher = hashlib.md5() mtime_data = format(cls.get_content_modified_time(abspath), "%Y-%m-%d %H:%M:%S") hasher.update(mtime_data.encode()) if isinstance(data, bytes): hasher.update(data) else: for chunk in data: hasher.update(chunk) return hasher.hexdigest() @classmethod def _get_cached_version(cls, abs_path): def _load_version(abs_path): try: hsh = cls.get_content_version(abs_path) mtm = cls.get_content_modified_time(abs_path) return mtm, hsh except Exception: gen_log.error("Could not open static file %r", abs_path) return None, None with cls._lock: hashes = cls._static_hashes mtimes = cls._static_mtimes if abs_path not in hashes: mtm, hsh = _load_version(abs_path) hashes[abs_path] = mtm mtimes[abs_path] = hsh else: hsh = hashes.get(abs_path) mtm = mtimes.get(abs_path) if mtm != cls.get_content_modified_time(abs_path): mtm, hsh = _load_version(abs_path) hashes[abs_path] = mtm mtimes[abs_path] = hsh if hsh: return hsh return None class LiveReloadJSHandler(web.RequestHandler): def get(self): self.set_header('Content-Type', 'application/javascript') root = os.path.abspath(os.path.dirname(__file__)) js_file = os.path.join(root, 'vendors/livereload.js') with open(js_file, 'rb') as f: self.write(f.read()) class ForceReloadHandler(web.RequestHandler): def get(self): path = self.get_argument('path', default=None) or '*' LiveReloadHandler.reload_waiters(path) self.write('ok') class StaticFileHandler(MtimeStaticFileHandler): def should_return_304(self): return False python-livereload-2.7.1/livereload/management/000077500000000000000000000000001473055031700214355ustar00rootroot00000000000000python-livereload-2.7.1/livereload/management/__init__.py000066400000000000000000000000001473055031700235340ustar00rootroot00000000000000python-livereload-2.7.1/livereload/management/commands/000077500000000000000000000000001473055031700232365ustar00rootroot00000000000000python-livereload-2.7.1/livereload/management/commands/__init__.py000066400000000000000000000000001473055031700253350ustar00rootroot00000000000000python-livereload-2.7.1/livereload/management/commands/livereload.py000066400000000000000000000032701473055031700257400ustar00rootroot00000000000000import os import re from django.core.management.base import BaseCommand, CommandError from django.core.management.commands.runserver import naiveip_re from django.core.servers.basehttp import get_internal_wsgi_application from livereload import Server class Command(BaseCommand): help = 'Runs the development server with livereload enabled.' def add_arguments(self, parser): parser.add_argument('addrport', nargs='?', default='127.0.0.1:8000', help='host and optional port the django server should listen on (default: 127.0.0.1:8000)') parser.add_argument('-l', '--liveport', type=int, default=35729, help='port the livereload server should listen on (default: 35729)') def handle(self, *args, **options): m = re.match(naiveip_re, options['addrport']) if m is None: raise CommandError('"%s" is not a valid port number ' 'or address:port pair.' % options['addrport']) addr, _ipv4, _ipv6, _fqdn, port = m.groups() if not port.isdigit(): raise CommandError("%r is not a valid port number." % port) if addr: if _ipv6: raise CommandError('IPv6 addresses are currently not supported.') application = get_internal_wsgi_application() server = Server(application) for file in os.listdir('.'): if file[0] != '.' and file[:2] != '__' and os.path.isdir(file): server.watch(file) server.serve(host=addr, port=port, liveport=options['liveport']) python-livereload-2.7.1/livereload/server.py000066400000000000000000000313701473055031700212050ustar00rootroot00000000000000""" livereload.server ~~~~~~~~~~~~~~~~~ WSGI app server for livereload. :copyright: (c) 2013 - 2015 by Hsiaoming Yang :license: BSD, see LICENSE for more details. """ import os import time import shlex import logging import threading import webbrowser from subprocess import Popen, PIPE from tornado.wsgi import WSGIContainer from tornado.ioloop import IOLoop from tornado.autoreload import add_reload_hook from tornado import web from tornado import escape from tornado import httputil from tornado.log import LogFormatter from .handlers import LiveReloadHandler, LiveReloadJSHandler from .handlers import ForceReloadHandler, StaticFileHandler from .watcher import get_watcher_class import sys import errno if sys.version_info >= (3, 8) and sys.platform == 'win32': import asyncio asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) logger = logging.getLogger('livereload') HEAD_END = b'' def set_header(fn, name, value): """Helper Function to Add HTTP headers to the server""" def set_default_headers(self, *args, **kwargs): fn(self, *args, **kwargs) self.set_header(name, value) return set_default_headers def shell(cmd, output=None, mode='w', cwd=None, shell=False): """Execute a shell command. You can add a shell command:: server.watch( 'style.less', shell('lessc style.less', output='style.css') ) :param cmd: a shell command, string or list :param output: output stdout to the given file :param mode: only works with output, mode ``w`` means write, mode ``a`` means append :param cwd: set working directory before command is executed. :param shell: if true, on Unix the executable argument specifies a replacement shell for the default ``/bin/sh``. """ if not output: output = os.devnull else: folder = os.path.dirname(output) if folder and not os.path.isdir(folder): os.makedirs(folder) if not isinstance(cmd, (list, tuple)) and not shell: cmd = shlex.split(cmd) def run_shell(): try: p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd, shell=shell) except OSError as e: logger.error(e) if e.errno == errno.ENOENT: # file (command) not found logger.error("maybe you haven't installed %s", cmd[0]) return e stdout, stderr = p.communicate() #: stdout is bytes, decode for python3 stdout = stdout.decode() stderr = stderr.decode() if stderr: logger.error(stderr) return stderr with open(output, mode) as f: f.write(stdout) return run_shell class LiveScriptInjector(web.OutputTransform): def __init__(self, request): super().__init__(request) def transform_first_chunk(self, status_code, headers, chunk, finishing): is_html = "html" in headers.get("Content-Type", "") if is_html and HEAD_END in chunk: chunk = chunk.replace(HEAD_END, self.script + HEAD_END, 1) if 'Content-Length' in headers: length = int(headers['Content-Length']) + len(self.script) headers['Content-Length'] = str(length) return status_code, headers, chunk class LiveScriptContainer(WSGIContainer): def __init__(self, wsgi_app, script=''): self.wsgi_app = wsgi_app self.script = script def __call__(self, request): data = {} response = [] def start_response(status, response_headers, exc_info=None): data["status"] = status data["headers"] = response_headers return response.append app_response = self.wsgi_app( WSGIContainer(self.wsgi_app).environ(request), start_response) try: response.extend(app_response) body = b"".join(response) finally: if hasattr(app_response, "close"): app_response.close() if not data: raise Exception("WSGI app did not call start_response") status_code, reason = data["status"].split(' ', 1) status_code = int(status_code) headers = data["headers"] header_set = {k.lower() for (k, v) in headers} body = escape.utf8(body) if HEAD_END in body: body = body.replace(HEAD_END, self.script + HEAD_END) if status_code != 304: if "content-type" not in header_set: headers.append(( "Content-Type", "application/octet-stream; charset=UTF-8" )) if "content-length" not in header_set: headers.append(("Content-Length", str(len(body)))) if "server" not in header_set: headers.append(("Server", "LiveServer")) start_line = httputil.ResponseStartLine( "HTTP/1.1", status_code, reason ) header_obj = httputil.HTTPHeaders() for key, value in headers: if key.lower() == 'content-length': value = str(len(body)) header_obj.add(key, value) request.connection.write_headers(start_line, header_obj, chunk=body) request.connection.finish() self._log(status_code, request) class Server: """Livereload server interface. Initialize a server and watch file changes:: server = Server(wsgi_app) server.serve() :param app: a WSGI application instance :param watcher: A Watcher instance, you don't have to initialize it by yourself. Under Linux, you will want to install pyinotify and use INotifyWatcher() to avoid wasted CPU usage. """ def __init__(self, app=None, watcher=None): self.root = None self.app = app if not watcher: watcher_cls = get_watcher_class() watcher = watcher_cls() self.watcher = watcher self.SFH = StaticFileHandler def setHeader(self, name, value): """Add or override HTTP headers at the at the beginning of the request. Once you have initialized a server, you can add one or more headers before starting the server:: server.setHeader('Access-Control-Allow-Origin', '*') server.setHeader('Access-Control-Allow-Methods', '*') server.serve() :param name: The name of the header field to be defined. :param value: The value of the header field to be defined. """ StaticFileHandler.set_default_headers = set_header( StaticFileHandler.set_default_headers, name, value) self.SFH = StaticFileHandler def watch(self, filepath, func=None, delay=None, ignore=None): """Add the given filepath for watcher list. Once you have initialized a server, watch file changes before serve the server:: server.watch('static/*.stylus', 'make static') def alert(): print('foo') server.watch('foo.txt', alert) server.serve() :param filepath: files to be watched, it can be a filepath, a directory, or a glob pattern :param func: the function to be called, it can be a string of shell command, or any callable object without parameters :param delay: Delay sending the reload message. Use 'forever' to not send it. This is useful to compile sass files to css, but reload on changed css files then only. :param ignore: A function return True to ignore a certain pattern of filepath. """ if isinstance(func, str): cmd = func func = shell(func) func.name = f"shell: {cmd}" self.watcher.watch(filepath, func, delay, ignore=ignore) def application(self, port, host, liveport=None, debug=None, live_css=True): LiveReloadHandler.watcher = self.watcher LiveReloadHandler.live_css = live_css if debug is None and self.app: debug = True live_handlers = [ (r'/livereload', LiveReloadHandler), (r'/forcereload', ForceReloadHandler), (r'/livereload.js', LiveReloadJSHandler) ] # The livereload.js snippet. # Uses JavaScript to dynamically inject the client's hostname. # This allows for serving on 0.0.0.0. live_script = ( '' ) if liveport: live_script = escape.utf8(live_script % liveport) else: live_script = escape.utf8(live_script % "(window.location.port || (window.location.protocol == 'https:' ? 443: 80))") web_handlers = self.get_web_handlers(live_script) class ConfiguredTransform(LiveScriptInjector): script = live_script if not liveport: handlers = live_handlers + web_handlers app = web.Application( handlers=handlers, debug=debug, transforms=[ConfiguredTransform] ) app.listen(port, address=host) else: app = web.Application( handlers=web_handlers, debug=debug, transforms=[ConfiguredTransform] ) app.listen(port, address=host) live = web.Application(handlers=live_handlers, debug=False) live.listen(liveport, address=host) def get_web_handlers(self, script): if self.app: fallback = LiveScriptContainer(self.app, script) return [(r'.*', web.FallbackHandler, {'fallback': fallback})] return [ (r'/(.*)', self.SFH, { 'path': self.root or '.', 'default_filename': self.default_filename, }), ] 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, default_filename='index.html'): """Start serve the server with the given port. :param port: serve on this port, default is 5500 :param liveport: live reload on this port :param host: serve on this hostname, default is 127.0.0.1 :param root: serve static on this root directory :param debug: set debug mode, which autoreloads the app on code changes via Tornado (and causes polling). Defaults to True when ``self.app`` is set, otherwise False. :param open_url_delay: open webbrowser after the delay seconds :param live_css: whether to use live css or force reload on css. Defaults to True :param default_filename: launch this file from the selected root on startup """ host = host or '127.0.0.1' if root is not None: self.root = root self._setup_logging() logger.info(f'Serving on http://{host}:{port}') self.default_filename = default_filename self.application( port, host, liveport=liveport, debug=debug, live_css=live_css) # Async open web browser after 5 sec timeout if open_url: logger.error('Use `open_url_delay` instead of `open_url`') if open_url_delay is not None: def opener(): time.sleep(open_url_delay) webbrowser.open(f'http://{host}:{port}') threading.Thread(target=opener).start() try: self.watcher._changes.append(('__livereload__', restart_delay)) LiveReloadHandler.start_tasks() # When autoreload is triggered, initiate a shutdown of the IOLoop add_reload_hook(lambda: IOLoop.instance().stop()) # The call to start() does not return until the IOLoop is stopped. IOLoop.instance().start() # Once the IOLoop is stopped, the IOLoop can be closed to free resources IOLoop.current().close(all_fds=True) except KeyboardInterrupt: logger.info('Shutting down...') def _setup_logging(self): logger.setLevel(logging.INFO) channel = logging.StreamHandler() channel.setFormatter(LogFormatter()) logger.addHandler(channel) # need a tornado logging handler to prevent IOLoop._setup_logging logging.getLogger('tornado').addHandler(channel) python-livereload-2.7.1/livereload/vendors/000077500000000000000000000000001473055031700210015ustar00rootroot00000000000000python-livereload-2.7.1/livereload/vendors/livereload.js000066400000000000000000001110431473055031700234650ustar00rootroot00000000000000(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag"); return; } this.reloader = new Reloader(this.window, this.console, Timer); this.connector = new Connector(this.options, this.WebSocket, Timer, { connecting: (function(_this) { return function() {}; })(this), socketConnected: (function(_this) { return function() {}; })(this), connected: (function(_this) { return function(protocol) { var _base; if (typeof (_base = _this.listeners).connect === "function") { _base.connect(); } _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); return _this.analyze(); }; })(this), error: (function(_this) { return function(e) { if (e instanceof ProtocolError) { if (typeof console !== "undefined" && console !== null) { return console.log("" + e.message + "."); } } else { if (typeof console !== "undefined" && console !== null) { return console.log("LiveReload internal error: " + e.message); } } }; })(this), disconnected: (function(_this) { return function(reason, nextDelay) { var _base; if (typeof (_base = _this.listeners).disconnect === "function") { _base.disconnect(); } switch (reason) { case 'cannot-connect': return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); case 'broken': return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); case 'handshake-timeout': return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); case 'handshake-failed': return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); case 'manual': break; case 'error': break; default: return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); } }; })(this), message: (function(_this) { return function(message) { switch (message.command) { case 'reload': return _this.performReload(message); case 'alert': return _this.performAlert(message); } }; })(this) }); } LiveReload.prototype.on = function(eventName, handler) { return this.listeners[eventName] = handler; }; LiveReload.prototype.log = function(message) { return this.console.log("" + message); }; LiveReload.prototype.performReload = function(message) { var _ref, _ref1; this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2))); return this.reloader.reload(message.path, { liveCSS: (_ref = message.liveCSS) != null ? _ref : true, liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true, originalPath: message.originalPath || '', overrideURL: message.overrideURL || '', serverURL: "http://" + this.options.host + ":" + this.options.port }); }; LiveReload.prototype.performAlert = function(message) { return alert(message.message); }; LiveReload.prototype.shutDown = function() { var _base; this.connector.disconnect(); this.log("LiveReload disconnected."); return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; }; LiveReload.prototype.hasPlugin = function(identifier) { return !!this.pluginIdentifiers[identifier]; }; LiveReload.prototype.addPlugin = function(pluginClass) { var plugin; if (this.hasPlugin(pluginClass.identifier)) { return; } this.pluginIdentifiers[pluginClass.identifier] = true; plugin = new pluginClass(this.window, { _livereload: this, _reloader: this.reloader, _connector: this.connector, console: this.console, Timer: Timer, generateCacheBustUrl: (function(_this) { return function(url) { return _this.reloader.generateCacheBustUrl(url); }; })(this) }); this.plugins.push(plugin); this.reloader.addPlugin(plugin); }; LiveReload.prototype.analyze = function() { var plugin, pluginData, pluginsData, _i, _len, _ref; if (!(this.connector.protocol >= 7)) { return; } pluginsData = {}; _ref = this.plugins; for (_i = 0, _len = _ref.length; _i < _len; _i++) { plugin = _ref[_i]; pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; pluginData.version = plugin.constructor.version; } this.connector.sendCommand({ command: 'info', plugins: pluginsData, url: this.window.location.href }); }; return LiveReload; })(); }).call(this); },{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){ (function() { var Options; exports.Options = Options = (function() { function Options() { this.host = null; this.port = 35729; this.snipver = null; this.ext = null; this.extver = null; this.mindelay = 1000; this.maxdelay = 60000; this.handshake_timeout = 5000; } Options.prototype.set = function(name, value) { if (typeof value === 'undefined') { return; } if (!isNaN(+value)) { value = +value; } return this[name] = value; }; return Options; })(); Options.extract = function(document) { var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1; _ref = document.getElementsByTagName('script'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { element = _ref[_i]; if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { options = new Options(); if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { options.host = mm[1]; if (mm[2]) { options.port = parseInt(mm[2], 10); } } if (m[2]) { _ref1 = m[2].split('&'); for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { pair = _ref1[_j]; if ((keyAndValue = pair.split('=')).length > 1) { options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); } } } return options; } } return null; }; }).call(this); },{}],6:[function(require,module,exports){ (function() { var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; exports.ProtocolError = ProtocolError = (function() { function ProtocolError(reason, data) { this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; } return ProtocolError; })(); exports.Parser = Parser = (function() { function Parser(handlers) { this.handlers = handlers; this.reset(); } Parser.prototype.reset = function() { return this.protocol = null; }; Parser.prototype.process = function(data) { var command, e, message, options, _ref; try { if (this.protocol == null) { if (data.match(/^!!ver:([\d.]+)$/)) { this.protocol = 6; } else if (message = this._parseMessage(data, ['hello'])) { if (!message.protocols.length) { throw new ProtocolError("no protocols specified in handshake message"); } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { this.protocol = 7; } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { this.protocol = 6; } else { throw new ProtocolError("no supported protocols found"); } } return this.handlers.connected(this.protocol); } else if (this.protocol === 6) { message = JSON.parse(data); if (!message.length) { throw new ProtocolError("protocol 6 messages must be arrays"); } command = message[0], options = message[1]; if (command !== 'refresh') { throw new ProtocolError("unknown protocol 6 command"); } return this.handlers.message({ command: 'reload', path: options.path, liveCSS: (_ref = options.apply_css_live) != null ? _ref : true }); } else { message = this._parseMessage(data, ['reload', 'alert']); return this.handlers.message(message); } } catch (_error) { e = _error; if (e instanceof ProtocolError) { return this.handlers.error(e); } else { throw e; } } }; Parser.prototype._parseMessage = function(data, validCommands) { var e, message, _ref; try { message = JSON.parse(data); } catch (_error) { e = _error; throw new ProtocolError('unparsable JSON', data); } if (!message.command) { throw new ProtocolError('missing "command" key', data); } if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); } return message; }; return Parser; })(); }).call(this); },{}],7:[function(require,module,exports){ (function() { var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; splitUrl = function(url) { var hash, index, params; if ((index = url.indexOf('#')) >= 0) { hash = url.slice(index); url = url.slice(0, index); } else { hash = ''; } if ((index = url.indexOf('?')) >= 0) { params = url.slice(index); url = url.slice(0, index); } else { params = ''; } return { url: url, params: params, hash: hash }; }; pathFromUrl = function(url) { var path; url = splitUrl(url).url; if (url.indexOf('file://') === 0) { path = url.replace(/^file:\/\/(localhost)?/, ''); } else { path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); } return decodeURIComponent(path); }; pickBestMatch = function(path, objects, pathFunc) { var bestMatch, object, score, _i, _len; bestMatch = { score: 0 }; for (_i = 0, _len = objects.length; _i < _len; _i++) { object = objects[_i]; score = numberOfMatchingSegments(path, pathFunc(object)); if (score > bestMatch.score) { bestMatch = { object: object, score: score }; } } if (bestMatch.score > 0) { return bestMatch; } else { return null; } }; numberOfMatchingSegments = function(path1, path2) { var comps1, comps2, eqCount, len; path1 = path1.replace(/^\/+/, '').toLowerCase(); path2 = path2.replace(/^\/+/, '').toLowerCase(); if (path1 === path2) { return 10000; } comps1 = path1.split('/').reverse(); comps2 = path2.split('/').reverse(); len = Math.min(comps1.length, comps2.length); eqCount = 0; while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { ++eqCount; } return eqCount; }; pathsMatch = function(path1, path2) { return numberOfMatchingSegments(path1, path2) > 0; }; IMAGE_STYLES = [ { selector: 'background', styleNames: ['backgroundImage'] }, { selector: 'border', styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] } ]; exports.Reloader = Reloader = (function() { function Reloader(window, console, Timer) { this.window = window; this.console = console; this.Timer = Timer; this.document = this.window.document; this.importCacheWaitPeriod = 200; this.plugins = []; } Reloader.prototype.addPlugin = function(plugin) { return this.plugins.push(plugin); }; Reloader.prototype.analyze = function(callback) { return results; }; Reloader.prototype.reload = function(path, options) { var plugin, _base, _i, _len, _ref; this.options = options; if ((_base = this.options).stylesheetReloadTimeout == null) { _base.stylesheetReloadTimeout = 15000; } _ref = this.plugins; for (_i = 0, _len = _ref.length; _i < _len; _i++) { plugin = _ref[_i]; if (plugin.reload && plugin.reload(path, options)) { return; } } if (options.liveCSS) { if (path.match(/\.css$/i)) { if (this.reloadStylesheet(path)) { return; } } } if (options.liveImg) { if (path.match(/\.(jpe?g|png|gif)$/i)) { this.reloadImages(path); return; } } return this.reloadPage(); }; Reloader.prototype.reloadPage = function() { return this.window.document.location.reload(); }; Reloader.prototype.reloadImages = function(path) { var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; expando = this.generateUniqueString(); _ref = this.document.images; for (_i = 0, _len = _ref.length; _i < _len; _i++) { img = _ref[_i]; if (pathsMatch(path, pathFromUrl(img.src))) { img.src = this.generateCacheBustUrl(img.src, expando); } } if (this.document.querySelectorAll) { for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { img = _ref2[_k]; this.reloadStyleImages(img.style, styleNames, path, expando); } } } if (this.document.styleSheets) { _ref3 = this.document.styleSheets; _results = []; for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { styleSheet = _ref3[_l]; _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); } return _results; } }; Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { var e, rule, rules, styleNames, _i, _j, _len, _len1; try { rules = styleSheet != null ? styleSheet.cssRules : void 0; } catch (_error) { e = _error; } if (!rules) { return; } for (_i = 0, _len = rules.length; _i < _len; _i++) { rule = rules[_i]; switch (rule.type) { case CSSRule.IMPORT_RULE: this.reloadStylesheetImages(rule.styleSheet, path, expando); break; case CSSRule.STYLE_RULE: for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { styleNames = IMAGE_STYLES[_j].styleNames; this.reloadStyleImages(rule.style, styleNames, path, expando); } break; case CSSRule.MEDIA_RULE: this.reloadStylesheetImages(rule, path, expando); } } }; Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { var newValue, styleName, value, _i, _len; for (_i = 0, _len = styleNames.length; _i < _len; _i++) { styleName = styleNames[_i]; value = style[styleName]; if (typeof value === 'string') { newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) { return function(match, src) { if (pathsMatch(path, pathFromUrl(src))) { return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; } else { return match; } }; })(this)); if (newValue !== value) { style[styleName] = newValue; } } } }; Reloader.prototype.reloadStylesheet = function(path) { var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1; links = (function() { var _i, _len, _ref, _results; _ref = this.document.getElementsByTagName('link'); _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { link = _ref[_i]; if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) { _results.push(link); } } return _results; }).call(this); imported = []; _ref = this.document.getElementsByTagName('style'); for (_i = 0, _len = _ref.length; _i < _len; _i++) { style = _ref[_i]; if (style.sheet) { this.collectImportedStylesheets(style, style.sheet, imported); } } for (_j = 0, _len1 = links.length; _j < _len1; _j++) { link = links[_j]; this.collectImportedStylesheets(link, link.sheet, imported); } if (this.window.StyleFix && this.document.querySelectorAll) { _ref1 = this.document.querySelectorAll('style[data-href]'); for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { style = _ref1[_k]; links.push(style); } } this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); match = pickBestMatch(path, links.concat(imported), (function(_this) { return function(l) { return pathFromUrl(_this.linkHref(l)); }; })(this)); if (match) { if (match.object.rule) { this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); this.reattachImportedRule(match.object); } else { this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); this.reattachStylesheetLink(match.object); } } else { this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one"); for (_l = 0, _len3 = links.length; _l < _len3; _l++) { link = links[_l]; this.reattachStylesheetLink(link); } } return true; }; Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { var e, index, rule, rules, _i, _len; try { rules = styleSheet != null ? styleSheet.cssRules : void 0; } catch (_error) { e = _error; } if (rules && rules.length) { for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { rule = rules[index]; switch (rule.type) { case CSSRule.CHARSET_RULE: continue; case CSSRule.IMPORT_RULE: result.push({ link: link, rule: rule, index: index, href: rule.href }); this.collectImportedStylesheets(link, rule.styleSheet, result); break; default: break; } } } }; Reloader.prototype.waitUntilCssLoads = function(clone, func) { var callbackExecuted, executeCallback, poll; callbackExecuted = false; executeCallback = (function(_this) { return function() { if (callbackExecuted) { return; } callbackExecuted = true; return func(); }; })(this); clone.onload = (function(_this) { return function() { _this.console.log("LiveReload: the new stylesheet has finished loading"); _this.knownToSupportCssOnLoad = true; return executeCallback(); }; })(this); if (!this.knownToSupportCssOnLoad) { (poll = (function(_this) { return function() { if (clone.sheet) { _this.console.log("LiveReload is polling until the new CSS finishes loading..."); return executeCallback(); } else { return _this.Timer.start(50, poll); } }; })(this))(); } return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); }; Reloader.prototype.linkHref = function(link) { return link.href || link.getAttribute('data-href'); }; Reloader.prototype.reattachStylesheetLink = function(link) { var clone, parent; if (link.__LiveReload_pendingRemoval) { return; } link.__LiveReload_pendingRemoval = true; if (link.tagName === 'STYLE') { clone = this.document.createElement('link'); clone.rel = 'stylesheet'; clone.media = link.media; clone.disabled = link.disabled; } else { clone = link.cloneNode(false); } clone.href = this.generateCacheBustUrl(this.linkHref(link)); parent = link.parentNode; if (parent.lastChild === link) { parent.appendChild(clone); } else { parent.insertBefore(clone, link.nextSibling); } return this.waitUntilCssLoads(clone, (function(_this) { return function() { var additionalWaitingTime; if (/AppleWebKit/.test(navigator.userAgent)) { additionalWaitingTime = 5; } else { additionalWaitingTime = 200; } return _this.Timer.start(additionalWaitingTime, function() { var _ref; if (!link.parentNode) { return; } link.parentNode.removeChild(link); clone.onreadystatechange = null; return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; }); }; })(this)); }; Reloader.prototype.reattachImportedRule = function(_arg) { var href, index, link, media, newRule, parent, rule, tempLink; rule = _arg.rule, index = _arg.index, link = _arg.link; parent = rule.parentStyleSheet; href = this.generateCacheBustUrl(rule.href); media = rule.media.length ? [].join.call(rule.media, ', ') : ''; newRule = "@import url(\"" + href + "\") " + media + ";"; rule.__LiveReload_newHref = href; tempLink = this.document.createElement("link"); tempLink.rel = 'stylesheet'; tempLink.href = href; tempLink.__LiveReload_pendingRemoval = true; if (link.parentNode) { link.parentNode.insertBefore(tempLink, link); } return this.Timer.start(this.importCacheWaitPeriod, (function(_this) { return function() { if (tempLink.parentNode) { tempLink.parentNode.removeChild(tempLink); } if (rule.__LiveReload_newHref !== href) { return; } parent.insertRule(newRule, index); parent.deleteRule(index + 1); rule = parent.cssRules[index]; rule.__LiveReload_newHref = href; return _this.Timer.start(_this.importCacheWaitPeriod, function() { if (rule.__LiveReload_newHref !== href) { return; } parent.insertRule(newRule, index); return parent.deleteRule(index + 1); }); }; })(this)); }; Reloader.prototype.generateUniqueString = function() { return 'livereload=' + Date.now(); }; Reloader.prototype.generateCacheBustUrl = function(url, expando) { var hash, oldParams, originalUrl, params, _ref; if (expando == null) { expando = this.generateUniqueString(); } _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; if (this.options.overrideURL) { if (url.indexOf(this.options.serverURL) < 0) { originalUrl = url; url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url); } } params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { return "" + sep + expando; }); if (params === oldParams) { if (oldParams.length === 0) { params = "?" + expando; } else { params = "" + oldParams + "&" + expando; } } return url + params + hash; }; return Reloader; })(); }).call(this); },{}],8:[function(require,module,exports){ (function() { var CustomEvents, LiveReload, k; CustomEvents = require('./customevents'); LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window); for (k in window) { if (k.match(/^LiveReloadPlugin/)) { LiveReload.addPlugin(window[k]); } } LiveReload.addPlugin(require('./less')); LiveReload.on('shutdown', function() { return delete window.LiveReload; }); LiveReload.on('connect', function() { return CustomEvents.fire(document, 'LiveReloadConnect'); }); LiveReload.on('disconnect', function() { return CustomEvents.fire(document, 'LiveReloadDisconnect'); }); CustomEvents.bind(document, 'LiveReloadShutDown', function() { return LiveReload.shutDown(); }); }).call(this); },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){ (function() { var Timer; exports.Timer = Timer = (function() { function Timer(func) { this.func = func; this.running = false; this.id = null; this._handler = (function(_this) { return function() { _this.running = false; _this.id = null; return _this.func(); }; })(this); } Timer.prototype.start = function(timeout) { if (this.running) { clearTimeout(this.id); } this.id = setTimeout(this._handler, timeout); return this.running = true; }; Timer.prototype.stop = function() { if (this.running) { clearTimeout(this.id); this.running = false; return this.id = null; } }; return Timer; })(); Timer.start = function(timeout, func) { return setTimeout(func, timeout); }; }).call(this); },{}]},{},[8]); python-livereload-2.7.1/livereload/watcher.py000066400000000000000000000162141473055031700213340ustar00rootroot00000000000000""" livereload.watcher ~~~~~~~~~~~~~~~~~~ A file watch management for LiveReload Server. :copyright: (c) 2013 - 2015 by Hsiaoming Yang :license: BSD, see LICENSE for more details. """ import glob import logging import os import time from inspect import signature try: import pyinotify except ImportError: pyinotify = None logger = logging.getLogger('livereload') class Watcher: """A file watcher registry.""" def __init__(self): self._tasks = {} # modification time of filepaths for each task, # before and after checking for changes self._task_mtimes = {} self._new_mtimes = {} # setting changes self._changes = [] # filepath that is changed self.filepath = None self._start = time.time() # list of ignored dirs self.ignored_dirs = ['.git', '.hg', '.svn', '.cvs'] def ignore_dirs(self, *args): self.ignored_dirs.extend(args) def remove_dirs_from_ignore(self, *args): for a in args: self.ignored_dirs.remove(a) def ignore(self, filename): """Ignore a given filename or not.""" _, ext = os.path.splitext(filename) return ext in ['.pyc', '.pyo', '.o', '.swp'] def watch(self, path, func=None, delay=0, ignore=None): """Add a task to watcher. :param path: a filepath or directory path or glob pattern :param func: the function to be executed when file changed :param delay: Delay sending the reload message. Use 'forever' to not send it. This is useful to compile sass files to css, but reload on changed css files then only. :param ignore: A function return True to ignore a certain pattern of filepath. """ self._tasks[path] = { 'func': func, 'delay': delay, 'ignore': ignore, 'mtimes': {}, } def start(self, callback): """Start the watcher running, calling callback when changes are observed. If this returns False, regular polling will be used.""" return False def examine(self): """Check if there are changes. If so, run the given task. Returns a tuple of modified filepath and reload delay. """ if self._changes: return self._changes.pop() # clean filepath self.filepath = None delays = set() for path in self._tasks: item = self._tasks[path] self._task_mtimes = item['mtimes'] changed = self.is_changed(path, item['ignore']) if changed: func = item['func'] delay = item['delay'] if delay and isinstance(delay, float): delays.add(delay) if func: name = getattr(func, 'name', None) if not name: name = getattr(func, '__name__', 'anonymous') logger.info( f"Running task: {name} (delay: {delay})") if len(signature(func).parameters) > 0 and isinstance(changed, list): func(changed) else: func() if delays: delay = max(delays) else: delay = None return self.filepath, delay def is_changed(self, path, ignore=None): """Check if any filepaths have been added, modified, or removed. Updates filepath modification times in self._task_mtimes. """ self._new_mtimes = {} changed = False if os.path.isfile(path): changed = self.is_file_changed(path, ignore) elif os.path.isdir(path): changed = self.is_folder_changed(path, ignore) else: changed = self.get_changed_glob_files(path, ignore) if not changed: changed = self.is_file_removed() self._task_mtimes.update(self._new_mtimes) return changed def is_file_removed(self): """Check if any filepaths have been removed since last check. Deletes removed paths from self._task_mtimes. Sets self.filepath to one of the removed paths. """ removed_paths = set(self._task_mtimes) - set(self._new_mtimes) if not removed_paths: return False for path in removed_paths: self._task_mtimes.pop(path) # self.filepath seems purely informational, so setting one # of several removed files seems sufficient self.filepath = path return True def is_file_changed(self, path, ignore=None): """Check if filepath has been added or modified since last check. Updates filepath modification times in self._new_mtimes. Sets self.filepath to changed path. """ if not os.path.isfile(path): return False if self.ignore(path): return False if ignore and ignore(path): return False mtime = os.path.getmtime(path) if path not in self._task_mtimes: self._new_mtimes[path] = mtime self.filepath = path return mtime > self._start if self._task_mtimes[path] != mtime: self._new_mtimes[path] = mtime self.filepath = path return True self._new_mtimes[path] = mtime return False def is_folder_changed(self, path, ignore=None): """Check if directory path has any changed filepaths.""" for root, dirs, files in os.walk(path, followlinks=True): for d in self.ignored_dirs: if d in dirs: dirs.remove(d) for f in files: if self.is_file_changed(os.path.join(root, f), ignore): return True return False def get_changed_glob_files(self, path, ignore=None): """Check if glob path has any changed filepaths.""" files = glob.glob(path, recursive=True) changed_files = [f for f in files if self.is_file_changed(f, ignore)] return changed_files class INotifyWatcher(Watcher): def __init__(self): Watcher.__init__(self) self.wm = pyinotify.WatchManager() self.notifier = None self.callback = None def watch(self, path, func=None, delay=None, ignore=None): flag = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY self.wm.add_watch(path, flag, rec=True, do_glob=True, auto_add=True) Watcher.watch(self, path, func, delay, ignore) def inotify_event(self, event): self.callback() def start(self, callback): if not self.notifier: self.callback = callback from tornado import ioloop self.notifier = pyinotify.TornadoAsyncNotifier( self.wm, ioloop.IOLoop.instance(), default_proc_fun=self.inotify_event ) callback() return True def get_watcher_class(): if pyinotify is None or not hasattr(pyinotify, 'TornadoAsyncNotifier'): return Watcher return INotifyWatcher python-livereload-2.7.1/server.py000066400000000000000000000002151473055031700170510ustar00rootroot00000000000000from livereload import Server, shell server = Server() server.watch('docs/*.rst', shell('make html')) server.serve(root='docs/_build/html') python-livereload-2.7.1/setup.cfg000066400000000000000000000000431473055031700170110ustar00rootroot00000000000000[metadata] license_files = LICENSE python-livereload-2.7.1/setup.py000066400000000000000000000037441473055031700167150ustar00rootroot00000000000000#!/usr/bin/env python import re from setuptools import setup def fread(filepath): with open(filepath) as f: return f.read() def version(): content = fread('livereload/__init__.py') pattern = r"__version__ = '([0-9\.dev]*)'" m = re.findall(pattern, content) return m[0] setup( name='livereload', version=version(), author='Hsiaoming Yang', author_email='me@lepture.com', url='https://github.com/lepture/python-livereload', packages=['livereload', 'livereload.management.commands'], description='Python LiveReload is an awesome tool for web developers', long_description_content_type='text/x-rst', long_description=fread('README.md'), entry_points={ 'console_scripts': [ 'livereload = livereload.cli:main', ] }, install_requires=[ 'tornado', ], license='BSD', include_package_data=True, python_requires='>=3.7', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Environment :: Web Environment :: Mozilla', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Build Tools', 'Topic :: Software Development :: Compilers', 'Topic :: Software Development :: Debuggers', ] ) python-livereload-2.7.1/tests/000077500000000000000000000000001473055031700163355ustar00rootroot00000000000000python-livereload-2.7.1/tests/__init__.py000066400000000000000000000000001473055031700204340ustar00rootroot00000000000000python-livereload-2.7.1/tests/test_watcher.py000066400000000000000000000106301473055031700214030ustar00rootroot00000000000000#!/usr/bin/python import os import time import shutil import unittest from livereload.watcher import get_watcher_class Watcher = get_watcher_class() tmpdir = os.path.join(os.path.dirname(__file__), 'tmp') class TestWatcher(unittest.TestCase): def setUp(self): if os.path.isdir(tmpdir): shutil.rmtree(tmpdir) os.mkdir(tmpdir) def tearDown(self): shutil.rmtree(tmpdir) def test_watch_dir(self): os.mkdir(os.path.join(tmpdir, '.git')) os.mkdir(os.path.join(tmpdir, '.hg')) os.mkdir(os.path.join(tmpdir, '.svn')) os.mkdir(os.path.join(tmpdir, '.cvs')) watcher = Watcher() watcher.watch(tmpdir) assert watcher.is_changed(tmpdir) is False # sleep 1 second so that mtime will be different # TODO: This doesn't seem necessary; test passes without it time.sleep(1) filepath = os.path.join(tmpdir, 'foo') with open(filepath, 'w') as f: f.write('') assert watcher.is_changed(tmpdir) assert watcher.is_changed(tmpdir) is False os.remove(filepath) assert watcher.is_changed(tmpdir) assert watcher.is_changed(tmpdir) is False def test_watch_file(self): watcher = Watcher() watcher.count = 0 # sleep 1 second so that mtime will be different # TODO: This doesn't seem necessary; test passes without it time.sleep(1) filepath = os.path.join(tmpdir, 'foo') with open(filepath, 'w') as f: f.write('') def add_count(): watcher.count += 1 watcher.watch(filepath, add_count) assert watcher.is_changed(filepath) assert watcher.is_changed(filepath) is False # sleep 1 second so that mtime will be different # TODO: This doesn't seem necessary; test passes without it time.sleep(1) with open(filepath, 'w') as f: f.write('') abs_filepath = os.path.abspath(filepath) assert watcher.examine() == (abs_filepath, None) assert watcher.examine() == (None, None) assert watcher.count == 1 os.remove(filepath) assert watcher.examine() == (abs_filepath, None) assert watcher.examine() == (None, None) assert watcher.count == 2 def test_watch_glob(self): watcher = Watcher() watcher.watch(tmpdir + '/*') assert watcher.examine() == (None, None) with open(os.path.join(tmpdir, 'foo.pyc'), 'w') as f: f.write('') assert watcher.examine() == (None, None) filepath = os.path.join(tmpdir, 'foo') with open(filepath, 'w') as f: f.write('') abs_filepath = os.path.abspath(filepath) assert watcher.examine() == (abs_filepath, None) assert watcher.examine() == (None, None) os.remove(filepath) assert watcher.examine() == (abs_filepath, None) assert watcher.examine() == (None, None) def test_watch_ignore(self): watcher = Watcher() watcher.watch(tmpdir + '/*', ignore=lambda o: o.endswith('.ignore')) assert watcher.examine() == (None, None) with open(os.path.join(tmpdir, 'foo.ignore'), 'w') as f: f.write('') assert watcher.examine() == (None, None) def test_watch_multiple_dirs(self): first_dir = os.path.join(tmpdir, 'first') second_dir = os.path.join(tmpdir, 'second') watcher = Watcher() os.mkdir(first_dir) watcher.watch(first_dir) assert watcher.examine() == (None, None) first_path = os.path.join(first_dir, 'foo') with open(first_path, 'w') as f: f.write('') assert watcher.examine() == (first_path, None) assert watcher.examine() == (None, None) os.mkdir(second_dir) watcher.watch(second_dir) assert watcher.examine() == (None, None) second_path = os.path.join(second_dir, 'bar') with open(second_path, 'w') as f: f.write('') assert watcher.examine() == (second_path, None) assert watcher.examine() == (None, None) with open(first_path, 'a') as f: f.write('foo') assert watcher.examine() == (first_path, None) assert watcher.examine() == (None, None) os.remove(second_path) assert watcher.examine() == (second_path, None) assert watcher.examine() == (None, None)