Imported Upstream version 2.1.0
Agustin Henze
10 years ago
0 | ################# | |
1 | # Common ignore # | |
2 | ################# | |
3 | build | |
4 | # Test DB | |
5 | *.sqlite | |
6 | # Test | |
0 | .DS_Store | |
1 | ||
7 | 2 | test.* |
8 | .hg | |
9 | .svn | |
10 | CVS | |
11 | *~.nib | |
12 | 3 | *.swp |
13 | 4 | *~ |
14 | ||
15 | ||
16 | ################# | |
17 | # python ignore # | |
18 | ################# | |
19 | 5 | *.py[co] |
20 | 6 | |
21 | # Packages | |
22 | 7 | *.egg |
23 | 8 | *.egg-info |
24 | 9 | dist |
25 | 10 | eggs |
26 | parts | |
27 | bin | |
28 | var | |
29 | 11 | sdist |
30 | 12 | develop-eggs |
31 | 13 | .installed.cfg |
32 | 14 | |
33 | # Installer logs | |
15 | build | |
16 | ||
34 | 17 | pip-log.txt |
35 | 18 | |
36 | # Unit test / coverage reports | |
37 | 19 | .coverage |
38 | 20 | .tox |
39 | 21 | |
40 | # Translations | |
41 | *.mo | |
42 | ||
43 | # Mr Developer | |
44 | .mr.developer.cfg | |
45 | ||
46 | # sphinx | |
47 | 22 | 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> | |
1 | 1 | |
2 | 2 | Redistribution and use in source and binary forms, with or without |
3 | 3 | modification, are permitted provided that the following conditions |
0 | .PHONY: clean-pyc clean-build docs | |
0 | .PHONY: clean-pyc clean-build docs test coverage | |
1 | 1 | |
2 | 2 | clean: clean-build clean-pyc |
3 | 3 | |
18 | 18 | |
19 | 19 | docs: |
20 | 20 | $(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 | ========== | |
2 | 2 | |
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. | |
9 | 4 | |
10 | 5 | Installation |
11 | 6 | ------------ |
12 | 7 | |
13 | 8 | Python LiveReload is designed for web developers who know Python. |
14 | ||
15 | Install python-livereload | |
16 | ~~~~~~~~~~~~~~~~~~~~~~~~~ | |
17 | 9 | |
18 | 10 | Install Python LiveReload with pip:: |
19 | 11 | |
24 | 16 | $ easy_install livereload |
25 | 17 | |
26 | 18 | |
27 | Install Browser Extensions | |
28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
19 | Developer Guide | |
20 | --------------- | |
29 | 21 | |
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:: | |
32 | 24 | |
33 | <script type="text/javascript" src="http://127.0.0.1:35729/livereload.js"></script> | |
25 | from livereload import Server, shell | |
34 | 26 | |
35 | But a browser extension will make your life easier, available extensions: | |
27 | server = Server(wsgi_app) | |
36 | 28 | |
37 | + Chrome Extension | |
38 | + Safari Extension | |
39 | + Firefox Extension | |
29 | # run a shell command | |
30 | server.watch('static/*.stylus', 'make static') | |
40 | 31 | |
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) | |
42 | 36 | |
43 | Quickstart | |
44 | ------------ | |
37 | # output stdout into a file | |
38 | server.watch('style.less', shell('lessc style.less', output='style.css')) | |
45 | 39 | |
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() | |
48 | 41 | |
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: | |
51 | 43 | |
52 | $ livereload [-p port] | |
44 | - app: a wsgi application | |
45 | - watcher: a watcher instance, you don't have to create one | |
53 | 46 | |
54 | your browser will reload, if any file in the working directory changed. | |
47 | server.watch | |
48 | ~~~~~~~~~~~~ | |
55 | 49 | |
50 | ``server.watch`` can watch a filepath, a directory and a glob pattern:: | |
56 | 51 | |
57 | LiveReload as SimpleHTTPServer | |
58 | ------------------------------- | |
52 | server.watch('path/to/file.txt') | |
53 | server.watch('directory/path/') | |
54 | server.watch('glob/*.pattern') | |
59 | 55 | |
60 | Livereload server can be a SimpleHTTPServer:: | |
56 | You can also use other library (for example: formic) for more powerful | |
57 | file adding:: | |
61 | 58 | |
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') |
1 | 1 | ========= |
2 | 2 | |
3 | 3 | 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. | |
4 | 15 | |
5 | 16 | Version 1.0.1 |
6 | 17 | ------------- |
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. |
92 | 92 | |
93 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for |
94 | 94 | # a list of builtin themes. |
95 | html_theme = 'flask' | |
95 | html_theme = 'flask_small' | |
96 | 96 | |
97 | 97 | # Theme options are theme-specific and customize the look and feel of a theme |
98 | 98 | # further. For a list of options available for each theme, see the |
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 | |
2 | 1 | |
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 | --- | |
6 | 4 | |
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 | |
10 | 6 | |
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: | |
13 | 9 | |
14 | **Bug Report** https://github.com/lepture/python-livereload/issues | |
10 | .. autofunction:: shell | |
15 | 11 | |
16 | User's Guide | |
17 | ------------- | |
12 | Changelog | |
13 | --------- | |
18 | 14 | |
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. | |
23 | 16 | |
24 | 17 | .. toctree:: |
25 | :maxdepth: 2 | |
18 | :maxdepth: 2 | |
26 | 19 | |
27 | install | |
28 | quickstart | |
29 | guardfile | |
30 | compiler | |
31 | changelog | |
20 | changelog | |
32 | 21 | |
33 | 22 | |
34 | 23 | Contact |
35 | --------- | |
24 | ------- | |
36 | 25 | |
37 | 26 | Have any trouble? Want to know more? |
38 | 27 | |
42 | 31 | |
43 | 32 | .. _GitHub: https://github.com/lepture |
44 | 33 | .. _Twitter: https://twitter.com/lepture |
45 | .. _Email: lepture@me.com | |
34 | .. _Email: me@lepture.com |
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 | .. _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 | #!/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 | ~~~~~~~~~~ | |
30 | 3 | |
31 | """ | |
32 | Python LiveReload | |
33 | ================= | |
4 | A python version of livereload. | |
34 | 5 | |
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 | |
133 | 7 | """ |
134 | 8 | |
135 | __version__ = '1.0.1' | |
9 | __version__ = '2.1.0' | |
136 | 10 | __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 | #!/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 | #!/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>') |
0 | 0 | # -*- 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 | |
5 | 8 | """ |
6 | 9 | |
7 | 10 | import os |
8 | 11 | 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 | |
14 | 13 | 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(): | |
48 | 47 | 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 | |
90 | 156 | """ |
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)) | |
98 | 174 | ) |
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 | # -*- 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 |
0 | 0 | #!/usr/bin/env python |
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 | |
3 | import os | |
4 | ROOT = os.path.dirname(__file__) | |
3 | import re | |
4 | from setuptools import setup | |
5 | 5 | |
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 | |
12 | 6 | |
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 | ||
17 | 18 | |
18 | 19 | setup( |
19 | 20 | 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'], | |
25 | 26 | 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'), | |
30 | 28 | install_requires=[ |
31 | 'tornado', 'docopt', | |
29 | 'tornado', | |
32 | 30 | ], |
33 | license=open(os.path.join(ROOT, 'LICENSE')).read(), | |
31 | license='BSD', | |
32 | include_package_data=True, | |
34 | 33 | classifiers=[ |
35 | 34 | 'Development Status :: 4 - Beta', |
36 | 35 | 'Environment :: Console', |
42 | 41 | 'Operating System :: POSIX :: Linux', |
43 | 42 | 'Programming Language :: Python :: 2.6', |
44 | 43 | 'Programming Language :: Python :: 2.7', |
44 | 'Programming Language :: Python :: 3.3', | |
45 | 45 | 'Programming Language :: Python :: Implementation :: CPython', |
46 | 46 | 'Programming Language :: Python :: Implementation :: PyPy', |
47 | 47 | 'Topic :: Software Development :: Build Tools', |
48 | 48 | 'Topic :: Software Development :: Compilers', |
49 | 49 | 'Topic :: Software Development :: Debuggers', |
50 | ], | |
51 | **kwargs | |
50 | ] | |
52 | 51 | ) |
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) |