Codebase list python-livereload / 001f99e
Imported Upstream version 2.1.0 Agustin Henze 10 years ago
25 changed file(s) with 725 addition(s) and 1244 deletion(s). Raw diff Collapse all Expand all
0 #################
1 # Common ignore #
2 #################
3 build
4 # Test DB
5 *.sqlite
6 # Test
0 .DS_Store
1
72 test.*
8 .hg
9 .svn
10 CVS
11 *~.nib
123 *.swp
134 *~
14
15
16 #################
17 # python ignore #
18 #################
195 *.py[co]
206
21 # Packages
227 *.egg
238 *.egg-info
249 dist
2510 eggs
26 parts
27 bin
28 var
2911 sdist
3012 develop-eggs
3113 .installed.cfg
3214
33 # Installer logs
15 build
16
3417 pip-log.txt
3518
36 # Unit test / coverage reports
3719 .coverage
3820 .tox
3921
40 # Translations
41 *.mo
42
43 # Mr Developer
44 .mr.developer.cfg
45
46 # sphinx
4722 docs/_build
48
49 ##############
50 # Mac ignore #
51 ##############
52 .DS_Store
53 *.mode1
54 *.mode1v3
55 *.mode2v3
56 *.perspective
57 *.perspectivev3
58 *.pbxuser
59 xcuserdata
60 *.[oa]
61 *(Autosaved).rtfd/
62 Backup[ ]of[ ]*.pages/
63 Backup[ ]of[ ]*.key/
64 Backup[ ]of[ ]*.numbers/
23 example/style.css
24 tests/tmp
25 cover/
0 Copyright (c) 2012, Hsiaoming Yang <http://lepture.com>
0 Copyright (c) 2012-2013, Hsiaoming Yang <me@lepture.com>
11
22 Redistribution and use in source and binary forms, with or without
33 modification, are permitted provided that the following conditions
0 .PHONY: clean-pyc clean-build docs
0 .PHONY: clean-pyc clean-build docs test coverage
11
22 clean: clean-build clean-pyc
33
1818
1919 docs:
2020 $(MAKE) -C docs html
21
22 test:
23 @nosetests -s
24
25 coverage:
26 @rm -f .coverage
27 @nosetests --with-coverage --cover-package=livereload --cover-html
0 Python LiveReload
1 =================
0 LiveReload
1 ==========
22
3 `LiveReload <http://livereload.com/>`_ Server in Python Version.
4
5 Web Developers need to refresh a browser everytime when he saved a file (css,
6 javascript, html), it is really boring. LiveReload will take care of that for
7 you. When you saved a file, your browser will refresh itself. And what's more,
8 it can do some tasks like compiling less to css before the browser refreshing.
3 This is a brand new LiveReload in version 2.0.0.
94
105 Installation
116 ------------
127
138 Python LiveReload is designed for web developers who know Python.
14
15 Install python-livereload
16 ~~~~~~~~~~~~~~~~~~~~~~~~~
179
1810 Install Python LiveReload with pip::
1911
2416 $ easy_install livereload
2517
2618
27 Install Browser Extensions
28 ~~~~~~~~~~~~~~~~~~~~~~~~~~
19 Developer Guide
20 ---------------
2921
30 A browser extension is not required, you can insert a script into your
31 html page manually::
22 The new livereload server is designed for developers. It can power a
23 wsgi application now::
3224
33 <script type="text/javascript" src="http://127.0.0.1:35729/livereload.js"></script>
25 from livereload import Server, shell
3426
35 But a browser extension will make your life easier, available extensions:
27 server = Server(wsgi_app)
3628
37 + Chrome Extension
38 + Safari Extension
39 + Firefox Extension
29 # run a shell command
30 server.watch('static/*.stylus', 'make static')
4031
41 Visit: http://help.livereload.com/kb/general-use/browser-extensions
32 # run a function
33 def alert():
34 print('foo')
35 server.watch('foo.txt', alert)
4236
43 Quickstart
44 ------------
37 # output stdout into a file
38 server.watch('style.less', shell('lessc style.less', output='style.css'))
4539
46 LiveReload is designed for more complex tasks, not just for refreshing a
47 browser. But you can still do the simple task.
40 server.serve()
4841
49 Assume you have livereload and its extension installed, and now you are in your
50 working directory. With command::
42 The ``Server`` class accepts parameters:
5143
52 $ livereload [-p port]
44 - app: a wsgi application
45 - watcher: a watcher instance, you don't have to create one
5346
54 your browser will reload, if any file in the working directory changed.
47 server.watch
48 ~~~~~~~~~~~~
5549
50 ``server.watch`` can watch a filepath, a directory and a glob pattern::
5651
57 LiveReload as SimpleHTTPServer
58 -------------------------------
52 server.watch('path/to/file.txt')
53 server.watch('directory/path/')
54 server.watch('glob/*.pattern')
5955
60 Livereload server can be a SimpleHTTPServer::
56 You can also use other library (for example: formic) for more powerful
57 file adding::
6158
62 $ livereload -p 8000
63
64 It will set up a server at port 8000, take a look at http://127.0.0.1:8000.
65 Oh, it can livereload!
66
67 **IF YOU ARE NOT USING IT AS A HTTP SERVER, DO NOT ADD THE PORT OPTION**.
68
69 Guardfile
70 ----------
71
72 More complex tasks can be done by Guardfile. Write a Guardfile in your working
73 directory, the basic syntax::
74
75 #!/usr/bin/env python
76 from livereload.task import Task
77
78 Task.add('static/style.css')
79 Task.add('*.html')
80
81 Now livereload will only guard static/style.css and html in your workding
82 directory.
83
84 But python-livereload is more than that, you can specify a task before
85 refreshing the browser::
86
87 #!/usr/bin/env python
88 from livereload.task import Task
89 from livereload.compiler import lessc
90
91 Task.add('style.less', lessc('style.less', 'style.css'))
92
93 And it will compile less css before refreshing the browser now.
94
95
96 Linux
97 ----------
98
99 If you're using python-livereload under Linux, you should also install pyinotify,
100 as it will greatly improve responsiveness and reduce CPU load.
101
102 You may see errors such as::
103
104 [2013-06-19 11:11:07,499 pyinotify ERROR] add_watch: cannot watch somefile WD=-1, Errno=No space left on device (ENOSPC)
105
106 If so, you need to increase the number of "user watches". You can either do this temporarily by running (as root)::
107
108 echo 51200 > /proc/sys/fs/inotify/max_user_watches
109
110 To make this change permanent, add the following line to /etc/sysctl.conf and reboot::
111
112 fs.inotify.max_user_watches = 51200
113
114
115 Others
116 --------
117
118 If you are on a Mac, you can buy `LiveReload2 <http://livereload.com/>`_.
119
120 If you are a rubist, you can get guard-livereload.
59 for filepath in formic.FileSet(include="**.css"):
60 server.watch(filepath, 'make css')
11 =========
22
33 The full list of changes between each Python LiveReload release.
4
5 Version 2.1.0
6 -------------
7
8 Add ForceReloadHandler.
9
10 Version 2.0.0
11 -------------
12
13 A new designed livereload server which has the power to serve a wsgi
14 application.
415
516 Version 1.0.1
617 -------------
+0
-84
docs/compiler.rst less more
0 .. _compiler:
1
2
3 Compiler
4 =========
5
6 In web development, compiling (compressing) is a common task, Python LiveReload
7 has provided some compilers for you.
8
9
10 Overview
11 ----------
12
13 In :ref:`quickstart` and :ref:`guardfile` you already know ``lessc``. It is simple.
14 But ``lessc`` just write code to a file, sometimes you don't want to write
15 code, you want to append code. In this case, you should know the basic of a
16 `Compiler`.
17
18 ``CommandCompiler`` takes source path for constructor,
19 and has ``init_command()`` method to setup a executable.
20
21 ::
22
23 from livereload.compiler import CommandCompiler
24
25 c = CommandCompiler('style.less')
26 c.init_command('lessc --compress')
27 c.write('site.css') #: write compiled code to 'site.css'
28 c.append('global.css') #: append compiled code to 'global.css'
29
30
31 Quick Alias
32 ------------
33
34 In most cases, you don't need to write every `Compiler`, you can use a simple
35 and easy alias. The available:
36
37 + lessc
38 + uglifyjs
39 + slimmer
40 + coffee
41 + shell
42
43 These aliases accept ``mode`` parameter to switch calling ``write()`` or ``append()``.
44 "``w``" leads ``write()``, while "``a``" leads ``append()``. And "``w``" is the default value.
45
46 Above example can be changed as followings::
47
48 from livereload.compiler import lessc
49
50 lessc('style.less', 'site.css')
51 lessc('style.less', 'global.css', mode='a')
52
53 Get static files from internet
54 -------------------------------
55
56 New in :ref:`ver0.3`.
57
58 With this new feature, you can keep the source of your project clean.
59 If the path starts with "``http://``" or "``https://``", download it automatically. ::
60
61 from livereload.compiler import uglifyjs
62
63 uglifyjs('http://code.jquery.com/jquery.js', 'static/lib.js')
64
65
66 Invoke command line task
67 ------------------------
68
69 Using ``shell``, you can invoke any command line tasks such as *Sphinx*
70 html documentation::
71
72 from livereload.task import Task
73 from livereload.compiler import shell
74
75 Task.add('*.rst', shell('make html'))
76
77
78 Contribute
79 -----------
80
81 Want more compiler?
82
83 Fork GitHub Repo and send pull request to me.
9292
9393 # The theme to use for HTML and HTML Help pages. See the documentation for
9494 # a list of builtin themes.
95 html_theme = 'flask'
95 html_theme = 'flask_small'
9696
9797 # Theme options are theme-specific and customize the look and feel of a theme
9898 # further. For a list of options available for each theme, see the
+0
-104
docs/guardfile.rst less more
0 .. _guardfile:
1
2 Guardfile
3 =========
4
5 :file:`Guardfile` is an executable python file which defines the tasks Python LiveReload
6 should guard.
7
8 Writing Guardfile is simple (or not).
9
10 The basic syntax::
11
12 #!/usr/bin/env python
13 from livereload.task import Task
14
15 Task.add('static/style.css')
16
17 Which means our Server should guard ``static/style.css`` , when this (and only this)
18 file is saved, the server will send a signal to the client side, and the browser
19 will refresh itself.
20
21
22 Add a task
23 -----------
24
25 In Guardfile, the most important thing is adding a task::
26
27 Task.add(...)
28
29 ``Task.add`` accepts two parameters:
30
31 1. the first one is the path you want to guard
32 2. the second one is optional, it should be a callable function
33
34
35 Define a path
36 --------------
37
38 Path is the first parameter of a Task, a path can be absolute or relative:
39
40 1. a filepath: ``static/style.css``
41 2. a directory path: ``static``
42 3. a glob pattern: ``static/*.css``
43
44
45 Define a function
46 -------------------
47
48 Function is the second parameter of a Task, it is not required.
49 When files in the given path changed, the related function will execute.
50
51 A good example in :ref:`quickstart`::
52
53 #!/usr/bin/env python
54 from livereload.task import Task
55 from livereload.compiler import lessc
56
57 Task.add('style.less', lessc('style.less', 'style.css'))
58
59 This means when ``style.less`` is saved, the server will execute::
60
61 lessc('style.less', 'style.css')()
62
63 Please note that ``lessc`` here will create a function. You can't do::
64
65 #!/usr/bin/env python
66 from livereload.task import Task
67
68 def say(word):
69 print(word)
70
71 Task.add('style.less', say('hello'))
72
73 Because ``say('hello')`` is not a callable function, it is executed already.
74 But you can easily create a function by::
75
76 #!/usr/bin/env python
77 from livereload.task import Task
78 import functools
79
80 @functools.partial
81 def say(word):
82 print(word)
83
84 Task.add('style.less', say('hello'))
85
86 And there is one more thing you should know. When the function is called,
87 it losts its context already, which means you should never import a module
88 outside of the task function::
89
90 #: don't
91 import A
92
93 def task1():
94 return A.do_some_thing()
95
96 #: do
97 def task2():
98 import B
99 return B.do_some_thing()
100
101
102 Python LiveReload provides some common tasks for web developers,
103 check :ref:`compiler` .
0 Welcome to Python LiveReload
1 =============================
0 .. include:: ../README.rst
21
3 `LiveReload <http://livereload.com/>`_ contains two parts,
4 the client side and the server side.
5 And Python LiveReload is the server side in python version.
2 API
3 ---
64
7 Web Developers need to refresh a browser everytime when he saves a file (css,
8 javascript, html). It is really boring. LiveReload will take care of that for
9 you. When you save a file, your browser will refresh itself.
5 .. module:: livereload
106
11 And what's more, it can do some tasks like
12 **compiling less to css before the browser refreshing**.
7 .. autoclass:: Server
8 :members:
139
14 **Bug Report** https://github.com/lepture/python-livereload/issues
10 .. autofunction:: shell
1511
16 User's Guide
17 -------------
12 Changelog
13 ---------
1814
19 Python LiveReload is designed for **Web Developers who know Python**. It
20 assumes that you want to do some complex tasks that LiveReload2.app can't do.
21
22 If you are not, you should buy LiveReload2.app instead.
15 The full list of changes between each Python LiveReload release.
2316
2417 .. toctree::
25 :maxdepth: 2
18 :maxdepth: 2
2619
27 install
28 quickstart
29 guardfile
30 compiler
31 changelog
20 changelog
3221
3322
3423 Contact
35 ---------
24 -------
3625
3726 Have any trouble? Want to know more?
3827
4231
4332 .. _GitHub: https://github.com/lepture
4433 .. _Twitter: https://twitter.com/lepture
45 .. _Email: lepture@me.com
34 .. _Email: me@lepture.com
+0
-79
docs/install.rst less more
0 .. _installation:
1
2 Installation
3 =============
4
5 This section covers the installation of Python LiveReload and other
6 essentials to make LiveReload available.
7
8 LiveReload contains two parts, the client side and the server side.
9 Client means the browser, it listens to the server's signal, and refreshs
10 your browser when catching the proper signals.
11
12 Install Browser Extensions
13 ----------------------------
14
15 A browser extension is not required, you can insert a script into your
16 html page manually::
17
18 <script type="text/javascript" src="http://127.0.0.1:35729/livereload.js"></script>
19
20 But a browser extension will make your life easier, available extensions:
21
22 + Chrome Extension
23 + Safari Extension
24 + Firefox Extension
25
26 Visit: http://help.livereload.com/kb/general-use/browser-extensions
27
28
29 Distribute & Pip
30 -----------------
31
32 Installing Python LiveReload is simple with pip::
33
34 $ pip install livereload
35
36 If you don't have pip installed, try easy_install::
37
38 $ easy_install livereload
39
40
41 Enhancement
42 ------------
43
44 Python LiveReload is designed to do some complex tasks like compiling.
45 The package itself has provided some useful compilers for you. But
46 you need to install them first.
47
48 Get Lesscss
49 ~~~~~~~~~~~~
50
51 Lesscss_ is a dynamic stylesheet language that makes css more elegent.
52
53 Install less with npm::
54
55 $ npm install less -g
56
57 Get UglifyJS
58 ~~~~~~~~~~~~
59
60 UglifyJS_ is a popular JavaScript parser/compressor/beautifier.
61
62 Install UglifyJS with npm::
63
64 $ npm install uglify-js -g
65
66
67 Get slimmer
68 ~~~~~~~~~~~~
69
70 Slimmer is a python library that compressing css, JavaScript, and
71 html.
72
73 Install slimmer::
74
75 $ pip install slimmer
76
77 .. _Lesscss: http://lesscss.org
78 .. _UglifyJs: https://github.com/mishoo/UglifyJS
+0
-85
docs/quickstart.rst less more
0 .. _quickstart:
1
2 Quickstart
3 ==========
4
5 This section assumes that you have everything installed. If you do not,
6 head over to the :ref:`installation` section.
7
8
9 Simple Task
10 ------------
11
12 LiveReload is designed for more complex tasks, not just for refreshing a
13 browser. But you can still do the simple task.
14
15 Assume you have livereload and its extension installed, and now you are in your
16 working directory. With command::
17
18 $ livereload
19
20 your browser will reload, if any file in the working directory changed.
21
22
23 Working with file protocal
24 ---------------------------
25
26 Enable file protocal on Chrome:
27
28 .. image:: http://i.imgur.com/qGpJI.png
29
30
31 Guardfile
32 ----------
33 More complex tasks can be done by Guardfile. Write a Guardfile in your working
34 directory, the basic syntax::
35
36 #!/usr/bin/env python
37 from livereload.task import Task
38
39 Task.add('static/style.css')
40 Task.add('*.html')
41
42 Now livereload will only guard static/style.css and html in your workding
43 directory.
44
45 But python-livereload is more than that, you can specify a task before
46 refreshing the browser::
47
48 #!/usr/bin/env python
49 from livereload.task import Task
50 from livereload.compiler import lessc
51
52 Task.add('style.less', lessc('style.less', 'style.css'))
53
54 And it will compile less css before refreshing the browser now.
55
56 Want to know about :ref:`guardfile` ?
57
58
59 Commands like Makefile
60 -----------------------
61
62 New in :ref:`ver0.3`
63
64 If you want to do some tasks in Guardfile manually::
65
66 # Guardfile
67
68 def task1():
69 print('task1')
70
71 def task2():
72 print('task2')
73
74 In terminal::
75
76 $ livereload task1 task2
77
78
79 Others
80 --------
81
82 If you are on a Mac, you can buy `LiveReload2 <http://livereload.com/>`_.
83
84 If you are a rubist, you can get guard-livereload.
+0
-8
example/Guardfile less more
0 #!/usr/bin/env python
1
2 from livereload.task import Task
3 from livereload.compiler import lessc
4
5
6 Task.add('style.less', lessc('style.less', 'style.css'))
7 Task.add('*.html')
0 #!/usr/bin/env python
1
2 from livereload import Server, shell
3
4 server = Server()
5 server.watch('style.less', shell('lessc style.less', output='style.css'))
6 server.serve()
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (c) 2012, Hsiaoming Yang <http://lepture.com>
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 # * Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # * Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials provided
14 # with the distribution.
15 # * Neither the name of the author nor the names of its contributors
16 # may be used to endorse or promote products derived from this
17 # software without specific prior written permission.
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 """
1 livereload
2 ~~~~~~~~~~
303
31 """
32 Python LiveReload
33 =================
4 A python version of livereload.
345
35 `LiveReload <http://livereload.com/>`_ Server in Python Version.
36
37 Web Developers need to refresh a browser everytime when he saved a file (css,
38 javascript, html), it is really boring. LiveReload will take care of that for
39 you. When you saved a file, your browser will refresh itself. And what's more,
40 it can do some tasks like compiling less to css before the browser refreshing.
41
42 Installation
43 ------------
44
45 Python LiveReload is designed for web developers who know Python.
46
47 Install python-livereload
48 ~~~~~~~~~~~~~~~~~~~~~~~~~
49
50 Install Python LiveReload with pip::
51
52 $ pip install livereload
53
54 If you don't have pip installed, try easy_install::
55
56 $ easy_install livereload
57
58
59 Install Browser Extensions
60 ~~~~~~~~~~~~~~~~~~~~~~~~~~
61
62 Get Browser Extensions From LiveReload.com
63
64 + Chrome Extension
65 + Safari Extension
66 + Firefox Extension
67
68 Visit: http://help.livereload.com/kb/general-use/browser-extensions
69
70 Get Notification
71 ~~~~~~~~~~~~~~~~~
72
73 If you are on Mac, and you are a Growl user::
74
75 $ pip install gntp
76
77 If you are on Ubuntu, you don't need to do anything. Notification just works.
78
79 Working with file protocal
80 ~~~~~~~~~~~~~~~~~~~~~~~~~~
81
82 Enable file protocal on Chrome:
83
84 .. image:: http://i.imgur.com/qGpJI.png
85
86
87 Quickstart
88 ------------
89
90 LiveReload is designed for more complex tasks, not just for refreshing a
91 browser. But you can still do the simple task.
92
93 Assume you have livereload and its extension installed, and now you are in your
94 working directory. With command::
95
96 $ livereload
97
98 your browser will reload, if any file in the working directory changed.
99
100
101 Guardfile
102 ----------
103 More complex tasks can be done by Guardfile. Write a Guardfile in your working
104 directory, the basic syntax::
105
106 #!/usr/bin/env python
107 from livereload.task import Task
108
109 Task.add('static/style.css')
110 Task.add('*.html')
111
112 Now livereload will only guard static/style.css and html in your workding
113 directory.
114
115 But python-livereload is more than that, you can specify a task before
116 refreshing the browser::
117
118 #!/usr/bin/env python
119 from livereload.task import Task
120 from livereload.compiler import lessc
121
122 Task.add('style.less', lessc('style.less', 'style.css'))
123
124 And it will compile less css before refreshing the browser now.
125
126
127 Others
128 --------
129
130 If you are on a Mac, you can buy `LiveReload2 <http://livereload.com/>`_.
131
132 If you are a rubist, you can get guard-livereload.
6 :copyright: (c) 2013 by Hsiaoming Yang
1337 """
1348
135 __version__ = '1.0.1'
9 __version__ = '2.1.0'
13610 __author__ = 'Hsiaoming Yang <me@lepture.com>'
137 __homepage__ = 'http://lab.lepture.com/livereload/'
11 __homepage__ = 'https://github.com/lepture/python-livereload'
12
13 from .server import Server, shell
14
15 __all__ = ('Server', 'shell')
0 # coding: utf-8
1 """
2 livereload._compat
3 ~~~~~~~~~~~~~~~~~~
4
5 Compatible module for python2 and python3.
6
7 :copyright: (c) 2013 by Hsiaoming Yang
8 """
9
10
11 import sys
12 PY3 = sys.version_info[0] == 3
13
14 if PY3:
15 unicode_type = str
16 bytes_type = bytes
17 text_types = (str,)
18 else:
19 unicode_type = unicode
20 bytes_type = str
21 text_types = (str, unicode)
22
23
24 def to_unicode(value, encoding='utf-8'):
25 """Convert different types of objects to unicode."""
26 if isinstance(value, unicode_type):
27 return value
28
29 if isinstance(value, bytes_type):
30 return unicode_type(value, encoding=encoding)
31
32 if isinstance(value, int):
33 return unicode_type(str(value))
34
35 return value
36
37
38 def to_bytes(value, encoding='utf-8'):
39 """Convert different types of objects to bytes."""
40 if isinstance(value, bytes_type):
41 return value
42 return value.encode(encoding)
+0
-29
livereload/cli.py less more
0 #!/usr/bin/env python
1
2 from docopt import docopt
3 from .server import start
4
5
6 cmd = """Python LiveReload
7
8 Usage:
9 livereload [-p <port>|--port=<port>] [-b|--browser] [<directory>]
10
11 Options:
12 -h --help show this screen
13 -p <port> --port=<port> specify a server port, default is 35729
14 -b --browser open browser when start server
15 """
16
17
18 def main():
19 args = docopt(cmd)
20 port = args.get('--port')
21 root = args.get('<directory>')
22 autoraise = args.get('--browser')
23 if port:
24 port = int(port)
25 else:
26 port = 35729
27
28 start(port, root, autoraise)
+0
-174
livereload/compiler.py less more
0 #!/usr/bin/python
1 # -*- coding: utf-8 -*-
2
3 """livereload.compiler
4
5 Provides a set of compilers for web developers.
6
7 Available compilers now:
8
9 + less
10 + coffee
11 + uglifyjs
12 + slimmer
13 """
14
15 import os
16 import functools
17 import logging
18 from subprocess import Popen, PIPE
19
20
21 def make_folder(dest):
22 folder = os.path.split(dest)[0]
23 if not folder:
24 return
25 if os.path.isdir(folder):
26 return
27 try:
28 os.makedirs(folder)
29 except:
30 pass
31
32
33 def _get_http_file(url, build_dir='build/assets'):
34 import hashlib
35 key = hashlib.md5(url).hexdigest()
36 filename = os.path.join(os.getcwd(), build_dir, key)
37 if os.path.exists(filename):
38 return filename
39 make_folder(filename)
40
41 import urllib
42 print('Downloading: %s' % url)
43 urllib.urlretrieve(url, filename)
44 return filename
45
46
47 class BaseCompiler(object):
48 """BaseCompiler
49
50 BaseCompiler defines the basic syntax of a Compiler.
51
52 >>> c = BaseCompiler('a')
53 >>> c.write('b') #: write compiled code to 'b'
54 >>> c.append('c') #: append compiled code to 'c'
55 """
56 def __init__(self, path=None):
57 if path:
58 if path.startswith('http://') or path.startswith('https://'):
59 path = _get_http_file(path)
60 self.filetype = os.path.splitext(path)[1]
61 self.path = path
62
63 def get_code(self):
64 f = open(self.path)
65 code = f.read()
66 f.close()
67 return code
68
69 def write(self, output):
70 """write code to output"""
71 logging.info('write %s' % output)
72 make_folder(output)
73 f = open(output, 'w')
74 code = self.get_code()
75 if code:
76 f.write(code)
77 f.close()
78
79 def append(self, output):
80 """append code to output"""
81 logging.info('append %s' % output)
82 make_folder(output)
83 f = open(output, 'a')
84 f.write(self.get_code())
85 f.close()
86
87 def __call__(self, output, mode='w'):
88 if mode == 'a':
89 self.append(output)
90 return
91 self.write(output)
92 return
93
94
95 class CommandCompiler(BaseCompiler):
96 def init_command(self, command, source=None):
97 self.command = command
98 self.source = source
99
100 def get_code(self):
101 cmd = self.command.split()
102 if self.path:
103 cmd.append(self.path)
104
105 try:
106 p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
107 except OSError as e:
108 logging.error(e)
109 if e.errno == os.errno.ENOENT: # file (command) not found
110 logging.error("maybe you haven't installed %s", cmd[0])
111 return None
112 if self.source:
113 stdout, stderr = p.communicate(input=self.source)
114 else:
115 stdout, stderr = p.communicate()
116 if stderr:
117 logging.error(stderr)
118 return None
119 #: stdout is bytes, decode for python3
120 return stdout.decode()
121
122
123 def lessc(path, output, mode='w'):
124 _compile = CommandCompiler(path)
125 _compile.init_command('lessc --compress')
126 return functools.partial(_compile, output, mode)
127
128
129 def uglifyjs(path, output, mode='w'):
130 _compile = CommandCompiler(path)
131 _compile.init_command('uglifyjs --nc')
132 return functools.partial(_compile, output, mode)
133
134
135 class SlimmerCompiler(BaseCompiler):
136 def get_code(self):
137 import slimmer
138 f = open(self.path)
139 code = f.read()
140 f.close()
141 if self.filetype == '.css':
142 return slimmer.css_slimmer(code)
143 if self.filetype == '.js':
144 return slimmer.js_slimmer(code)
145 if self.filetype == '.html':
146 return slimmer.xhtml_slimmer(code)
147 return code
148
149
150 def slimmer(path, output, mode='w'):
151 _compile = SlimmerCompiler(path)
152 return functools.partial(_compile, output, mode)
153
154
155 def rstc(path, output, mode='w'):
156 _compile = CommandCompiler(path)
157 _compile.init_command('rst2html.py')
158 return functools.partial(_compile, output, mode)
159
160
161 def shell(command, path=None, output=os.devnull, mode='w'):
162 _compile = CommandCompiler(path)
163 _compile.init_command(command)
164 return functools.partial(_compile, output, mode)
165
166
167 def coffee(path, output, mode='w'):
168 _compile = CommandCompiler(path)
169 f = open(path)
170 code = f.read()
171 f.close()
172 _compile.init_command('coffee --compile --stdio', code)
173 return functools.partial(_compile, output, mode)
0 # -*- coding: utf-8 -*-
1 """
2 livereload.handlers
3 ~~~~~~~~~~~~~~~~~~~
4
5 HTTP and WebSocket handlers for livereload.
6
7 :copyright: (c) 2013 by Hsiaoming Yang
8 """
9
10 import os
11 import time
12 import hashlib
13 import logging
14 import mimetypes
15 from tornado import ioloop
16 from tornado import escape
17 from tornado.websocket import WebSocketHandler
18 from tornado.web import RequestHandler
19 from tornado.util import ObjectDict
20 from ._compat import to_bytes
21
22
23 class LiveReloadHandler(WebSocketHandler):
24 waiters = set()
25 watcher = None
26 _last_reload_time = None
27
28 def allow_draft76(self):
29 return True
30
31 def on_close(self):
32 if self in LiveReloadHandler.waiters:
33 LiveReloadHandler.waiters.remove(self)
34
35 def send_message(self, message):
36 if isinstance(message, dict):
37 message = escape.json_encode(message)
38
39 try:
40 self.write_message(message)
41 except:
42 logging.error('Error sending message', exc_info=True)
43
44 def poll_tasks(self):
45 filepath = self.watcher.examine()
46 if not filepath:
47 return
48 logging.info('File %s changed', filepath)
49 self.watch_tasks()
50
51 def watch_tasks(self):
52 if time.time() - self._last_reload_time < 3:
53 # if you changed lot of files in one time
54 # it will refresh too many times
55 logging.info('ignore this reload action')
56 return
57
58 logging.info('Reload %s waiters', len(self.waiters))
59
60 msg = {
61 'command': 'reload',
62 'path': self.watcher.filepath or '*',
63 'liveCSS': True
64 }
65
66 self._last_reload_time = time.time()
67 for waiter in LiveReloadHandler.waiters:
68 try:
69 waiter.write_message(msg)
70 except:
71 logging.error('Error sending message', exc_info=True)
72 LiveReloadHandler.waiters.remove(waiter)
73
74 def on_message(self, message):
75 """Handshake with livereload.js
76
77 1. client send 'hello'
78 2. server reply 'hello'
79 3. client send 'info'
80
81 http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
82 """
83 message = ObjectDict(escape.json_decode(message))
84 if message.command == 'hello':
85 handshake = {}
86 handshake['command'] = 'hello'
87 handshake['protocols'] = [
88 'http://livereload.com/protocols/official-7',
89 'http://livereload.com/protocols/official-8',
90 'http://livereload.com/protocols/official-9',
91 'http://livereload.com/protocols/2.x-origin-version-negotiation',
92 'http://livereload.com/protocols/2.x-remote-control'
93 ]
94 handshake['serverName'] = 'livereload-tornado'
95 self.send_message(handshake)
96
97 if message.command == 'info' and 'url' in message:
98 logging.info('Browser Connected: %s' % message.url)
99 LiveReloadHandler.waiters.add(self)
100
101 if not LiveReloadHandler._last_reload_time:
102 if not self.watcher._tasks:
103 logging.info('Watch current working directory')
104 self.watcher.watch(os.getcwd())
105
106 LiveReloadHandler._last_reload_time = time.time()
107 logging.info('Start watching changes')
108 ioloop.PeriodicCallback(self.poll_tasks, 800).start()
109
110
111 class LiveReloadJSHandler(RequestHandler):
112 def initialize(self, port):
113 self._port = port
114
115 def get(self):
116 js = os.path.join(
117 os.path.abspath(os.path.dirname(__file__)), 'livereload.js',
118 )
119 self.set_header('Content-Type', 'application/javascript')
120 with open(js, 'r') as f:
121 content = f.read()
122 content = content.replace('{{port}}', str(self._port))
123 self.write(content)
124
125
126 class ForceReloadHandler(RequestHandler):
127 def get(self):
128 msg = {
129 'command': 'reload',
130 'path': '*',
131 'liveCSS': True
132 }
133 for waiter in LiveReloadHandler.waiters:
134 try:
135 waiter.write_message(msg)
136 except:
137 logging.error('Error sending message', exc_info=True)
138 LiveReloadHandler.waiters.remove(waiter)
139 self.write('ok')
140
141
142 class StaticHandler(RequestHandler):
143 def initialize(self, root, fallback=None):
144 self._root = os.path.abspath(root)
145 self._fallback = fallback
146
147 def filepath(self, url):
148 url = url.lstrip('/')
149 url = os.path.join(self._root, url)
150
151 if url.endswith('/'):
152 url += 'index.html'
153 elif not os.path.exists(url) and not url.endswith('.html'):
154 url += '.html'
155
156 if not os.path.exists(url):
157 return None
158 return url
159
160 def get(self, path='/'):
161 filepath = self.filepath(path)
162 if not filepath and path.endswith('/'):
163 rootdir = os.path.join(self._root, path.lstrip('/'))
164 return self.create_index(rootdir)
165
166 if not filepath:
167 if self._fallback:
168 self._fallback(self.request)
169 self._finished = True
170 return
171 return self.send_error(404)
172
173 mime_type, encoding = mimetypes.guess_type(filepath)
174 if not mime_type:
175 mime_type = 'text/html'
176
177 self.mime_type = mime_type
178 self.set_header('Content-Type', mime_type)
179
180 with open(filepath, 'r') as f:
181 data = f.read()
182
183 hasher = hashlib.sha1()
184 hasher.update(to_bytes(data))
185 self.set_header('Etag', '"%s"' % hasher.hexdigest())
186
187 ua = self.request.headers.get('User-Agent', 'bot').lower()
188 if mime_type == 'text/html' and 'msie' not in ua:
189 data = data.replace(
190 '</head>',
191 '<script src="/livereload.js"></script></head>'
192 )
193 self.write(data)
194
195 def create_index(self, root):
196 files = os.listdir(root)
197 self.write('<ul>')
198 for f in files:
199 path = os.path.join(root, f)
200 self.write('<li>')
201 if os.path.isdir(path):
202 self.write('<a href="%s/">%s</a>' % (f, f))
203 else:
204 self.write('<a href="%s">%s</a>' % (f, f))
205 self.write('</li>')
206 self.write('</ul>')
00 # -*- coding: utf-8 -*-
1
2 """livereload.app
3
4 Core Server of LiveReload.
1 """
2 livereload.server
3 ~~~~~~~~~~~~~~~~~
4
5 WSGI app server for livereload.
6
7 :copyright: (c) 2013 by Hsiaoming Yang
58 """
69
710 import os
811 import logging
9 import time
10 import mimetypes
11 import webbrowser
12 import hashlib
13 from tornado import ioloop
12 from subprocess import Popen, PIPE
1413 from tornado import escape
15 from tornado import websocket
16 from tornado.web import RequestHandler, Application
17 from tornado.util import ObjectDict
18 try:
19 from tornado.log import enable_pretty_logging
20 except ImportError:
21 from tornado.options import enable_pretty_logging
22 from livereload.task import Task
23
24
25 PORT = 35729
26 ROOT = '.'
27 LIVERELOAD = os.path.join(
28 os.path.abspath(os.path.dirname(__file__)),
29 'livereload.js',
30 )
31
32
33 class LiveReloadHandler(websocket.WebSocketHandler):
34 waiters = set()
35 _last_reload_time = None
36
37 def allow_draft76(self):
38 return True
39
40 def on_close(self):
41 if self in LiveReloadHandler.waiters:
42 LiveReloadHandler.waiters.remove(self)
43
44 def send_message(self, message):
45 if isinstance(message, dict):
46 message = escape.json_encode(message)
47
14 from tornado.wsgi import WSGIContainer
15 from tornado.ioloop import IOLoop
16 from tornado.web import Application, FallbackHandler
17 from .handlers import LiveReloadHandler, LiveReloadJSHandler
18 from .handlers import ForceReloadHandler, StaticHandler
19 from .watcher import Watcher
20 from ._compat import text_types
21 from tornado.log import enable_pretty_logging
22 enable_pretty_logging()
23
24
25 def shell(command, output=None, mode='w'):
26 """Command shell command.
27
28 You can add a shell command::
29
30 server.watch('style.less', shell('lessc style.less', output='style.css'))
31
32 :param command: a shell command
33 :param output: output stdout to the given file
34 :param mode: only works with output, mode ``w`` means write,
35 mode ``a`` means append
36 """
37 if not output:
38 output = os.devnull
39 else:
40 folder = os.path.dirname(output)
41 if folder and not os.path.isdir(folder):
42 os.makedirs(folder)
43
44 cmd = command.split()
45
46 def run_shell():
4847 try:
49 self.write_message(message)
50 except:
51 logging.error('Error sending message', exc_info=True)
52
53 def poll_tasks(self):
54 changes = Task.watch()
55 if not changes:
56 return
57 self.watch_tasks()
58
59 def watch_tasks(self):
60 if time.time() - self._last_reload_time < 3:
61 # if you changed lot of files in one time
62 # it will refresh too many times
63 logging.info('ignore this reload action')
64 return
65
66 logging.info('Reload %s waiters', len(self.waiters))
67
68 msg = {
69 'command': 'reload',
70 'path': Task.last_modified or '*',
71 'liveCSS': True
72 }
73
74 self._last_reload_time = time.time()
75 for waiter in LiveReloadHandler.waiters:
76 try:
77 waiter.write_message(msg)
78 except:
79 logging.error('Error sending message', exc_info=True)
80 LiveReloadHandler.waiters.remove(waiter)
81
82 def on_message(self, message):
83 """Handshake with livereload.js
84
85 1. client send 'hello'
86 2. server reply 'hello'
87 3. client send 'info'
88
89 http://help.livereload.com/kb/ecosystem/livereload-protocol
48 p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
49 except OSError as e:
50 logging.error(e)
51 if e.errno == os.errno.ENOENT: # file (command) not found
52 logging.error("maybe you haven't installed %s", cmd[0])
53 return e
54 stdout, stderr = p.communicate()
55 if stderr:
56 logging.error(stderr)
57 return stderr
58 #: stdout is bytes, decode for python3
59 code = stdout.decode()
60 with open(output, mode) as f:
61 f.write(code)
62
63 return run_shell
64
65
66 class WSGIWrapper(WSGIContainer):
67 """Insert livereload scripts into response body."""
68
69 def __call__(self, request):
70 data = {}
71 response = []
72
73 def start_response(status, response_headers, exc_info=None):
74 data["status"] = status
75 data["headers"] = response_headers
76 return response.append
77 app_response = self.wsgi_application(
78 WSGIContainer.environ(request), start_response)
79 try:
80 response.extend(app_response)
81 body = b"".join(response)
82 finally:
83 if hasattr(app_response, "close"):
84 app_response.close()
85 if not data:
86 raise Exception("WSGI app did not call start_response")
87
88 status_code = int(data["status"].split()[0])
89 headers = data["headers"]
90 header_set = set(k.lower() for (k, v) in headers)
91 body = escape.utf8(body)
92 body = body.replace(
93 b'</head>',
94 b'<script src="/livereload.js"></script></head>'
95 )
96
97 if status_code != 304:
98 if "content-length" not in header_set:
99 headers.append(("Content-Length", str(len(body))))
100 if "content-type" not in header_set:
101 headers.append(("Content-Type", "text/html; charset=UTF-8"))
102 if "server" not in header_set:
103 headers.append(("Server", "livereload-tornado"))
104
105 parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
106 for key, value in headers:
107 if key.lower() == 'content-length':
108 value = str(len(body))
109 parts.append(
110 escape.utf8(key) + b": " + escape.utf8(value) + b"\r\n"
111 )
112 parts.append(b"\r\n")
113 parts.append(body)
114 request.write(b"".join(parts))
115 request.finish()
116 self._log(status_code, request)
117
118
119 class Server(object):
120 """Livereload server interface.
121
122 Initialize a server and watch file changes::
123
124 server = Server(wsgi_app)
125 server.serve()
126
127 :param app: a wsgi application instance
128 :param watcher: A Watcher instance, you don't have to initialize
129 it by yourself
130 """
131 def __init__(self, app=None, watcher=None):
132 self.app = app
133 self.port = 5500
134 self.root = None
135 if not watcher:
136 watcher = Watcher()
137 self.watcher = watcher
138
139 def watch(self, filepath, func=None):
140 """Add the given filepath for watcher list.
141
142 Once you have intialized a server, watch file changes before
143 serve the server::
144
145 server.watch('static/*.stylus', 'make static')
146 def alert():
147 print('foo')
148 server.watch('foo.txt', alert)
149 server.serve()
150
151 :param filepath: files to be watched, it can be a filepath,
152 a directory, or a glob pattern
153 :param func: the function to be called, it can be a string of
154 shell command, or any callable object without
155 parameters
90156 """
91 message = ObjectDict(escape.json_decode(message))
92 if message.command == 'hello':
93 handshake = {}
94 handshake['command'] = 'hello'
95 protocols = message.protocols
96 protocols.append(
97 'http://livereload.com/protocols/2.x-remote-control'
157 if isinstance(func, text_types):
158 func = shell(func)
159
160 self.watcher.watch(filepath, func)
161
162 def application(self, debug=True):
163 LiveReloadHandler.watcher = self.watcher
164 handlers = [
165 (r'/livereload', LiveReloadHandler),
166 (r'/forcereload', ForceReloadHandler),
167 (r'/livereload.js', LiveReloadJSHandler, dict(port=self.port)),
168 ]
169
170 if self.app:
171 self.app = WSGIWrapper(self.app)
172 handlers.append(
173 (r'.*', FallbackHandler, dict(fallback=self.app))
98174 )
99 handshake['protocols'] = protocols
100 handshake['serverName'] = 'livereload-tornado'
101 self.send_message(handshake)
102
103 if message.command == 'info' and 'url' in message:
104 logging.info('Browser Connected: %s' % message.url)
105 LiveReloadHandler.waiters.add(self)
106 if not LiveReloadHandler._last_reload_time:
107 if os.path.exists('Guardfile'):
108 logging.info('Reading Guardfile')
109 execfile('Guardfile', {})
110 else:
111 logging.info('No Guardfile')
112 Task.add(os.getcwd())
113
114 LiveReloadHandler._last_reload_time = time.time()
115 logging.info('Start watching changes')
116 if not Task.start(self.watch_tasks):
117 ioloop.PeriodicCallback(self.poll_tasks, 800).start()
118
119
120 class IndexHandler(RequestHandler):
121
122 def get(self, path='/'):
123 abspath = os.path.join(os.path.abspath(ROOT), path.lstrip('/'))
124 mime_type, encoding = mimetypes.guess_type(abspath)
125 if not mime_type:
126 mime_type = 'text/html'
127
128 self.mime_type = mime_type
129 self.set_header('Content-Type', mime_type)
130 self.read_path(abspath)
131
132 def inject_livereload(self):
133 if self.mime_type != 'text/html':
134 return
135 ua = self.request.headers.get('User-Agent', 'bot').lower()
136 if 'msie' not in ua:
137 self.write('<script src="/livereload.js"></script>')
138
139 def read_path(self, abspath):
140 filepath = abspath
141 if os.path.isdir(filepath):
142 filepath = os.path.join(abspath, 'index.html')
143 if not os.path.exists(filepath):
144 self.create_index(abspath)
145 return
146 elif not os.path.exists(abspath):
147 filepath = abspath + '.html'
148
149 if os.path.exists(filepath):
150 if self.mime_type == 'text/html':
151 f = open(filepath)
152 data = f.read()
153 f.close()
154 before, after = data.split('</head>')
155 self.write(before)
156 self.inject_livereload()
157 self.write('</head>')
158 self.write(after)
159 else:
160 f = open(filepath, 'rb')
161 data = f.read()
162 f.close()
163 self.write(data)
164
165 hasher = hashlib.sha1()
166 hasher.update(data)
167 self.set_header('Etag', '"%s"' % hasher.hexdigest())
168 return
169 self.send_error(404)
170 return
171
172 def create_index(self, root):
173 self.inject_livereload()
174 files = os.listdir(root)
175 self.write('<ul>')
176 for f in files:
177 path = os.path.join(root, f)
178 self.write('<li>')
179 if os.path.isdir(path):
180 self.write('<a href="%s/">%s</a>' % (f, f))
181 else:
182 self.write('<a href="%s">%s</a>' % (f, f))
183 self.write('</li>')
184
185 self.write('</ul>')
186
187
188 class LiveReloadJSHandler(RequestHandler):
189 def get(self):
190 f = open(LIVERELOAD)
191 self.set_header('Content-Type', 'application/javascript')
192 for line in f:
193 if '{{port}}' in line:
194 line = line.replace('{{port}}', str(PORT))
195 self.write(line)
196 f.close()
197
198 handlers = [
199 (r'/livereload', LiveReloadHandler),
200 (r'/livereload.js', LiveReloadJSHandler),
201 (r'(.*)', IndexHandler),
202 ]
203
204
205 def start(port=35729, root='.', autoraise=False):
206 global PORT
207 PORT = port
208 global ROOT
209 if root is None:
210 root = '.'
211 ROOT = root
212 logging.getLogger().setLevel(logging.INFO)
213 enable_pretty_logging()
214 app = Application(handlers=handlers)
215 app.listen(port)
216 print('Serving path %s on 127.0.0.1:%s' % (root, port))
217
218 if autoraise:
219 webbrowser.open(
220 'http://127.0.0.1:%s' % port, new=2, autoraise=True
221 )
222 try:
223 ioloop.IOLoop.instance().start()
224 except KeyboardInterrupt:
225 print('Shutting down...')
226
227
228 if __name__ == '__main__':
229 start(8000)
175 else:
176 handlers.append(
177 (r'(.*)', StaticHandler, dict(root=self.root or '.')),
178 )
179 return Application(handlers=handlers, debug=debug)
180
181 def serve(self, port=None, host=None, root=None, debug=True):
182 """Start serve the server with the given port.
183
184 :param port: serve on this port, default is 5500
185 :param host: serve on this hostname, default is 0.0.0.0
186 :param root: serve static on this root directory
187 """
188 if root:
189 self.root = root
190 if port:
191 self.port = port
192 if host is None:
193 host = ''
194
195 self.application(debug=debug).listen(self.port, address=host)
196 logging.getLogger().setLevel(logging.INFO)
197 print('Serving on 127.0.0.1:%s' % self.port)
198 try:
199 IOLoop.instance().start()
200 except KeyboardInterrupt:
201 print('Shutting down...')
+0
-136
livereload/task.py less more
0 # -*- coding: utf-8 -*-
1
2 """livereload.task
3
4 Task management for LiveReload Server.
5
6 A basic syntax overview::
7
8 from livereload.task import Task
9
10 Task.add('file.css')
11
12 def do_some_thing():
13 pass
14
15 Task.add('file.css', do_some_thing)
16 """
17
18 import os
19 import glob
20 import logging
21
22 try:
23 import pyinotify
24 from tornado import ioloop
25
26 class TaskEventHandler(pyinotify.ProcessEvent):
27 def my_init(self, **kwargs):
28 self.func = kwargs['func']
29
30 def process_default(self, event):
31 if Task.watch():
32 self.func()
33
34 HAS_PYINOTIFY = True
35 except ImportError:
36 HAS_PYINOTIFY = False
37
38 IGNORE = [
39 '.pyc', '.pyo', '.o', '.swp'
40 ]
41
42
43 class Task(object):
44 tasks = {}
45 _modified_times = {}
46 last_modified = None
47 if HAS_PYINOTIFY:
48 wm = pyinotify.WatchManager()
49 notifier = None
50
51 @classmethod
52 def add(cls, path, func=None):
53 logging.info('Add task: %s' % path)
54 if HAS_PYINOTIFY:
55 cls.wm.add_watch(path, pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY, rec=True, do_glob=True, auto_add=True)
56 cls.tasks[path] = func
57
58 @classmethod
59 def start(cls, func):
60 if HAS_PYINOTIFY:
61 if not cls.notifier:
62 cls.notifier = pyinotify.TornadoAsyncNotifier(cls.wm, ioloop.IOLoop.instance(), default_proc_fun=TaskEventHandler(func=func))
63 Task.watch() # initial run so we don't miss the first change
64 return HAS_PYINOTIFY
65
66 @classmethod
67 def watch(cls):
68 _changed = False
69 for path in cls.tasks:
70 if cls.is_changed(path):
71 _changed = True
72 func = cls.tasks[path]
73 func and func()
74
75 return _changed
76
77 @classmethod
78 def is_changed(cls, path):
79 def is_file_changed(path):
80 if not os.path.isfile(path):
81 return False
82
83 _, ext = os.path.splitext(path)
84 if ext in IGNORE:
85 return False
86
87 modified = int(os.stat(path).st_mtime)
88
89 if path not in cls._modified_times:
90 cls._modified_times[path] = modified
91 return True
92
93 if path in cls._modified_times and \
94 cls._modified_times[path] != modified:
95 logging.info('file changed: %s' % path)
96 cls._modified_times[path] = modified
97 cls.last_modified = path
98 return True
99
100 cls._modified_times[path] = modified
101 return False
102
103 def is_folder_changed(path):
104 _changed = False
105 for root, dirs, files in os.walk(path, followlinks=True):
106 if '.git' in dirs:
107 dirs.remove('.git')
108 if '.hg' in dirs:
109 dirs.remove('.hg')
110 if '.svn' in dirs:
111 dirs.remove('.svn')
112 if '.cvs' in dirs:
113 dirs.remove('.cvs')
114
115 for f in files:
116 if is_file_changed(os.path.join(root, f)):
117 _changed = True
118
119 return _changed
120
121 def is_glob_changed(path):
122 _changed = False
123 for f in glob.glob(path):
124 if is_file_changed(f):
125 _changed = True
126
127 return _changed
128
129 if os.path.isfile(path):
130 return is_file_changed(path)
131 elif os.path.isdir(path):
132 return is_folder_changed(path)
133 else:
134 return is_glob_changed(path)
135 return False
0 # -*- coding: utf-8 -*-
1 """
2 livereload.watcher
3 ~~~~~~~~~~~~~~~~~~
4
5 A file watch management for LiveReload Server.
6
7 :copyright: (c) 2013 by Hsiaoming Yang
8 """
9
10 import os
11 import glob
12 import time
13
14
15 class Watcher(object):
16 """A file watcher registery."""
17 def __init__(self):
18 self._tasks = {}
19 self._mtimes = {}
20
21 # filepath that is changed
22 self.filepath = None
23 self._start = time.time()
24
25 def ignore(self, filename):
26 """Ignore a given filename or not."""
27 _, ext = os.path.splitext(filename)
28 return ext in ['.pyc', '.pyo', '.o', '.swp']
29
30 def watch(self, path, func=None):
31 """Add a task to watcher."""
32 self._tasks[path] = func
33
34 def examine(self):
35 """Check if there are changes, if true, run the given task."""
36 # clean filepath
37 self.filepath = None
38 for path in self._tasks:
39 if self.is_changed(path):
40 func = self._tasks[path]
41 # run function
42 func and func()
43 return self.filepath
44
45 def is_changed(self, path):
46 if os.path.isfile(path):
47 return self.is_file_changed(path)
48 elif os.path.isdir(path):
49 return self.is_folder_changed(path)
50 return self.is_glob_changed(path)
51
52 def is_file_changed(self, path):
53 if not os.path.isfile(path):
54 return False
55
56 if self.ignore(path):
57 return False
58
59 mtime = os.path.getmtime(path)
60
61 if path not in self._mtimes:
62 self._mtimes[path] = mtime
63 self.filepath = path
64 return mtime > self._start
65
66 if self._mtimes[path] != mtime:
67 self._mtimes[path] = mtime
68 self.filepath = path
69 return True
70
71 self._mtimes[path] = mtime
72 return False
73
74 def is_folder_changed(self, path):
75 for root, dirs, files in os.walk(path, followlinks=True):
76 if '.git' in dirs:
77 dirs.remove('.git')
78 if '.hg' in dirs:
79 dirs.remove('.hg')
80 if '.svn' in dirs:
81 dirs.remove('.svn')
82 if '.cvs' in dirs:
83 dirs.remove('.cvs')
84
85 for f in files:
86 if self.is_file_changed(os.path.join(root, f)):
87 return True
88 return False
89
90 def is_glob_changed(self, path):
91 for f in glob.glob(path):
92 if self.is_file_changed(f):
93 return True
94 return False
00 #!/usr/bin/env python
11 # -*- coding: utf-8 -*-
22
3 import os
4 ROOT = os.path.dirname(__file__)
3 import re
4 from setuptools import setup
55
6 import sys
7 kwargs = {}
8 kwargs['include_package_data'] = True
9 major, minor = sys.version_info[:2]
10 if major >= 3:
11 kwargs['use_2to3'] = True
126
13 from setuptools import setup, find_packages
14 import livereload
15 from email.utils import parseaddr
16 author, author_email = parseaddr(livereload.__author__)
7 def fread(filepath):
8 with open(filepath, 'r') as f:
9 return f.read()
10
11
12 def version():
13 content = fread('livereload/__init__.py')
14 pattern = r"__version__ = '([0-9\.]*)'"
15 m = re.findall(pattern, content)
16 return m[0]
17
1718
1819 setup(
1920 name='livereload',
20 version=livereload.__version__,
21 author=author,
22 author_email=author_email,
23 url=livereload.__homepage__,
24 packages=find_packages(),
21 version=version(),
22 author='Hsiaoming Yang',
23 author_email='me@lepture.com',
24 url='https://github.com/lepture/python-livereload',
25 packages=['livereload'],
2526 description='Python LiveReload is an awesome tool for web developers',
26 long_description=livereload.__doc__,
27 entry_points={
28 'console_scripts': ['livereload= livereload.cli:main'],
29 },
27 long_description=fread('README.rst'),
3028 install_requires=[
31 'tornado', 'docopt',
29 'tornado',
3230 ],
33 license=open(os.path.join(ROOT, 'LICENSE')).read(),
31 license='BSD',
32 include_package_data=True,
3433 classifiers=[
3534 'Development Status :: 4 - Beta',
3635 'Environment :: Console',
4241 'Operating System :: POSIX :: Linux',
4342 'Programming Language :: Python :: 2.6',
4443 'Programming Language :: Python :: 2.7',
44 'Programming Language :: Python :: 3.3',
4545 'Programming Language :: Python :: Implementation :: CPython',
4646 'Programming Language :: Python :: Implementation :: PyPy',
4747 'Topic :: Software Development :: Build Tools',
4848 'Topic :: Software Development :: Compilers',
4949 'Topic :: Software Development :: Debuggers',
50 ],
51 **kwargs
50 ]
5251 )
(New empty file)
+0
-1
tests/test_compiler.py less more
0 #!/usr/bin/python
0 #!/usr/bin/python
1
2 import os
3 import time
4 import shutil
5 from livereload.watcher import Watcher
6
7 tmpdir = os.path.join(os.path.dirname(__file__), 'tmp')
8
9
10 class TestWatcher(object):
11
12 def setUp(self):
13 if os.path.isdir(tmpdir):
14 shutil.rmtree(tmpdir)
15 os.mkdir(tmpdir)
16
17 def test_watch_dir(self):
18 os.mkdir(os.path.join(tmpdir, '.git'))
19 os.mkdir(os.path.join(tmpdir, '.hg'))
20 os.mkdir(os.path.join(tmpdir, '.svn'))
21 os.mkdir(os.path.join(tmpdir, '.cvs'))
22
23 watcher = Watcher()
24 watcher.watch(tmpdir)
25 assert watcher.is_changed(tmpdir) is False
26
27 with open(os.path.join(tmpdir, 'foo'), 'w') as f:
28 f.write('')
29
30 assert watcher.is_changed(tmpdir)
31 assert watcher.is_changed(tmpdir) is False
32
33 def test_watch_file(self):
34 watcher = Watcher()
35 watcher.count = 0
36
37 filepath = os.path.join(tmpdir, 'foo')
38 with open(filepath, 'w') as f:
39 f.write('')
40
41 def add_count():
42 watcher.count += 1
43
44 watcher.watch(filepath, add_count)
45 assert watcher.is_changed(filepath)
46
47 # sleep 1 second so that mtime will be different
48 time.sleep(1)
49
50 with open(filepath, 'w') as f:
51 f.write('')
52
53 assert watcher.examine() == os.path.abspath(filepath)
54 assert watcher.count == 1
55
56 def test_watch_glob(self):
57 watcher = Watcher()
58 watcher.watch(tmpdir + '/*')
59 assert watcher.examine() is None
60
61 with open(os.path.join(tmpdir, 'foo.pyc'), 'w') as f:
62 f.write('')
63
64 assert watcher.examine() is None
65
66 filepath = os.path.join(tmpdir, 'foo')
67
68 with open(filepath, 'w') as f:
69 f.write('')
70
71 assert watcher.examine() == os.path.abspath(filepath)