├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── README.rst ├── b ├── build.py ├── epydoc.config ├── example.py ├── pynt ├── __init__.py ├── _pynt.py └── tests │ ├── __init__.py │ ├── build_scripts │ ├── __init__.py │ ├── annotation_misuse_1.py │ ├── annotation_misuse_2.py │ ├── annotation_misuse_3.py │ ├── build_with_local_import.py │ ├── build_with_params.py │ ├── default_task_and_import_dependencies.py │ ├── dependencies.py │ ├── options.py │ ├── runtime_error.py │ ├── simple.py │ └── test_module.py │ └── test_pynt.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | apidocs 3 | MANIFEST 4 | dist 5 | .project 6 | build 7 | .pydevproject 8 | .ropeproject 9 | *.out 10 | pynt.egg-info -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.6" 6 | # - "pypy" #module level vars in tests are not mutable in pypy? 7 | 8 | script: 9 | - py.test 10 | 11 | 12 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 0.8.2 - 23/06/2018 4 | ------------------ 5 | * private tasks. Any tasks that start with an underscore(_) are private by convention. 6 | 7 | 8 | 0.8.1 - 02/09/2013 9 | ------------------ 10 | * Enabling extensions 11 | 12 | 0.8.0 - 02/09/2013 13 | ------------------ 14 | * Support for specifying a default task with __DEFAULT__ variable 15 | * pynt -v (--version) for displays version info 16 | * pynt -l lists tasks in alphabetical order 17 | 18 | 0.7.1 - 17/03/2013 19 | ------------------ 20 | * Migrated pynt to work on python 3.x. pynt still works on 2.7. 21 | * pynt version now displayed as part of help output. 22 | 23 | 0.7.0 - 16/02/2013 24 | ------------------ 25 | 26 | * New commandline interface. Distribution now includes 'pynt' executable. 27 | * 'build.py' is the default build file. 28 | * Build files no longer need "if main" construct. 29 | * pynt no longer exposes build method. This is a backward incompatible change. 30 | 31 | 32 | 0.6.0 - 17/12/2012 33 | ------------------ 34 | 35 | * Simplified ignoring tasks. ignore a keyword param for task and not a separate decorator. [This change is NOT backward compatible!!!] 36 | * Added support for listing tasks 37 | * Improved help 38 | 39 | 40 | 0.5.0 - 01/12/2012 41 | ------------------ 42 | 43 | * Ability to pass params to tasks. 44 | * Major rewrite and flattening the package hierarchy. 45 | 46 | 0.4.0 - 17/11/2012 47 | ------------------ 48 | 49 | * Support for running multiple tasks from commandline. 50 | * Ability to run tasks by typing in just the first few unambigious charecters. 51 | 52 | 53 | Changes before forking from microbuild 54 | ====================================== 55 | 56 | 0.3.0 - 18/09/2012 57 | ------------------ 58 | 59 | * Fixed bug in logging. No longer modifies root logger. 60 | * Added ignore functionality. 61 | * Extended API documentation. 62 | 63 | 0.2.0 - 29/08/2012 64 | ------------------ 65 | 66 | * Added progress tracking output. 67 | * Added handling of exceptions within tasks. 68 | 69 | 0.1.0 - 28/08/2012 70 | ------------------ 71 | 72 | * Initial release. 73 | * Added management of dependancies between tasks. 74 | * Added automatic generation of command line interface. 75 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Raghunandan Rao 2 | Copyright (C) 2012 Calum J. Eadie 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in the 6 | Software without restriction, including without limitation the rights to use, copy, 7 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES.rst 3 | include LICENSE.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rags/pynt.png?branch=master)](https://travis-ci.org/rags/pynt) 2 | 3 | A pynt of Python build. 4 | ============================= 5 | 6 | [Raghunandan Rao](https://github.com/rags) 7 | 8 | ## Features 9 | 10 | * Easy to learn. 11 | * Build tasks are just python funtions. 12 | * Manages dependencies between tasks. 13 | * Automatically generates a command line interface. 14 | * Rake style param passing to tasks 15 | * Supports python 2.7 and python 3.x 16 | 17 | ## Installation 18 | 19 | 20 | You can install pynt from the Python Package Index (PyPI) or from source. 21 | 22 | Using pip 23 | 24 | ```bash 25 | $ pip install pynt 26 | ``` 27 | 28 | Using easy_install 29 | 30 | ```bash 31 | $ easy_install pynt 32 | ``` 33 | 34 | ## Example 35 | 36 | 37 | The build script is written in pure Python and pynt takes care of managing 38 | any dependencies between tasks and generating a command line interface. 39 | 40 | Writing build tasks is really simple, all you need to know is the @task decorator. Tasks are just regular Python 41 | functions marked with the ``@task()`` decorator. Dependencies are specified with ``@task()`` too. Tasks can be 42 | ignored with the ``@task(ignore=True)``. Disabling a task is an useful feature to have in situations where you have one 43 | task that a lot of other tasks depend on and you want to quickly remove it from the dependency chains of all the 44 | dependent tasks. Note that any task whose name starts with an underscore(``_``) will be considered private. 45 | Private tasks are not listed in with ``pynt -l``, but they can still be run with ``pynt _private_task_name`` 46 | 47 | **build.py** 48 | ------------ 49 | 50 | ```python 51 | 52 | #!/usr/bin/python 53 | 54 | import sys 55 | from pynt import task 56 | 57 | @task() 58 | def clean(): 59 | '''Clean build directory.''' 60 | print 'Cleaning build directory...' 61 | 62 | @task() 63 | def _copy_resources(): 64 | '''Copy resource files. This is a private task. "pynt -l" will not list this''' 65 | print('Copying resource files') 66 | 67 | @task(clean, _copy_resources) 68 | def html(target='.'): 69 | '''Generate HTML.''' 70 | print 'Generating HTML in directory "%s"' % target 71 | 72 | @task(clean, _copy_resources, ignore=True) 73 | def images(): 74 | '''Prepare images.''' 75 | print 'Preparing images...' 76 | 77 | @task(html,images) 78 | def start_server(server='localhost', port = '80'): 79 | '''Start the server''' 80 | print 'Starting server at %s:%s' % (server, port) 81 | 82 | @task(start_server) #Depends on task with all optional params 83 | def stop_server(): 84 | print 'Stopping server....' 85 | 86 | @task() 87 | def copy_file(src, dest): 88 | print 'Copying from %s to %s' % (src, dest) 89 | 90 | @task() 91 | def echo(*args,**kwargs): 92 | print args 93 | print kwargs 94 | 95 | # Default task (if specified) is run when no task is specified in the command line 96 | # make sure you define the variable __DEFAULT__ after the task is defined 97 | # A good convention is to define it at the end of the module 98 | # __DEFAULT__ is an optional member 99 | 100 | __DEFAULT__=start_server 101 | ``` 102 | 103 | **Running pynt tasks** 104 | ----------------------- 105 | 106 | The command line interface and help is automatically generated. Task descriptions 107 | are extracted from function docstrings. 108 | 109 | ```bash 110 | $ pynt -h 111 | usage: pynt [-h] [-l] [-v] [-f file] [task [task ...]] 112 | 113 | positional arguments: 114 | task perform specified task and all its dependencies 115 | 116 | optional arguments: 117 | -h, --help show this help message and exit 118 | -l, --list-tasks List the tasks 119 | -v, --version Display the version information 120 | -f file, --file file Build file to read the tasks from. Default is 121 | 'build.py' 122 | ``` 123 | 124 | ```bash 125 | $ pynt -l 126 | Tasks in build file ./build.py: 127 | clean Clean build directory. 128 | copy_file 129 | echo 130 | html Generate HTML. 131 | images [Ignored] Prepare images. 132 | start_server [Default] Start the server 133 | stop_server 134 | 135 | Powered by pynt - A Lightweight Python Build Tool. 136 | ``` 137 | 138 | pynt takes care of dependencies between tasks. In the following case start_server depends on clean, html and image generation (image task is ignored). 139 | 140 | ```bash 141 | $ pynt #Runs the default task start_server. It does exactly what "pynt start_server" would do. 142 | [ example.py - Starting task "clean" ] 143 | Cleaning build directory... 144 | [ example.py - Completed task "clean" ] 145 | [ example.py - Starting task "html" ] 146 | Generating HTML in directory "." 147 | [ example.py - Completed task "html" ] 148 | [ example.py - Ignoring task "images" ] 149 | [ example.py - Starting task "start_server" ] 150 | Starting server at localhost:80 151 | [ example.py - Completed task "start_server" ] 152 | ``` 153 | 154 | The first few characters of the task name is enough to execute the task, as long as the partial name is unambigious. You can specify multiple tasks to run in the commandline. Again the dependencies are taken taken care of. 155 | 156 | ```bash 157 | $ pynt cle ht cl 158 | [ example.py - Starting task "clean" ] 159 | Cleaning build directory... 160 | [ example.py - Completed task "clean" ] 161 | [ example.py - Starting task "html" ] 162 | Generating HTML in directory "." 163 | [ example.py - Completed task "html" ] 164 | [ example.py - Starting task "clean" ] 165 | Cleaning build directory... 166 | [ example.py - Completed task "clean" ] 167 | ``` 168 | 169 | The 'html' task dependency 'clean' is run only once. But clean can be explicitly run again later. 170 | 171 | pynt tasks can accept parameters from commandline. 172 | 173 | ```bash 174 | $ pynt "copy_file[/path/to/foo, path_to_bar]" 175 | [ example.py - Starting task "clean" ] 176 | Cleaning build directory... 177 | [ example.py - Completed task "clean" ] 178 | [ example.py - Starting task "copy_file" ] 179 | Copying from /path/to/foo to path_to_bar 180 | [ example.py - Completed task "copy_file" ] 181 | ``` 182 | 183 | pynt can also accept keyword arguments. 184 | 185 | ```bash 186 | $ pynt start[port=8888] 187 | [ example.py - Starting task "clean" ] 188 | Cleaning build directory... 189 | [ example.py - Completed task "clean" ] 190 | [ example.py - Starting task "html" ] 191 | Generating HTML in directory "." 192 | [ example.py - Completed task "html" ] 193 | [ example.py - Ignoring task "images" ] 194 | [ example.py - Starting task "start_server" ] 195 | Starting server at localhost:8888 196 | [ example.py - Completed task "start_server" ] 197 | 198 | $ pynt echo[hello,world,foo=bar,blah=123] 199 | [ example.py - Starting task "echo" ] 200 | ('hello', 'world') 201 | {'blah': '123', 'foo': 'bar'} 202 | [ example.py - Completed task "echo" ] 203 | ``` 204 | 205 | **Organizing build scripts** 206 | ----------------------------- 207 | 208 | You can break up your build files into modules and simple import them into your main build file. 209 | 210 | ```python 211 | from deploy_tasks import * 212 | from test_tasks import functional_tests, report_coverage 213 | ``` 214 | 215 | ## pynt-contrib 216 | 217 | [pynt-contrib](https://github.com/rags/pynt-contrib) contains a set of extra tasks/utilities. The idea is to keep this package simple and bloat-free. 218 | 219 | ## Contributors/Contributing 220 | 221 | 222 | * Calum J. Eadie - pynt is preceded by and forked from [microbuild](https://github.com/CalumJEadie/microbuild), which was created by [Calum J. Eadie](https://github.com/CalumJEadie). 223 | 224 | 225 | If you want to make changes the repo is at https://github.com/rags/pynt. You will need [pytest](http://www.pytest.org) to run the tests 226 | 227 | ```bash 228 | $ ./b t 229 | ``` 230 | 231 | It will be great if you can raise a [pull request](https://help.github.com/articles/using-pull-requests) once you are done. 232 | 233 | *If you find any bugs or need new features please raise a ticket in the [issues section](https://github.com/rags/pynt/issues) of the github repo.* 234 | 235 | ## License 236 | 237 | pynt is licensed under a [MIT license](http://opensource.org/licenses/MIT) 238 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `|Build Status| `_ 2 | 3 | A pynt of Python build. 4 | ======================= 5 | 6 | `Raghunandan Rao `_ 7 | 8 | Features 9 | -------- 10 | 11 | - Easy to learn. 12 | - Build tasks are just python funtions. 13 | - Manages dependencies between tasks. 14 | - Automatically generates a command line interface. 15 | - Rake style param passing to tasks 16 | - Supports python 2.7 and python 3.x 17 | 18 | Installation 19 | ------------ 20 | 21 | You can install pynt from the Python Package Index (PyPI) or from 22 | source. 23 | 24 | Using pip 25 | 26 | :: 27 | 28 | $ pip install pynt 29 | 30 | Using easy\_install 31 | 32 | :: 33 | 34 | $ easy_install pynt 35 | 36 | Example 37 | ------- 38 | 39 | The build script is written in pure Python and pynt takes care of 40 | managing any dependencies between tasks and generating a command line 41 | interface. 42 | 43 | Writing build tasks is really simple, all you need to know is the @task 44 | decorator. Tasks are just regular Python functions marked with the 45 | ``@task()`` decorator. Dependencies are specified with ``@task()`` too. 46 | Tasks can be ignored with the ``@task(ignore=True)``. Disabling a task 47 | is an useful feature to have in situations where you have one task that 48 | a lot of other tasks depend on and you want to quickly remove it from 49 | the dependency chains of all the dependent tasks. Note that any task 50 | whose name starts with an underscore(\ ``_``) will be considered 51 | private. Private tasks are not listed in with ``pynt -l``, but they can 52 | still be run with ``pynt _private_task_name`` 53 | 54 | **build.py** 55 | ------------ 56 | 57 | :: 58 | 59 | 60 | #!/usr/bin/python 61 | 62 | import sys 63 | from pynt import task 64 | 65 | @task() 66 | def clean(): 67 | '''Clean build directory.''' 68 | print 'Cleaning build directory...' 69 | 70 | @task(clean) 71 | def html(target='.'): 72 | '''Generate HTML.''' 73 | print 'Generating HTML in directory "%s"' % target 74 | 75 | @task() 76 | def _copy_resources(): 77 | '''Copy resource files. This is a private task. "pynt -l" will not list this''' 78 | print('Copying resource files') 79 | 80 | @task(clean, _copy_resources) 81 | def html(target='.'): 82 | '''Generate HTML.''' 83 | print 'Generating HTML in directory "%s"' % target 84 | 85 | @task(clean, _copy_resources, ignore=True) 86 | def images(): 87 | '''Prepare images.''' 88 | print 'Preparing images...' 89 | 90 | @task(start_server) #Depends on task with all optional params 91 | def stop_server(): 92 | print 'Stopping server....' 93 | 94 | @task() 95 | def copy_file(src, dest): 96 | print 'Copying from %s to %s' % (src, dest) 97 | 98 | @task() 99 | def echo(*args,**kwargs): 100 | print args 101 | print kwargs 102 | 103 | # Default task (if specified) is run when no task is specified in the command line 104 | # make sure you define the variable __DEFAULT__ after the task is defined 105 | # A good convention is to define it at the end of the module 106 | # __DEFAULT__ is an optional member 107 | 108 | __DEFAULT__=start_server 109 | 110 | **Running pynt tasks** 111 | ---------------------- 112 | 113 | The command line interface and help is automatically generated. Task 114 | descriptions are extracted from function docstrings. 115 | 116 | :: 117 | 118 | $ pynt -h 119 | usage: pynt [-h] [-l] [-v] [-f file] [task [task ...]] 120 | 121 | positional arguments: 122 | task perform specified task and all its dependencies 123 | 124 | optional arguments: 125 | -h, --help show this help message and exit 126 | -l, --list-tasks List the tasks 127 | -v, --version Display the version information 128 | -f file, --file file Build file to read the tasks from. Default is 129 | 'build.py' 130 | 131 | :: 132 | 133 | $ pynt -l 134 | Tasks in build file ./build.py: 135 | clean Clean build directory. 136 | copy_file 137 | echo 138 | html Generate HTML. 139 | images [Ignored] Prepare images. 140 | start_server [Default] Start the server 141 | stop_server 142 | 143 | Powered by pynt - A Lightweight Python Build Tool. 144 | 145 | pynt takes care of dependencies between tasks. In the following case 146 | start\_server depends on clean, html and image generation (image task is 147 | ignored). 148 | 149 | :: 150 | 151 | $ pynt #Runs the default task start_server. It does exactly what "pynt start_server" would do. 152 | [ example.py - Starting task "clean" ] 153 | Cleaning build directory... 154 | [ example.py - Completed task "clean" ] 155 | [ example.py - Starting task "html" ] 156 | Generating HTML in directory "." 157 | [ example.py - Completed task "html" ] 158 | [ example.py - Ignoring task "images" ] 159 | [ example.py - Starting task "start_server" ] 160 | Starting server at localhost:80 161 | [ example.py - Completed task "start_server" ] 162 | 163 | The first few characters of the task name is enough to execute the task, 164 | as long as the partial name is unambigious. You can specify multiple 165 | tasks to run in the commandline. Again the dependencies are taken taken 166 | care of. 167 | 168 | :: 169 | 170 | $ pynt cle ht cl 171 | [ example.py - Starting task "clean" ] 172 | Cleaning build directory... 173 | [ example.py - Completed task "clean" ] 174 | [ example.py - Starting task "html" ] 175 | Generating HTML in directory "." 176 | [ example.py - Completed task "html" ] 177 | [ example.py - Starting task "clean" ] 178 | Cleaning build directory... 179 | [ example.py - Completed task "clean" ] 180 | 181 | The 'html' task dependency 'clean' is run only once. But clean can be 182 | explicitly run again later. 183 | 184 | pynt tasks can accept parameters from commandline. 185 | 186 | :: 187 | 188 | $ pynt "copy_file[/path/to/foo, path_to_bar]" 189 | [ example.py - Starting task "clean" ] 190 | Cleaning build directory... 191 | [ example.py - Completed task "clean" ] 192 | [ example.py - Starting task "copy_file" ] 193 | Copying from /path/to/foo to path_to_bar 194 | [ example.py - Completed task "copy_file" ] 195 | 196 | pynt can also accept keyword arguments. 197 | 198 | :: 199 | 200 | $ pynt start[port=8888] 201 | [ example.py - Starting task "clean" ] 202 | Cleaning build directory... 203 | [ example.py - Completed task "clean" ] 204 | [ example.py - Starting task "html" ] 205 | Generating HTML in directory "." 206 | [ example.py - Completed task "html" ] 207 | [ example.py - Ignoring task "images" ] 208 | [ example.py - Starting task "start_server" ] 209 | Starting server at localhost:8888 210 | [ example.py - Completed task "start_server" ] 211 | 212 | $ pynt echo[hello,world,foo=bar,blah=123] 213 | [ example.py - Starting task "echo" ] 214 | ('hello', 'world') 215 | {'blah': '123', 'foo': 'bar'} 216 | [ example.py - Completed task "echo" ] 217 | 218 | **Organizing build scripts** 219 | ---------------------------- 220 | 221 | You can break up your build files into modules and simple import them 222 | into your main build file. 223 | 224 | :: 225 | 226 | from deploy_tasks import * 227 | from test_tasks import functional_tests, report_coverage 228 | 229 | Contributors/Contributing 230 | ------------------------- 231 | 232 | - Calum J. Eadie - pynt is preceded by and forked from 233 | `microbuild `_, which was 234 | created by `Calum J. Eadie `_. 235 | 236 | If you want to make changes the repo is at https://github.com/rags/pynt. 237 | You will need `pytest `_ to run the tests 238 | 239 | :: 240 | 241 | $ ./b t 242 | 243 | It will be great if you can raise a `pull 244 | request `_ once 245 | you are done. 246 | 247 | *If you find any bugs or need new features please raise a ticket in the 248 | `issues section `_ of the github 249 | repo.* 250 | 251 | License 252 | ------- 253 | 254 | pynt is licensed under a `MIT 255 | license `_ 256 | 257 | .. |Build 258 | Status| image:: https://travis-ci.org/rags/pynt.png?branch=master 259 | -------------------------------------------------------------------------------- /b: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pynt import main 4 | 5 | if __name__ == '__main__': 6 | main() 7 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import subprocess 4 | from pynt import task 5 | 6 | @task() 7 | def apidoc(): 8 | """ 9 | Generate API documentation using epydoc. 10 | """ 11 | subprocess.call(["epydoc","--config","epydoc.config"]) 12 | 13 | @task() 14 | def test(*args): 15 | """ 16 | Run unit tests. 17 | """ 18 | subprocess.call(["py.test-2.7"] + list(args)) 19 | subprocess.call(["py.test-3.3"] + list(args)) 20 | 21 | @task() 22 | def generate_rst(): 23 | 24 | subprocess.call(['pandoc', '-f', 'markdown', '-t', 'rst', '-o', 'README.rst', 'README.md']) 25 | 26 | @task(generate_rst) 27 | def upload(): 28 | subprocess.call(['ssh-add', '~/.ssh/id_rsa']) 29 | subprocess.call(['python', 'setup.py', 'sdist', 'bdist_wininst', 'upload']) 30 | 31 | __DEFAULT__ = test -------------------------------------------------------------------------------- /epydoc.config: -------------------------------------------------------------------------------- 1 | [epydoc] 2 | 3 | name: pynt - Really simple Python build tool. 4 | url: https://github.com/rags/pynt 5 | 6 | modules: pynt/__init__.py 7 | 8 | output: html 9 | target: apidocs 10 | 11 | css: white 12 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from pynt import task 3 | 4 | @task() 5 | def clean(): 6 | '''Clean build directory.''' 7 | print('Cleaning build directory...') 8 | 9 | @task() 10 | def _copy_resources(): 11 | '''Copy resource files.''' 12 | print('Copying resource files') 13 | 14 | @task(clean, _copy_resources) 15 | def html(target='.'): 16 | '''Generate HTML.''' 17 | print(('Generating HTML in directory "%s"' % target)) 18 | 19 | 20 | @task(clean, _copy_resources, ignore=True) 21 | def images(): 22 | '''Prepare images.''' 23 | print('Preparing images...') 24 | 25 | @task(html,images) 26 | def start_server(server='localhost', port = '80'): 27 | '''Start the server''' 28 | print(('Starting server at %s:%s' % (server, port))) 29 | 30 | @task(start_server) #Depends on task with all optional params 31 | def stop_server(): 32 | print('Stopping server....') 33 | 34 | @task() 35 | def copy_file(src, dest): 36 | print(('Copying from %s to %s' % (src, dest))) 37 | 38 | @task() 39 | def echo(*args,**kwargs): 40 | print(args) 41 | print(kwargs) 42 | 43 | @task() 44 | def error_task(): 45 | print("this should fail with an error") 46 | raise IOError 47 | 48 | # Default task (if specified) is run when no task is specified in the command line 49 | # make sure you define the variable __DEFAULT__ after the task is defined 50 | # A good convention is to define it at the end of the module 51 | # __DEFAULT__ is an optional member 52 | 53 | __DEFAULT__=start_server 54 | -------------------------------------------------------------------------------- /pynt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lightweight Python Build Tool 3 | """ 4 | 5 | __version__ = "0.8.2" 6 | __license__ = "MIT License" 7 | __contact__ = "http://rags.github.com/pynt/" 8 | from ._pynt import task, main 9 | import pkgutil 10 | 11 | __path__ = pkgutil.extend_path(__path__,__name__) 12 | 13 | __all__ = ["task", "main"] 14 | -------------------------------------------------------------------------------- /pynt/_pynt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Lightweight Python Build Tool 3 | 4 | """ 5 | 6 | import inspect 7 | import argparse 8 | import logging 9 | import os 10 | from os import path 11 | import re 12 | import imp 13 | import sys 14 | from pynt import __version__ 15 | 16 | _CREDIT_LINE = "Powered by pynt %s - A Lightweight Python Build Tool." % __version__ 17 | _LOGGING_FORMAT = "[ %(name)s - %(message)s ]" 18 | _TASK_PATTERN = re.compile("^([^\[]+)(\[([^\]]*)\])?$") 19 | #"^([^\[]+)(\[([^\],=]*(,[^\],=]+)*(,[^\],=]+=[^\],=]+)*)\])?$" 20 | def build(args): 21 | """ 22 | Build the specified module with specified arguments. 23 | 24 | @type module: module 25 | @type args: list of arguments 26 | """ 27 | # Build the command line. 28 | parser = _create_parser() 29 | 30 | #No args passed. 31 | #if not args: #todo: execute default task. 32 | # parser.print_help() 33 | # print("\n\n"+_CREDIT_LINE) 34 | # exit 35 | # Parse arguments. 36 | args = parser.parse_args(args) 37 | 38 | if args.version: 39 | print('pynt %s' % __version__) 40 | sys.exit(0) 41 | 42 | #load build file as a module 43 | module = _load_buildscript(args.file) 44 | 45 | # Run task and all its dependencies. 46 | if args.list_tasks: 47 | print_tasks(module, args.file) 48 | elif not args.tasks: 49 | if not _run_default_task(module): 50 | parser.print_help() 51 | print("\n") 52 | print_tasks(module, args.file) 53 | else: 54 | _run_from_task_names(module,args.tasks) 55 | 56 | def print_tasks(module, file): 57 | # Get all tasks. 58 | tasks = _get_tasks(module) 59 | 60 | # Build task_list to describe the tasks. 61 | task_list = "Tasks in build file %s:" % file 62 | name_width = _get_max_name_length(module)+4 63 | task_help_format = "\n {0:<%s} {1: ^10} {2}" % name_width 64 | default = _get_default_task(module) 65 | for task in sorted(tasks, key=lambda task: task.name): 66 | attributes = [] 67 | if task.ignored: 68 | attributes.append('Ignored') 69 | if default and task.name == default.name: 70 | attributes.append('Default') 71 | 72 | task_list += task_help_format.format(task.name, 73 | ('[' + ', '.join(attributes) + ']') 74 | if attributes else '', 75 | task.doc) 76 | print(task_list + "\n\n"+_CREDIT_LINE) 77 | 78 | def _load_buildscript(file_path): 79 | if not path.isfile(file_path): 80 | print("Build file '%s' does not exist. Please specify a build file\n" % file_path) 81 | parser.print_help() 82 | sys.exit(1) 83 | 84 | script_dir, script_base = path.split(file_path) 85 | 86 | # Append directory of build script to path, to allow importing modules relatively to the script 87 | sys.path.append(path.abspath(script_dir)) 88 | 89 | module_name, suffix = path.splitext(script_base) 90 | description = (suffix, 'r', imp.PY_SOURCE) 91 | 92 | with open(file_path, 'r') as script_file: 93 | return imp.load_module(module_name, script_file, file_path, description) 94 | 95 | def _get_default_task(module): 96 | matching_tasks = [task for name,task in inspect.getmembers(module,Task.is_task) 97 | if name == "__DEFAULT__"] 98 | if matching_tasks: 99 | return matching_tasks[0] 100 | 101 | def _run_default_task(module): 102 | default_task = _get_default_task(module) 103 | if not default_task: 104 | return False 105 | _run(module, _get_logger(module), default_task, set()) 106 | return True 107 | 108 | 109 | def _run_from_task_names(module,task_names): 110 | """ 111 | @type module: module 112 | @type task_name: string 113 | @param task_name: Task name, exactly corresponds to function name. 114 | """ 115 | # Create logger. 116 | logger = _get_logger(module) 117 | all_tasks = _get_tasks(module) 118 | completed_tasks = set([]) 119 | for task_name in task_names: 120 | task, args, kwargs= _get_task(module, task_name, all_tasks) 121 | _run(module, logger, task, completed_tasks, True, args, kwargs) 122 | 123 | def _get_task(module, name, tasks): 124 | # Get all tasks. 125 | match = _TASK_PATTERN.match(name) 126 | if not match: 127 | raise Exception("Invalid task argument %s" % name) 128 | task_name, _, args_str = match.groups() 129 | 130 | args, kwargs= _parse_args(args_str) 131 | if hasattr(module, task_name): 132 | return getattr(module, task_name), args, kwargs 133 | matching_tasks = [task for task in tasks if task.name.startswith(task_name)] 134 | 135 | if not matching_tasks: 136 | raise Exception("Invalid task '%s'. Task should be one of %s" % 137 | (name, 138 | ', '.join([task.name for task in tasks]))) 139 | if len(matching_tasks) == 1: 140 | return matching_tasks[0], args, kwargs 141 | raise Exception("Conflicting matches %s for task %s" % ( 142 | ', '.join([task.name for task in matching_tasks]), task_name 143 | )) 144 | 145 | def _parse_args(args_str): 146 | args = [] 147 | kwargs = {} 148 | if not args_str: 149 | return args, kwargs 150 | arg_parts = args_str.split(",") 151 | 152 | for i, part in enumerate(arg_parts): 153 | if "=" in part: 154 | key, value = [_str.strip() for _str in part.split("=")] 155 | if key in kwargs: 156 | raise Exception("duplicate keyword argument %s" % part) 157 | kwargs[key] = value 158 | else: 159 | if len(kwargs) > 0: 160 | raise Exception("Non keyword arg %s cannot follows a keyword arg %s" 161 | % (part, arg_parts[i - 1])) 162 | args.append(part.strip()) 163 | return args, kwargs 164 | 165 | def _run(module, logger, task, completed_tasks, from_command_line = False, args = None, kwargs = None): 166 | """ 167 | @type module: module 168 | @type logging: Logger 169 | @type task: Task 170 | @type completed_tasts: set Task 171 | @rtype: set Task 172 | @return: Updated set of completed tasks after satisfying all dependencies. 173 | """ 174 | # Satsify dependencies recursively. Maintain set of completed tasks so each 175 | # task is only performed once. 176 | for dependency in task.dependencies: 177 | completed_tasks = _run(module,logger,dependency,completed_tasks) 178 | 179 | # Perform current task, if need to. 180 | if from_command_line or task not in completed_tasks: 181 | 182 | if task.ignored: 183 | 184 | logger.info("Ignoring task \"%s\"" % task.name) 185 | 186 | else: 187 | 188 | logger.info("Starting task \"%s\"" % task.name) 189 | 190 | try: 191 | # Run task. 192 | task(*(args or []),**(kwargs or {})) 193 | except: 194 | logger.critical("Error in task \"%s\"" % task.name) 195 | logger.critical("Aborting build") 196 | raise 197 | 198 | logger.info("Completed task \"%s\"" % task.name) 199 | 200 | completed_tasks.add(task) 201 | 202 | return completed_tasks 203 | 204 | def _create_parser(): 205 | """ 206 | @rtype: argparse.ArgumentParser 207 | """ 208 | parser = argparse.ArgumentParser() 209 | parser.add_argument("tasks", help="perform specified task and all its dependencies", 210 | metavar="task", nargs = '*') 211 | parser.add_argument('-l', '--list-tasks', help = "List the tasks", 212 | action = 'store_true') 213 | parser.add_argument('-v', '--version', 214 | help = "Display the version information", 215 | action = 'store_true') 216 | parser.add_argument('-f', '--file', 217 | help = "Build file to read the tasks from. 'build.py' is default value assumed if this argument is unspecified", 218 | metavar = "file", default = "build.py") 219 | 220 | return parser 221 | 222 | # Abbreviate for convenience. 223 | #task = _TaskDecorator 224 | def task(*dependencies, **options): 225 | for i, dependency in enumerate(dependencies): 226 | if not Task.is_task(dependency): 227 | if inspect.isfunction(dependency): 228 | # Throw error specific to the most likely form of misuse. 229 | if i == 0: 230 | raise Exception("Replace use of @task with @task().") 231 | else: 232 | raise Exception("%s is not a task. Each dependency should be a task." % dependency) 233 | else: 234 | raise Exception("%s is not a task." % dependency) 235 | 236 | def decorator(fn): 237 | return Task(fn, dependencies, options) 238 | return decorator 239 | 240 | class Task(object): 241 | 242 | def __init__(self, func, dependencies, options): 243 | """ 244 | @type func: 0-ary function 245 | @type dependencies: list of Task objects 246 | """ 247 | self.func = func 248 | self.name = func.__name__ 249 | self.doc = inspect.getdoc(func) or '' 250 | self.dependencies = dependencies 251 | self.ignored = bool(options.get('ignore', False)) 252 | 253 | def show(self): 254 | return not self.name.startswith("_") 255 | 256 | def __call__(self,*args,**kwargs): 257 | self.func.__call__(*args,**kwargs) 258 | 259 | @classmethod 260 | def is_task(cls,obj): 261 | """ 262 | Returns true is an object is a build task. 263 | """ 264 | return isinstance(obj,cls) 265 | 266 | def _get_tasks(module): 267 | """ 268 | Returns all functions marked as tasks. 269 | 270 | @type module: module 271 | """ 272 | # Get all functions that are marked as task and pull out the task object 273 | # from each (name,value) pair. 274 | return set(member[1] for member in inspect.getmembers(module,Task.is_task) if member[1].show()) 275 | 276 | def _get_max_name_length(module): 277 | """ 278 | Returns the length of the longest task name. 279 | 280 | @type module: module 281 | """ 282 | return max([len(task.name) for task in _get_tasks(module)]) 283 | 284 | def _get_logger(module): 285 | """ 286 | @type module: module 287 | @rtype: logging.Logger 288 | """ 289 | 290 | # Create Logger 291 | logger = logging.getLogger(os.path.basename(module.__file__)) 292 | logger.setLevel(logging.DEBUG) 293 | 294 | # Create console handler and set level to debug 295 | ch = logging.StreamHandler() 296 | ch.setLevel(logging.DEBUG) 297 | 298 | # Create formatter 299 | formatter = logging.Formatter(_LOGGING_FORMAT) 300 | 301 | # Add formatter to ch 302 | ch.setFormatter(formatter) 303 | 304 | # Add ch to logger 305 | logger.addHandler(ch) 306 | 307 | return logger 308 | 309 | def main(): 310 | build(sys.argv[1:]) 311 | -------------------------------------------------------------------------------- /pynt/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rags/pynt/c0fbbc4c0c6c1ccbbf11a744fb62d33c78ac0ae0/pynt/tests/__init__.py -------------------------------------------------------------------------------- /pynt/tests/build_scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rags/pynt/c0fbbc4c0c6c1ccbbf11a744fb62d33c78ac0ae0/pynt/tests/build_scripts/__init__.py -------------------------------------------------------------------------------- /pynt/tests/build_scripts/annotation_misuse_1.py: -------------------------------------------------------------------------------- 1 | from pynt import task 2 | 3 | # Uses @_pynt.task form instead of @_pynt.task() form. 4 | @task 5 | def clean(): 6 | pass 7 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/annotation_misuse_2.py: -------------------------------------------------------------------------------- 1 | from pynt import task 2 | 3 | @task() 4 | def clean(): 5 | pass 6 | 7 | # Should be marked as task. 8 | def html(): 9 | pass 10 | 11 | # References a non task. 12 | @task(clean,html) 13 | def android(): 14 | pass 15 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/annotation_misuse_3.py: -------------------------------------------------------------------------------- 1 | from pynt import task 2 | 3 | @task() 4 | def clean(): 5 | pass 6 | 7 | # Referring to clean by name rather than reference. 8 | @task(1234) 9 | def html(): 10 | pass 11 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/build_with_local_import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pynt import task 4 | 5 | from test_module import do_stuff 6 | 7 | @task() 8 | def work(): 9 | do_stuff() -------------------------------------------------------------------------------- /pynt/tests/build_scripts/build_with_params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pynt import task 4 | 5 | tasks_run = [] 6 | 7 | @task() 8 | def clean(directory='/tmp'): 9 | tasks_run.append('clean[%s]' % directory) 10 | 11 | 12 | @task(clean) 13 | def html(): 14 | tasks_run.append('html') 15 | 16 | 17 | @task() 18 | def tests(*test_names): 19 | tasks_run.append('tests[%s]' % ','.join(test_names)) 20 | 21 | 22 | @task(clean) 23 | def copy_file(from_, to, fail_on_error='True'): 24 | tasks_run.append('copy_file[%s,%s,%s]' % (from_, to, fail_on_error)) 25 | 26 | 27 | @task(clean) 28 | def start_server(port='80', debug='True'): 29 | tasks_run.append('start_server[%s,%s]' % (port, debug)) 30 | 31 | @task(ignore=True) 32 | def ignored(file, contents): 33 | tasks_run.append('append_to_file[%s,%s]' % (file, contents)) 34 | 35 | @task(clean, ignored) 36 | def append_to_file(file, contents): 37 | tasks_run.append('append_to_file[%s,%s]' % (file, contents)) 38 | 39 | 40 | @task(ignored) 41 | def echo(*args,**kwargs): 42 | args_str = [] 43 | if args: 44 | args_str.append(','.join(args)) 45 | if kwargs: 46 | args_str.append(','.join("%s=%s" % (kw, kwargs[kw]) for kw in sorted(kwargs))) 47 | 48 | tasks_run.append('echo[%s]' % ','.join(args_str)) 49 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/default_task_and_import_dependencies.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pynt import task 4 | 5 | from pynt.tests.build_scripts.simple import * 6 | from pynt.tests.build_scripts import build_with_params 7 | 8 | tasks_run = [] 9 | 10 | @task() 11 | def local_task(): 12 | tasks_run.append('local_task') 13 | 14 | 15 | @task(clean, build_with_params.html, local_task) 16 | def task_with_imported_dependencies(): 17 | tasks_run.append('task_with_imported_dependencies') 18 | 19 | __DEFAULT__ = task_with_imported_dependencies -------------------------------------------------------------------------------- /pynt/tests/build_scripts/dependencies.py: -------------------------------------------------------------------------------- 1 | from pynt import task 2 | 3 | 4 | @task() 5 | def clean(): 6 | """Clean build directory.""" 7 | 8 | print("clean") 9 | 10 | @task(clean) 11 | def html(): 12 | """Generate HTML.""" 13 | print("html") 14 | 15 | @task(clean) 16 | def images(): 17 | """Prepare images.""" 18 | 19 | print("images") 20 | 21 | @task() 22 | def _common_private_task(): 23 | """Package iOS app.""" 24 | print("os agnostic task") 25 | 26 | @task(clean, html, images, _common_private_task) 27 | def android(): 28 | """Package Android app.""" 29 | print("android") 30 | 31 | @task(clean, html, images, _common_private_task) 32 | def ios(): 33 | """Package iOS app.""" 34 | print("ios") 35 | 36 | 37 | def some_utility_method(): 38 | """Some utility method.""" 39 | print("some utility method") 40 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/options.py: -------------------------------------------------------------------------------- 1 | from pynt import task 2 | 3 | tasks_run = [] 4 | 5 | @task() 6 | def clean(): 7 | tasks_run.append("clean") 8 | 9 | @task(clean) 10 | def html(): 11 | 'Generate HTML.' 12 | tasks_run.append("html") 13 | 14 | @task(clean, ignore=True) 15 | def images(): 16 | """Prepare images. 17 | 18 | Should be ignored.""" 19 | 20 | raise Exception("This task should have been ignored.") 21 | 22 | @task(clean,html,images) 23 | def android(): 24 | "Package Android app." 25 | 26 | tasks_run.append('android') 27 | 28 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/runtime_error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Build script with a runtime error. 3 | """ 4 | from pynt import task 5 | 6 | 7 | @task() 8 | def images(): 9 | """Prepare images. Raises IOError.""" 10 | global ran_images 11 | ran_images = True 12 | raise IOError 13 | 14 | @task(images) 15 | def android(): 16 | """Package Android app.""" 17 | global ran_android 18 | print("android") 19 | ran_android = True 20 | 21 | -------------------------------------------------------------------------------- /pynt/tests/build_scripts/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pynt import task 4 | 5 | 6 | @task() 7 | def clean(): 8 | """Clean build directory.""" 9 | 10 | print("clean") 11 | 12 | @task() 13 | def html(): 14 | """Generate HTML.""" 15 | 16 | print("html") 17 | 18 | @task() 19 | def images(): 20 | """Prepare images.""" 21 | 22 | print("images") 23 | 24 | @task() 25 | def android(): 26 | """Package Android app.""" 27 | 28 | print("android") 29 | 30 | @task() 31 | def ios(): 32 | """Package iOS app.""" 33 | 34 | print("ios") 35 | 36 | def some_utility_method(): 37 | """Some utility method.""" 38 | 39 | print("some utility method") 40 | 41 | __DEFAULT__ = ios -------------------------------------------------------------------------------- /pynt/tests/build_scripts/test_module.py: -------------------------------------------------------------------------------- 1 | 2 | def do_stuff(): 3 | pass -------------------------------------------------------------------------------- /pynt/tests/test_pynt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import re 3 | from pynt import _pynt, main 4 | import sys 5 | if sys.version.startswith("3."): 6 | from io import StringIO as SOut 7 | else: 8 | from StringIO import StringIO as SOut 9 | 10 | import os 11 | from os import path 12 | import imp 13 | 14 | def fpath(mod): 15 | return path.splitext(mod.__file__)[0] + '.py' 16 | 17 | 18 | def simulate_dynamic_module_load(mod): 19 | file_path = fpath(mod) 20 | 21 | #sys.path.append(path.abspath(script_dir)) 22 | 23 | module_name, suffix = path.splitext(path.basename(file_path)) 24 | description = (suffix, 'r', imp.PY_SOURCE) 25 | 26 | with open(file_path, 'r') as scriptfile: 27 | return imp.load_module(module_name, scriptfile, file_path, description) 28 | 29 | def reset_build_file(mod): 30 | mod.tasks_run = [] 31 | def build(mod, params=None, init_mod = reset_build_file): 32 | dynamically_loaded_mod = simulate_dynamic_module_load(mod) 33 | dynamically_loaded_mod.tasks_run = [] 34 | sys.argv = ['pynt', '-f', fpath(mod)] + (params or []) 35 | main() 36 | return dynamically_loaded_mod 37 | 38 | class TestParseArgs: 39 | def test_parsing_commandline(self): 40 | args = _pynt._create_parser().parse_args(['-f', "foo.py", "task1", "task2"]) 41 | assert "foo.py" == args.file 42 | assert not args.list_tasks 43 | assert ['task1', 'task2'] == args.tasks 44 | 45 | def test_parsing_commandline_help(self): 46 | assert _pynt._create_parser().parse_args(["-l"]).list_tasks 47 | assert _pynt._create_parser().parse_args([ "--list-tasks"]).list_tasks 48 | 49 | def test_parsing_commandline_build_file(self): 50 | assert "some_file" == _pynt._create_parser().parse_args(["-f", "some_file"]).file 51 | assert "build.py" == _pynt._create_parser().parse_args([]).file 52 | assert "/foo/bar" == _pynt._create_parser().parse_args( 53 | ["--file", "/foo/bar"]).file 54 | 55 | 56 | with pytest.raises(SystemExit): 57 | _pynt._create_parser().parse_args(["--file"]) 58 | with pytest.raises(SystemExit): 59 | _pynt._create_parser().parse_args(["-f"]) 60 | 61 | class TestBuildSimple: 62 | 63 | def test_get_tasks(self): 64 | from .build_scripts import simple 65 | ts = _pynt._get_tasks(simple) 66 | assert len(ts) == 5 67 | 68 | class TestBuildWithDependencies: 69 | 70 | def test_get_tasks(self): 71 | from .build_scripts import dependencies 72 | tasks = _pynt._get_tasks(dependencies)#private tasks are not in this list 73 | assert len(tasks) == 5 74 | assert 4 == len([task for task in tasks if task.name == 'android'][0].dependencies) 75 | assert 4 == len([task for task in tasks if task.name == 'ios'][0].dependencies) 76 | 77 | def test_dependencies_for_imported(self): 78 | from .build_scripts import default_task_and_import_dependencies 79 | tasks = _pynt._get_tasks(default_task_and_import_dependencies) 80 | assert 7 == len(tasks) 81 | assert [task for task in tasks if task.name == 'clean'] 82 | assert [task for task in tasks if task.name == 'local_task'] 83 | assert [task for task in tasks if task.name == 'android'] 84 | assert 3 == len([task for task in tasks 85 | if task.name == 'task_with_imported_dependencies'][0].dependencies) 86 | 87 | 88 | def test_dependencies_that_are_imported_e2e(self): 89 | from .build_scripts import default_task_and_import_dependencies 90 | def mod_init(mod): 91 | mod.tasks_run = [] 92 | mod.build_with_params.tasks_run = [] 93 | module = build(default_task_and_import_dependencies, 94 | ["task_with_imported_dependencies"], init_mod = mod_init) 95 | assert module.tasks_run == ['local_task', 'task_with_imported_dependencies'] 96 | assert module.build_with_params.tasks_run == ['clean[/tmp]', 'html'] 97 | 98 | class TestDecorationValidation: 99 | 100 | def test_task_without_braces(self): 101 | with pytest.raises(Exception) as exc: 102 | from .build_scripts import annotation_misuse_1 103 | assert 'Replace use of @task with @task().' in str(exc.value) 104 | 105 | def test_dependency_not_a_task(self): 106 | with pytest.raises(Exception) as exc: 107 | from .build_scripts import annotation_misuse_2 108 | assert re.findall('function html.* is not a task.', str(exc.value)) 109 | 110 | def test_dependency_not_a_function(self): 111 | with pytest.raises(Exception) as exc: 112 | from .build_scripts import annotation_misuse_3 113 | assert '1234 is not a task.' in str(exc.value) 114 | 115 | 116 | import contextlib 117 | @contextlib.contextmanager 118 | def mock_stdout(): 119 | oldout, olderr = sys.stdout, sys.stderr 120 | try: 121 | out = [SOut(), SOut()] 122 | sys.stdout, sys.stderr = out 123 | yield out 124 | finally: 125 | sys.stdout, sys.stderr = oldout, olderr 126 | out[0] = out[0].getvalue() 127 | out[1] = out[1].getvalue() 128 | 129 | 130 | class TestOptions: 131 | 132 | @pytest.fixture 133 | def module(self): 134 | from .build_scripts import options as module 135 | self.docs = {'clean': '', 'html': 'Generate HTML.', 136 | 'images': '''Prepare images.\n\nShould be ignored.''', 137 | 'android': 'Package Android app.'} 138 | return module 139 | 140 | def test_ignore_tasks(self, module): 141 | module = build(module,["android"]) 142 | assert ['clean', 'html', 'android'] == module.tasks_run 143 | 144 | def test_docs(self, module): 145 | tasks = _pynt._get_tasks(module) 146 | assert 4 == len(tasks) 147 | 148 | for task_ in tasks: 149 | assert task_.name in self.docs 150 | assert self.docs[task_.name] == task_.doc 151 | 152 | @pytest.mark.parametrize('args', [['-l'], ['--list-tasks'], []]) 153 | def test_list_docs(self, module, args): 154 | with mock_stdout() as out: 155 | build(module,args) 156 | stdout = out[0] 157 | tasks = _pynt._get_tasks(module) 158 | for task in tasks: 159 | if task.ignored: 160 | assert re.findall('%s\s+%s\s+%s' % (task.name,"\[Ignored\]", task.doc), stdout) 161 | else: 162 | assert re.findall('%s\s+%s' % (task.name, task.doc), stdout) 163 | 164 | 165 | 166 | class TestRuntimeError: 167 | 168 | def test_stop_on_exception(self): 169 | from .build_scripts import runtime_error as re 170 | with pytest.raises(IOError): 171 | build(re,["android"]) 172 | mod = simulate_dynamic_module_load(re) 173 | assert mod.ran_images 174 | assert not hasattr(mod, 'ran_android') 175 | 176 | def test_exception_on_invalid_task_name(self): 177 | from .build_scripts import build_with_params 178 | with pytest.raises(Exception) as exc: 179 | build(build_with_params,["doesnt_exist"]) 180 | 181 | assert 'task should be one of append_to_file, clean' \ 182 | ', copy_file, echo, html, start_server, tests' in str(exc.value) 183 | 184 | 185 | class TestPartialTaskNames: 186 | def setup_method(self,method): 187 | from .build_scripts import build_with_params 188 | self._mod = build_with_params 189 | 190 | 191 | def test_with_partial_name(self): 192 | mod = build(self._mod, ["cl"]) 193 | assert ['clean[/tmp]'] == mod.tasks_run 194 | 195 | def test_with_partial_name_and_dependencies(self): 196 | mod = build(self._mod, ["htm"]) 197 | assert ['clean[/tmp]','html'] == mod.tasks_run 198 | 199 | def test_exception_on_conflicting_partial_names(self): 200 | with pytest.raises(Exception) as exc: 201 | build(self._mod, ["c"]) 202 | assert ('Conflicting matches clean, copy_file for task c' in str(exc.value) or 203 | 'Conflicting matches copy_file, clean for task c' in str(exc.value)) 204 | 205 | 206 | 207 | class TestDefaultTask: 208 | def test_simple_default_task(self): 209 | from .build_scripts import simple 210 | assert _pynt._run_default_task(simple) #returns false if no default task 211 | 212 | def test_module_with_defaults_which_imports_other_files_with_defaults(self): 213 | from .build_scripts import default_task_and_import_dependencies 214 | mod = build(default_task_and_import_dependencies) 215 | assert 'task_with_imported_dependencies' in mod.tasks_run 216 | 217 | 218 | 219 | class TestMultipleTasks: 220 | def setup_method(self,method): 221 | from .build_scripts import build_with_params 222 | self._mod = build_with_params 223 | 224 | def test_dependency_is_run_only_once_unless_explicitly_invoked_again(self): 225 | mod = build(self._mod, ["clean", "html", 'tests', "clean"]) 226 | assert ['clean[/tmp]', "html", "tests[]", "clean[/tmp]"] == mod.tasks_run 227 | 228 | def test_multiple_partial_names(self): 229 | assert ['clean[/tmp]', "html"] == build(self._mod, ["cl", "htm"]).tasks_run 230 | 231 | 232 | 233 | class TesttaskArguments: 234 | def setup_method(self,method): 235 | from .build_scripts import build_with_params 236 | self._mod = build_with_params 237 | self._mod.tasks_run = [] 238 | 239 | def test_passing_optional_params_with_dependencies(self): 240 | mod = build(self._mod, ["clean[~/project/foo]", 241 | 'append_to_file[/foo/bar,ABCDEF]', 242 | "copy_file[/foo/bar,/foo/blah,False]", 243 | 'start_server[8080]']) 244 | assert ["clean[~/project/foo]", 'append_to_file[/foo/bar,ABCDEF]', 245 | "copy_file[/foo/bar,/foo/blah,False]", 'start_server[8080,True]' 246 | ] == mod.tasks_run 247 | 248 | def test_invoking_varargs_task(self): 249 | mod = build(self._mod, ['tests[test1,test2,test3]']) 250 | assert ['tests[test1,test2,test3]'] == mod.tasks_run 251 | 252 | def test_partial_name_with_args(self): 253 | 254 | mod = build(self._mod, ['co[foo,bar]','star']) 255 | assert ['clean[/tmp]','copy_file[foo,bar,True]', 'start_server[80,True]' 256 | ] == mod.tasks_run 257 | 258 | 259 | def test_passing_keyword_args(self): 260 | mod = build(self._mod, ['co[to=bar,from_=foo]','star[80,debug=False]', 'echo[foo=bar,blah=123]']) 261 | 262 | assert ['clean[/tmp]','copy_file[foo,bar,True]', 263 | 'start_server[80,False]', 264 | 'echo[blah=123,foo=bar]'] == mod.tasks_run 265 | 266 | 267 | 268 | def test_passing_varargs_and_keyword_args(self): 269 | assert (['echo[1,2,3,some_str,111=333,bar=123.3,foo=xyz]'] 270 | == 271 | build(self._mod, 272 | ['echo[1,2,3,some_str,111=333,foo=xyz,bar=123.3]'] 273 | ).tasks_run) 274 | 275 | def test_validate_keyword_arguments_always_after_args(self): 276 | with pytest.raises(Exception) as exc: 277 | build(self._mod, ['echo[bar=123.3,foo]']) 278 | assert "Non keyword arg foo cannot follows" \ 279 | " a keyword arg bar=123.3" in str(exc.value) 280 | 281 | with pytest.raises(Exception) as exc: 282 | build(self._mod, ['copy[from_=/foo,/foo1]']) 283 | 284 | assert "Non keyword arg /foo1 cannot follows" \ 285 | " a keyword arg from_=/foo" in str(exc.value) 286 | 287 | 288 | 289 | def test_invalid_number_of_args(self): 290 | with pytest.raises(TypeError) as exc: 291 | build(self._mod, ['append[1,2,3]']) 292 | print(str(exc.value)) 293 | assert re.findall('takes .*2 .*arguments', str(exc.value)) 294 | 295 | 296 | def test_invalid_names_for_kwargs(self): 297 | with pytest.raises(TypeError) as exc: 298 | build(self._mod, ['copy[1=2,to=bar]']) 299 | assert "got an unexpected keyword argument '1'" in str(exc.value) 300 | 301 | with pytest.raises(TypeError) as exc: 302 | build(self._mod, ['copy[bar123=2]']) 303 | assert "got an unexpected keyword argument 'bar123'" in str(exc.value) 304 | 305 | 306 | class TesttaskLocalImports: 307 | def setup_method(self,method): 308 | from .build_scripts import build_with_local_import 309 | self._mod = build_with_local_import 310 | self._mod.tasks_run = [] 311 | 312 | def test_load_build_with_local_import_does_not_fail(self): 313 | mod = build(self._mod, ["work"]) 314 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import pynt 3 | setup( 4 | name="pynt", 5 | version= pynt.__version__, 6 | author="Raghunandan Rao", 7 | author_email="r.raghunandan@gmail.com", 8 | url= pynt.__contact__, 9 | packages=["pynt"], 10 | entry_points = {'console_scripts': ['pynt=pynt:main']}, 11 | license="MIT License", 12 | description="Lightweight Python Build Tool.", 13 | long_description=open("README.rst").read()+"\n"+open("CHANGES.rst").read() 14 | ) 15 | --------------------------------------------------------------------------------