├── .gitattributes ├── .gitignore ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── pyzen ├── __init__.py ├── _colorama │ ├── COPYRIGHT │ ├── README.txt │ ├── __init__.py │ ├── ansi.py │ ├── ansitowin32.py │ ├── initialise.py │ ├── win32.py │ └── winterm.py ├── core.py ├── discover.py ├── distutils.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── zen.py ├── models.py ├── runner.py ├── simple.py └── ui │ ├── __init__.py │ ├── base.py │ ├── img │ ├── README │ ├── green.ico │ ├── green.png │ ├── logo.ico │ ├── logo.png │ ├── logo.svg │ ├── red.ico │ └── red.png │ ├── linux.py │ ├── osx.py │ ├── sublime.py │ └── win32 │ ├── __init__.py │ ├── types.py │ ├── win32con.py │ └── wrappers.py ├── setup.cfg └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | demo 4 | .DS_Store 5 | dist 6 | build 7 | docs/_build 8 | 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pyzen/ui/img * 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyZen 2 | ===== 3 | 4 | PyZen is a continuous test runner for paranoid developers. As long as the 5 | script is running, it will monitor for changes in your code and re-run your 6 | test suite when needed. There are frontends for multiple frameworks as well 7 | as several notification UIs. 8 | 9 | Features 10 | -------- 11 | 12 | * Monitor source code and run tests on change 13 | * OS-specific async notification UI 14 | * Colored test output 15 | 16 | Installation 17 | ------------ 18 | 19 | PyZen can be installed from PyPI using easy_install:: 20 | 21 | $ easy_install PyZen 22 | 23 | or pip:: 24 | 25 | $ pip install PyZen 26 | 27 | 28 | Frontends 29 | --------- 30 | 31 | PyZen provides multiple frontends to collect tests and run the continuous 32 | tester. 33 | 34 | Django 35 | ~~~~~~ 36 | 37 | To setup PyZen under Django add ``pyzen`` to your ``INSTALLED_APPS`` setting. 38 | The run ``manage.py zen`` to start the tester process. You can give an 39 | application label or test name using the same format as the built-in ``test`` 40 | command. 41 | 42 | Flask 43 | ~~~~~ 44 | 45 | The Flask frontend is maintained as separate package, `Flask-Zen`_. 46 | 47 | .. _Flask-Zen: http://pypi.python.org/pypi/Flask-Zen 48 | 49 | Distutils 50 | ~~~~~~~~~ 51 | 52 | The PyZen package provides a distutils command ``zen`` that will run the test 53 | suite configured in setup.py under PyZen. Run ``setup.py zen`` to start the 54 | tester process. 55 | 56 | Standalone 57 | ~~~~~~~~~~ 58 | 59 | The ``pyzen`` script provides a wrapper to run any test script under PyZen. 60 | Run ``pyzen yourscript.py arg1 arg2 ...`` to start the tester process. No 61 | configuration options are available at this time. 62 | 63 | Options 64 | ~~~~~~~ 65 | 66 | ``--nocolor`` : *flag, default: False* 67 | Disable colored output. 68 | 69 | ``-u``, ``--ui`` : *default: autodetect* 70 | Force the use of a specific UI module. Available options are ``win32``, 71 | ``osx``, ``linux``, and ``none``. 72 | 73 | UIs 74 | --- 75 | 76 | PyZen provides a UI to indicate the current test status after each run, even 77 | if the console is in the background. In addition to the three 78 | platform-specific interfaces, the ``none`` interface will disable this 79 | display. See the frontend documentation for details, but most frontends offer 80 | a ``--ui`` option to override the autodetection. 81 | 82 | Win32 83 | ~~~~~ 84 | 85 | The default UI on Windows is a systray icon indicating the current test status 86 | and balloon notifications after each run. This UI is tested on Windows XP and 87 | higher, though it may work with Windows 2000. 88 | 89 | OS X 90 | ~~~~ 91 | 92 | The default UI on OS X uses Growl via AppleScript. A Growl notification is 93 | posted after each test run. 94 | 95 | Linux 96 | ~~~~~ 97 | 98 | The default UI on Linux uses libnotify via the pynotify library. This is 99 | installed by default on current versions of Ubuntu. If pynotify is not found, 100 | the interface will be disabled. 101 | 102 | Test Runner 103 | ----------- 104 | 105 | By default PyZen enhances the test output with color. It is known to work on 106 | both Windows and \*nix systems. Most frontends have a ``--nocolor`` option to 107 | disable it if needed. 108 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyZen.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyZen.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PyZen" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyZen" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyZen documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Aug 14 18:14:46 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'PyZen' 44 | copyright = u'2010, Noah Kantrowitz' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'PyZendoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'PyZen.tex', u'PyZen Documentation', 182 | u'Noah Kantrowitz', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'pyzen', u'PyZen Documentation', 215 | [u'Noah Kantrowitz'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | PyZen 2 | ===== 3 | 4 | PyZen is a continuous test runner for paranoid developers. As long as the 5 | script is running, it will monitor for changes in your code and re-run your 6 | test suite when needed. There are frontends for multiple frameworks as well 7 | as several notification UIs. 8 | 9 | Features 10 | -------- 11 | 12 | * Monitor source code and run tests on change 13 | * OS-specific async notification UI 14 | * Colored test output 15 | 16 | Installation 17 | ------------ 18 | 19 | PyZen can be installed from PyPI using easy_install:: 20 | 21 | $ easy_install PyZen 22 | 23 | or pip:: 24 | 25 | $ pip install PyZen 26 | 27 | 28 | Frontends 29 | --------- 30 | 31 | PyZen provides multiple frontends to collect tests and run the continuous 32 | tester. 33 | 34 | Django 35 | ~~~~~~ 36 | 37 | To setup PyZen under Django add ``pyzen`` to your ``INSTALLED_APPS`` setting. 38 | The run ``manage.py zen`` to start the tester process. You can give an 39 | application label or test name using the same format as the built-in ``test`` 40 | command. 41 | 42 | Flask 43 | ~~~~~ 44 | 45 | The Flask frontend is maintained as separate package, `Flask-Zen`_. 46 | 47 | .. _Flask-Zen: http://pypi.python.org/pypi/Flask-Zen 48 | 49 | Distutils 50 | ~~~~~~~~~ 51 | 52 | The PyZen package provides a distutils command ``zen`` that will run the test 53 | suite configured in setup.py under PyZen. Run ``setup.py zen`` to start the 54 | tester process. 55 | 56 | Standalone 57 | ~~~~~~~~~~ 58 | 59 | The ``pyzen`` script provides a wrapper to run any test script under PyZen. 60 | Run ``pyzen yourscript.py arg1 arg2 ...`` to start the tester process. No 61 | configuration options are available at this time. 62 | 63 | Options 64 | ~~~~~~~ 65 | 66 | ``--nocolor`` : *flag, default: False* 67 | Disable colored output. 68 | 69 | ``-u``, ``--ui`` : *default: autodetect* 70 | Force the use of a specific UI module. Available options are ``win32``, 71 | ``osx``, ``linux``, and ``none``. 72 | 73 | UIs 74 | --- 75 | 76 | PyZen provides a UI to indicate the current test status after each run, even 77 | if the console is in the background. In addition to the three 78 | platform-specific interfaces, the ``none`` interface will disable this 79 | display. See the frontend documentation for details, but most frontends offer 80 | a ``--ui`` option to override the autodetection. 81 | 82 | Win32 83 | ~~~~~ 84 | 85 | The default UI on Windows is a systray icon indicating the current test status 86 | and balloon notifications after each run. This UI is tested on Windows XP and 87 | higher, though it may work with Windows 2000. 88 | 89 | OS X 90 | ~~~~ 91 | 92 | The default UI on OS X uses Growl via AppleScript. A Growl notification is 93 | posted after each test run. 94 | 95 | Linux 96 | ~~~~~ 97 | 98 | The default UI on Linux uses libnotify via the pynotify library. This is 99 | installed by default on current versions of Ubuntu. If pynotify is not found, 100 | the interface will be disabled. 101 | 102 | Test Runner 103 | ----------- 104 | 105 | By default PyZen enhances the test output with color. It is known to work on 106 | both Windows and \*nix systems. Most frontends have a ``--nocolor`` option to 107 | disable it if needed. -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyZen.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyZen.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /pyzen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/__init__.py -------------------------------------------------------------------------------- /pyzen/_colorama/COPYRIGHT: -------------------------------------------------------------------------------- 1 | This folder is a copy of the colorama library and is copyrighted by Jonathan 2 | Hartley . It is distributed under the BSD license. -------------------------------------------------------------------------------- /pyzen/_colorama/README.txt: -------------------------------------------------------------------------------- 1 | Download and docs: 2 | http://pypi.python.org/pypi/colorama 3 | Development: 4 | http://code.google.com/p/colorama 5 | 6 | Description 7 | =========== 8 | 9 | Makes ANSI escape character sequences for producing colored terminal text work 10 | under MS Windows. 11 | 12 | ANSI escape character sequences have long been used to produce colored terminal 13 | text on Unix and Macs. Colorama makes this work on Windows, too. It also 14 | provides some shortcuts to help generate ANSI sequences, and works fine in 15 | conjunction with any other ANSI sequence generation library, such as Termcolor 16 | (http://pypi.python.org/pypi/termcolor.) 17 | 18 | This has the upshot of providing a simple cross-platform API for printing 19 | colored terminal text from Python, and has the happy side-effect that existing 20 | applications or libraries which use ANSI sequences to produce colored output on 21 | Linux or Macs can now also work on Windows, simply by calling 22 | ``colorama.init()``. 23 | 24 | A demo script in the source code repository prints some colored text using 25 | ANSI sequences. Compare its output under Gnome-terminal's built in ANSI 26 | handling, versus on Windows Command-Prompt using Colorama: 27 | 28 | .. image:: http://colorama.googlecode.com/hg/screenshots/ubuntu-demo.png 29 | :width: 661 30 | :height: 357 31 | :alt: ANSI sequences on Ubuntu under gnome-terminal. 32 | 33 | .. image:: http://colorama.googlecode.com/hg/screenshots/windows-demo.png 34 | :width: 668 35 | :height: 325 36 | :alt: Same ANSI sequences on Windows, using Colorama. 37 | 38 | These screengrabs make it clear that Colorama on Windows does not support 39 | ANSI 'dim text': it looks the same as 'normal text'. 40 | 41 | 42 | Dependencies 43 | ============ 44 | 45 | None, other than Python. Tested on Python 2.5.5, 2.6.5, 2.7 & 3.1.2. 46 | 47 | 48 | Usage 49 | ===== 50 | 51 | Initialisation 52 | -------------- 53 | 54 | Applications should initialise Colorama using:: 55 | 56 | from colorama import init 57 | init() 58 | 59 | If you are on Windows, the call to ``init()`` will start filtering ANSI escape 60 | sequences out of any text sent to stdout or stderr, and will replace them with 61 | equivalent Win32 calls. 62 | 63 | Calling ``init()`` has no effect on other platforms (unless you request other 64 | optional functionality, see keyword args below.) The intention is that 65 | applications can call ``init()`` unconditionally on all platforms, after which 66 | ANSI output should just work. 67 | 68 | 69 | Colored Output 70 | -------------- 71 | 72 | Cross-platform printing of colored text can then be done using Colorama's 73 | constant shorthand for ANSI escape sequences:: 74 | 75 | from colorama import Fore, Back, Style 76 | print Fore.RED + 'some red text' 77 | print Back.GREEN + and with a green background' 78 | print Style.DIM + 'and in dim text' 79 | print + Fore.RESET + Back.RESET + Style.RESET_ALL 80 | print 'back to normal now' 81 | 82 | or simply by manually printing ANSI sequences from your own code:: 83 | 84 | print '/033[31m' + 'some red text' 85 | print '/033[30m' # and reset to default color 86 | 87 | or Colorama can be used happily in conjunction with existing ANSI libraries 88 | such as Termcolor:: 89 | 90 | from colorama import init 91 | from termcolor import colored 92 | 93 | # use Colorama to make Termcolor work on Windows too 94 | init() 95 | 96 | # then use Termcolor for all colored text output 97 | print colored('Hello, World!', 'green', 'on_red') 98 | 99 | Available formatting constants are:: 100 | 101 | Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. 102 | Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. 103 | Style: DIM, NORMAL, BRIGHT, RESET_ALL 104 | 105 | Style.RESET_ALL resets foreground, background and brightness. Colorama will 106 | perform this reset automatically on program exit. 107 | 108 | 109 | Init Keyword Args 110 | ----------------- 111 | 112 | ``init()`` accepts some kwargs to override default behaviour. 113 | 114 | init(autoreset=False): 115 | If you find yourself repeatedly sending reset sequences to turn off color 116 | changes at the end of every print, then ``init(autoreset=True)`` will 117 | automate that:: 118 | 119 | from colorama import init 120 | init(autoreset=True) 121 | print Fore.RED + 'some red text' 122 | print 'automatically back to default color again' 123 | 124 | init(strip=None): 125 | Pass ``True`` or ``False`` to override whether ansi codes should be 126 | stripped from the output. The default behaviour is to strip if on Windows. 127 | 128 | init(convert=None): 129 | Pass ``True`` or ``False`` to override whether to convert ansi codes in the 130 | output into win32 calls. The default behaviour is to convert if on Windows 131 | and output is to a tty (terminal). 132 | 133 | init(wrap=True): 134 | On Windows, colorama works by replacing ``sys.stdout`` and ``sys.stderr`` 135 | with proxy objects, which override the .write() method to do their work. If 136 | this wrapping causes you problems, then this can be disabled by passing 137 | ``init(wrap=False)``. The default behaviour is to wrap if autoreset or 138 | strip or convert are True. 139 | 140 | When wrapping is disabled, colored printing on non-Windows platforms will 141 | continue to work as normal. To do cross-platform colored output, you can 142 | use Colorama's ``AnsiToWin32`` proxy directly:: 143 | 144 | from colorama import init, AnsiToWin32 145 | init(wrap=False) 146 | stream = AnsiToWin32(sys.stderr).stream 147 | print >>stream, Fore.BLUE + 'blue text on stderr' 148 | 149 | 150 | Status & Known Problems 151 | ======================= 152 | 153 | Feature complete as far as colored text goes, but still finding bugs and 154 | occasionally making small changes to the API (such as new keyword arguments 155 | to ``init()``). 156 | 157 | Only tested on WinXP (CMD, Console2) and Ubuntu (gnome-terminal, xterm). Much 158 | obliged if anyone can let me know how it fares elsewhere, in particular on 159 | Macs. 160 | 161 | I'd like to add the ability to handle ANSI codes which position the text cursor 162 | and clear the terminal. 163 | 164 | See outstanding issues and wishlist at: 165 | http://code.google.com/p/colorama/issues/list 166 | 167 | If anything doesn't work for you, or doesn't do what you expected or hoped for, 168 | I'd *love* to hear about it on that issues list. 169 | 170 | 171 | Recognised ANSI Sequences 172 | ========================= 173 | 174 | ANSI sequences generally take the form: 175 | 176 | ESC [ ; ... 177 | 178 | Where is an integer, and is a single letter. Zero or more 179 | params are passed to a . If no params are passed, it is generally 180 | synonymous with passing a single zero. No spaces exist in the sequence, they 181 | have just been inserted here to make it easy to read. 182 | 183 | The only ANSI sequences that colorama converts into win32 calls are:: 184 | 185 | ESC [ 0 m # reset all (colors and brightness) 186 | ESC [ 1 m # bright 187 | ESC [ 2 m # dim (looks same as normal brightness) 188 | ESC [ 22 m # normal brightness 189 | 190 | # FOREGROUND: 191 | ESC [ 30 m # black 192 | ESC [ 31 m # red 193 | ESC [ 32 m # green 194 | ESC [ 33 m # yellow 195 | ESC [ 34 m # blue 196 | ESC [ 35 m # magenta 197 | ESC [ 36 m # cyan 198 | ESC [ 37 m # white 199 | ESC [ 39 m # reset 200 | 201 | # BACKGROUND 202 | ESC [ 40 m # black 203 | ESC [ 41 m # red 204 | ESC [ 42 m # green 205 | ESC [ 43 m # yellow 206 | ESC [ 44 m # blue 207 | ESC [ 45 m # magenta 208 | ESC [ 46 m # cyan 209 | ESC [ 47 m # white 210 | ESC [ 49 m # reset 211 | 212 | Multiple numeric params to the 'm' command can be combined into a single 213 | sequence, eg:: 214 | 215 | ESC [ 36 ; 45 ; 1 m # bright cyan text on magenta background 216 | 217 | All other ANSI sequences of the form ``ESC [ ; ... `` 218 | are silently stripped from the output on Windows. 219 | 220 | Any other form of ANSI sequence, such as single-character codes or alternative 221 | initial characters, are not recognised nor stripped. 222 | 223 | 224 | Development 225 | =========== 226 | 227 | Running tests requires: 228 | 229 | - Michael Foord's 'mock' module to be installed. 230 | - Either to be run under Python2.7 or 3.1 stdlib unittest, or to have Michael 231 | Foord's 'unittest2' module to be installed. 232 | 233 | unittest2 test discovery doesn't work for colorama, so I use 'nose':: 234 | 235 | nosetests -s 236 | 237 | The -s is required because 'nosetests' otherwise applies a proxy of its own to 238 | stdout, which confuses the unit tests. 239 | 240 | 241 | Thanks 242 | ====== 243 | Roger Binns, for many suggestions, valuable feedback, & bug reports. 244 | Tim Golden for thought and much appreciated feedback on the initial idea. 245 | 246 | 247 | Changes 248 | ======= 249 | 250 | 0.1.17 251 | Prevent printing of garbage ANSI codes upon installing with pip 252 | 0.1.16 253 | Re-upload to fix previous error. Make clean now removes old MANIFEST. 254 | 0.1.15 255 | Completely broken. Distribution was empty due to leftover invalid MANIFEST 256 | file from building on a different platform. 257 | Fix python3 incompatibility kindly reported by G |uumlaut| nter Kolousek 258 | 0.1.14 259 | Fix hard-coded reset to white-on-black colors. Fore.RESET, Back.RESET 260 | and Style.RESET_ALL now revert to the colors as they were when init() 261 | was called. Some lessons hopefully learned about testing prior to release. 262 | 0.1.13 263 | Completely broken: barfed when installed using pip. 264 | 0.1.12 265 | Completely broken: contained no source code. double oops. 266 | 0.1.11 267 | Completely broken: fatal import errors on Ubuntu. oops. 268 | 0.1.10 269 | Stop emulating 'bright' text with bright backgrounds. 270 | Display 'normal' text using win32 normal foreground instead of bright. 271 | Drop support for 'dim' text. 272 | 0.1.9 273 | Fix incompatibility with Python 2.5 and earlier. 274 | Remove setup.py dependency on setuptools, now uses stdlib distutils. 275 | 0.1.8 276 | Fix ghastly errors all over the place on Ubuntu. 277 | Add init kwargs 'convert' and 'strip', which supercede the old 'wrap'. 278 | 0.1.7 279 | Python 3 compatible. 280 | Fix: Now strips ansi on windows without necessarily converting it to 281 | win32 calls (eg. if output is not a tty.) 282 | Fix: Flaky interaction of interleaved ansi sent to stdout and stderr. 283 | Improved demo.sh (hg checkout only.) 284 | 0.1.6 285 | Fix ansi sequences with no params now default to parmlist of [0]. 286 | Fix flaky behaviour of autoreset and reset_all atexit. 287 | Fix stacking of repeated atexit calls - now just called once. 288 | Fix ghastly import problems while running tests. 289 | 'demo.py' (hg checkout only) now demonstrates autoreset and reset atexit. 290 | Provide colorama.VERSION, used by setup.py. 291 | Tests defanged so they no longer actually change terminal color when run. 292 | 0.1.5 293 | Now works on Ubuntu. 294 | 0.1.4 295 | Implemented RESET_ALL on application exit 296 | 0.1.3 297 | Implemented init(wrap=False) 298 | 0.1.2 299 | Implemented init(autoreset=True) 300 | 0.1.1 301 | Minor tidy 302 | 0.1 303 | Works on Windows for foreground color, background color, bright or dim 304 | 305 | .. |uumlaut| unicode:: U+00FC .. u with umlaut 306 | :trim: 307 | 308 | -------------------------------------------------------------------------------- /pyzen/_colorama/__init__.py: -------------------------------------------------------------------------------- 1 | from .initialise import init 2 | from .ansi import Fore, Back, Style 3 | from .ansitowin32 import AnsiToWin32 4 | 5 | VERSION = '0.1.18' 6 | 7 | -------------------------------------------------------------------------------- /pyzen/_colorama/ansi.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module generates ANSI character codes to printing colors to terminals. 3 | See: http://en.wikipedia.org/wiki/ANSI_escape_code 4 | ''' 5 | 6 | CSI = '\033[' 7 | 8 | def code_to_chars(code): 9 | return CSI + str(code) + 'm' 10 | 11 | class AnsiCodes(object): 12 | def __init__(self, codes): 13 | for name in dir(codes): 14 | if not name.startswith('_'): 15 | value = getattr(codes, name) 16 | setattr(self, name, code_to_chars(value)) 17 | 18 | class AnsiFore: 19 | BLACK = 30 20 | RED = 31 21 | GREEN = 32 22 | YELLOW = 33 23 | BLUE = 34 24 | MAGENTA = 35 25 | CYAN = 36 26 | WHITE = 37 27 | RESET = 39 28 | 29 | class AnsiBack: 30 | BLACK = 40 31 | RED = 41 32 | GREEN = 42 33 | YELLOW = 43 34 | BLUE = 44 35 | MAGENTA = 45 36 | CYAN = 46 37 | WHITE = 47 38 | RESET = 49 39 | 40 | class AnsiStyle: 41 | BRIGHT = 1 42 | DIM = 2 43 | NORMAL = 22 44 | RESET_ALL = 0 45 | 46 | Fore = AnsiCodes( AnsiFore ) 47 | Back = AnsiCodes( AnsiBack ) 48 | Style = AnsiCodes( AnsiStyle ) 49 | 50 | -------------------------------------------------------------------------------- /pyzen/_colorama/ansitowin32.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import sys 4 | 5 | from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style 6 | from .winterm import WinTerm, WinColor, WinStyle 7 | from .win32 import windll 8 | 9 | 10 | if windll is not None: 11 | winterm = WinTerm() 12 | 13 | 14 | def is_a_tty(stream): 15 | return hasattr(stream, 'isatty') and stream.isatty() 16 | 17 | 18 | class StreamWrapper(object): 19 | ''' 20 | Wraps a stream (such as stdout), acting as a transparent proxy for all 21 | attribute access apart from method 'write()', which is delegated to our 22 | Converter instance. 23 | ''' 24 | def __init__(self, wrapped, converter): 25 | # double-underscore everything to prevent clashes with names of 26 | # attributes on the wrapped stream object. 27 | self.__wrapped = wrapped 28 | self.__convertor = converter 29 | 30 | def __getattr__(self, name): 31 | return getattr(self.__wrapped, name) 32 | 33 | def write(self, text): 34 | self.__convertor.write(text) 35 | 36 | 37 | class AnsiToWin32(object): 38 | ''' 39 | Implements a 'write()' method which, on Windows, will strip ANSI character 40 | sequences from the text, and if outputting to a tty, will convert them into 41 | win32 function calls. 42 | ''' 43 | ANSI_RE = re.compile('\033\[((?:\d|;)*)([a-zA-Z])') 44 | 45 | def __init__(self, wrapped, convert=None, strip=None, autoreset=False): 46 | # The wrapped stream (normally sys.stdout or sys.stderr) 47 | self.wrapped = wrapped 48 | 49 | # should we reset colors to defaults after every .write() 50 | self.autoreset = autoreset 51 | 52 | # create the proxy wrapping our output stream 53 | self.stream = StreamWrapper(wrapped, self) 54 | 55 | on_windows = sys.platform.startswith('win') 56 | 57 | # should we strip ANSI sequences from our output? 58 | if strip is None: 59 | strip = on_windows 60 | self.strip = strip 61 | 62 | # should we should convert ANSI sequences into win32 calls? 63 | if convert is None: 64 | convert = on_windows and is_a_tty(wrapped) 65 | self.convert = convert 66 | 67 | # dict of ansi codes to win32 functions and parameters 68 | self.win32_calls = self.get_win32_calls() 69 | 70 | # are we wrapping stderr? 71 | self.on_stderr = self.wrapped is sys.stderr 72 | 73 | 74 | def should_wrap(self): 75 | ''' 76 | True if this class is actually needed. If false, then the output 77 | stream will not be affected, nor will win32 calls be issued, so 78 | wrapping stdout is not actually required. This will generally be 79 | False on non-Windows platforms, unless optional functionality like 80 | autoreset has been requested using kwargs to init() 81 | ''' 82 | return self.convert or self.strip or self.autoreset 83 | 84 | 85 | def get_win32_calls(self): 86 | if self.convert and winterm: 87 | return { 88 | AnsiStyle.RESET_ALL: (winterm.reset_all, ), 89 | AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT), 90 | AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL), 91 | AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL), 92 | AnsiFore.BLACK: (winterm.fore, WinColor.BLACK), 93 | AnsiFore.RED: (winterm.fore, WinColor.RED), 94 | AnsiFore.GREEN: (winterm.fore, WinColor.GREEN), 95 | AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW), 96 | AnsiFore.BLUE: (winterm.fore, WinColor.BLUE), 97 | AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA), 98 | AnsiFore.CYAN: (winterm.fore, WinColor.CYAN), 99 | AnsiFore.WHITE: (winterm.fore, WinColor.GREY), 100 | AnsiFore.RESET: (winterm.fore, ), 101 | AnsiBack.BLACK: (winterm.back, WinColor.BLACK), 102 | AnsiBack.RED: (winterm.back, WinColor.RED), 103 | AnsiBack.GREEN: (winterm.back, WinColor.GREEN), 104 | AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW), 105 | AnsiBack.BLUE: (winterm.back, WinColor.BLUE), 106 | AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA), 107 | AnsiBack.CYAN: (winterm.back, WinColor.CYAN), 108 | AnsiBack.WHITE: (winterm.back, WinColor.GREY), 109 | AnsiBack.RESET: (winterm.back, ), 110 | } 111 | 112 | 113 | def write(self, text): 114 | if self.strip or self.convert: 115 | self.write_and_convert(text) 116 | else: 117 | self.wrapped.write(text) 118 | self.wrapped.flush() 119 | if self.autoreset: 120 | self.reset_all() 121 | 122 | 123 | def reset_all(self): 124 | if self.convert: 125 | self.call_win32('m', (0,)) 126 | else: 127 | self.wrapped.write(Style.RESET_ALL) 128 | 129 | 130 | def write_and_convert(self, text): 131 | ''' 132 | Write the given text to our wrapped stream, stripping any ANSI 133 | sequences from the text, and optionally converting them into win32 134 | calls. 135 | ''' 136 | cursor = 0 137 | for match in self.ANSI_RE.finditer(text): 138 | start, end = match.span() 139 | self.write_plain_text(text, cursor, start) 140 | self.convert_ansi(*match.groups()) 141 | cursor = end 142 | self.write_plain_text(text, cursor, len(text)) 143 | 144 | 145 | def write_plain_text(self, text, start, end): 146 | if start < end: 147 | self.wrapped.write(text[start:end]) 148 | self.wrapped.flush() 149 | 150 | 151 | def convert_ansi(self, paramstring, command): 152 | if self.convert: 153 | params = self.extract_params(paramstring) 154 | self.call_win32(command, params) 155 | 156 | 157 | def extract_params(self, paramstring): 158 | def split(paramstring): 159 | for p in paramstring.split(';'): 160 | if p != '': 161 | yield int(p) 162 | return tuple(split(paramstring)) 163 | 164 | 165 | def call_win32(self, command, params): 166 | if params == []: 167 | params = [0] 168 | if command == 'm': 169 | for param in params: 170 | if param in self.win32_calls: 171 | func_args = self.win32_calls[param] 172 | func = func_args[0] 173 | args = func_args[1:] 174 | kwargs = dict(on_stderr=self.on_stderr) 175 | func(*args, **kwargs) 176 | 177 | -------------------------------------------------------------------------------- /pyzen/_colorama/initialise.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import sys 3 | 4 | from .ansitowin32 import AnsiToWin32 5 | 6 | 7 | orig_stdout = sys.stdout 8 | orig_stderr = sys.stderr 9 | 10 | atexit_done = False 11 | 12 | 13 | def reset_all(): 14 | AnsiToWin32(orig_stdout).reset_all() 15 | 16 | 17 | def init(autoreset=False, convert=None, strip=None, wrap=True): 18 | 19 | if wrap==False and (autoreset==True or convert==True or strip==True): 20 | raise ValueError('wrap=False conflicts with any other arg=True') 21 | 22 | sys.stdout = wrap_stream(orig_stdout, convert, strip, autoreset, wrap) 23 | sys.stderr = wrap_stream(orig_stderr, convert, strip, autoreset, wrap) 24 | 25 | global atexit_done 26 | if not atexit_done: 27 | atexit.register(reset_all) 28 | atexit_done = True 29 | 30 | 31 | def wrap_stream(stream, convert, strip, autoreset, wrap): 32 | if wrap: 33 | wrapper = AnsiToWin32(stream, 34 | convert=convert, strip=strip, autoreset=autoreset) 35 | if wrapper.should_wrap(): 36 | stream = wrapper.stream 37 | return stream 38 | 39 | -------------------------------------------------------------------------------- /pyzen/_colorama/win32.py: -------------------------------------------------------------------------------- 1 | 2 | # from winbase.h 3 | STDOUT = -11 4 | STDERR = -12 5 | 6 | try: 7 | from ctypes import windll 8 | except ImportError: 9 | windll = None 10 | SetConsoleTextAttribute = lambda *_: None 11 | else: 12 | from ctypes import ( 13 | byref, Structure, c_char, c_short, c_uint32, c_ushort 14 | ) 15 | 16 | handles = { 17 | STDOUT: windll.kernel32.GetStdHandle(STDOUT), 18 | STDERR: windll.kernel32.GetStdHandle(STDERR), 19 | } 20 | 21 | SHORT = c_short 22 | WORD = c_ushort 23 | DWORD = c_uint32 24 | TCHAR = c_char 25 | 26 | class COORD(Structure): 27 | """struct in wincon.h""" 28 | _fields_ = [ 29 | ('X', SHORT), 30 | ('Y', SHORT), 31 | ] 32 | 33 | class SMALL_RECT(Structure): 34 | """struct in wincon.h.""" 35 | _fields_ = [ 36 | ("Left", SHORT), 37 | ("Top", SHORT), 38 | ("Right", SHORT), 39 | ("Bottom", SHORT), 40 | ] 41 | 42 | class CONSOLE_SCREEN_BUFFER_INFO(Structure): 43 | """struct in wincon.h.""" 44 | _fields_ = [ 45 | ("dwSize", COORD), 46 | ("dwCursorPosition", COORD), 47 | ("wAttributes", WORD), 48 | ("srWindow", SMALL_RECT), 49 | ("dwMaximumWindowSize", COORD), 50 | ] 51 | 52 | def GetConsoleScreenBufferInfo(stream_id): 53 | handle = handles[stream_id] 54 | csbi = CONSOLE_SCREEN_BUFFER_INFO() 55 | success = windll.kernel32.GetConsoleScreenBufferInfo( 56 | handle, byref(csbi)) 57 | # This fails when imported via setup.py when installing using 'pip' 58 | # presumably the fix is that running setup.py should not trigger all 59 | # this activity. 60 | # assert success 61 | return csbi 62 | 63 | def SetConsoleTextAttribute(stream_id, attrs): 64 | handle = handles[stream_id] 65 | success = windll.kernel32.SetConsoleTextAttribute(handle, attrs) 66 | assert success 67 | 68 | def SetConsoleCursorPosition(stream_id, position): 69 | handle = handles[stream_id] 70 | position = COORD(*position) 71 | success = windll.kernel32.SetConsoleCursorPosition(handle, position) 72 | assert success 73 | 74 | def FillConsoleOutputCharacter(stream_id, char, length, start): 75 | handle = handles[stream_id] 76 | char = TCHAR(char) 77 | length = DWORD(length) 78 | start = COORD(*start) 79 | num_written = DWORD(0) 80 | # AttributeError: function 'FillConsoleOutputCharacter' not found 81 | # could it just be that my types are wrong? 82 | success = windll.kernel32.FillConsoleOutputCharacter( 83 | handle, char, length, start, byref(num_written)) 84 | assert success 85 | return num_written.value 86 | 87 | 88 | if __name__=='__main__': 89 | x = GetConsoleScreenBufferInfo(STDOUT) 90 | print(x.dwSize) 91 | print(x.dwCursorPosition) 92 | print(x.wAttributes) 93 | print(x.srWindow) 94 | print(x.dwMaximumWindowSize) 95 | 96 | -------------------------------------------------------------------------------- /pyzen/_colorama/winterm.py: -------------------------------------------------------------------------------- 1 | 2 | from . import win32 3 | 4 | 5 | # from wincon.h 6 | class WinColor(object): 7 | BLACK = 0 8 | BLUE = 1 9 | GREEN = 2 10 | CYAN = 3 11 | RED = 4 12 | MAGENTA = 5 13 | YELLOW = 6 14 | GREY = 7 15 | 16 | # from wincon.h 17 | class WinStyle(object): 18 | NORMAL = 0x00 # dim text, dim background 19 | BRIGHT = 0x08 # bright text, dim background 20 | 21 | 22 | class WinTerm(object): 23 | 24 | def __init__(self): 25 | self._default = \ 26 | win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes 27 | self.set_attrs(self._default) 28 | self._default_fore = self._fore 29 | self._default_back = self._back 30 | self._default_style = self._style 31 | 32 | def get_attrs(self): 33 | return self._fore + self._back * 16 + self._style 34 | 35 | def set_attrs(self, value): 36 | self._fore = value & 7 37 | self._back = (value >> 4) & 7 38 | self._style = value & WinStyle.BRIGHT 39 | 40 | def reset_all(self, on_stderr=None): 41 | self.set_attrs(self._default) 42 | self.set_console(attrs=self._default) 43 | 44 | def fore(self, fore=None, on_stderr=False): 45 | if fore is None: 46 | fore = self._default_fore 47 | self._fore = fore 48 | self.set_console(on_stderr=on_stderr) 49 | 50 | def back(self, back=None, on_stderr=False): 51 | if back is None: 52 | back = self._default_back 53 | self._back = back 54 | self.set_console(on_stderr=on_stderr) 55 | 56 | def style(self, style=None, on_stderr=False): 57 | if style is None: 58 | style = self._default_style 59 | self._style = style 60 | self.set_console(on_stderr=on_stderr) 61 | 62 | def set_console(self, attrs=None, on_stderr=False): 63 | if attrs is None: 64 | attrs = self.get_attrs() 65 | handle = win32.STDOUT 66 | if on_stderr: 67 | handle = win32.STDERR 68 | win32.SetConsoleTextAttribute(handle, attrs) 69 | 70 | -------------------------------------------------------------------------------- /pyzen/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import sys 3 | import os 4 | import time 5 | import traceback 6 | from multiprocessing import Process, Queue 7 | from threading import Thread, Lock 8 | from Queue import Empty 9 | 10 | from pyzen.ui import load_ui 11 | 12 | _SLEEP_TIME = 1 13 | 14 | MAGIC_RETURN_CODE = 254 15 | 16 | class ReloaderThread(Thread): 17 | def __init__(self): 18 | super(ReloaderThread, self).__init__() 19 | self.daemon = False 20 | self._quit = False 21 | self._quit_lock = Lock() 22 | self.do_reload = False 23 | 24 | def run(self): 25 | """When this is run, it will force other 26 | threads to exit when any modules currently loaded change. 27 | """ 28 | mtimes = {} 29 | while True: 30 | with self._quit_lock: 31 | if self._quit: 32 | return 33 | 34 | for filename in filter(None, [getattr(module, '__file__', None) 35 | for module in sys.modules.values()]): 36 | while not os.path.isfile(filename): # Probably in an egg or zip file 37 | filename = os.path.dirname(filename) 38 | if not filename: 39 | break 40 | if not filename: # Couldn't map to physical file, so just ignore 41 | continue 42 | 43 | if filename.endswith('.pyc') or filename.endswith('.pyo'): 44 | filename = filename[:-1] 45 | 46 | if not os.path.isfile(filename): 47 | # Compiled file for non-existant source 48 | continue 49 | 50 | mtime = os.stat(filename).st_mtime 51 | if filename not in mtimes: 52 | mtimes[filename] = mtime 53 | continue 54 | if mtime > mtimes[filename]: 55 | print >> sys.stderr, 'Detected modification of %s, restarting.' % filename 56 | self.do_reload = True 57 | sys.exit(MAGIC_RETURN_CODE) 58 | time.sleep(_SLEEP_TIME) 59 | 60 | def quit(self): 61 | with self._quit_lock(): 62 | self._quit = True 63 | 64 | 65 | class RunnerThread(Thread): 66 | """A thread to run the provided function and push the test results 67 | back to a Queue. 68 | """ 69 | 70 | def __init__(self, q, func, args, kwargs): 71 | super(RunnerThread, self).__init__() 72 | self.q = q 73 | self.func = func 74 | self.args = args 75 | self.kwargs = kwargs 76 | 77 | def run(self): 78 | try: 79 | start_time = time.clock() 80 | result = self.func(*self.args, **self.kwargs) 81 | end_time = time.clock() 82 | self.q.put({ 83 | 'failures': len(result.failures), 84 | 'errors': len(result.errors), 85 | 'total': result.testsRun, 86 | 'time': end_time - start_time, 87 | }) 88 | except Exception: 89 | traceback.print_exc() 90 | self.q.put({ 91 | 'failures': -1, 92 | 'errors': -1, 93 | 'total': -1, 94 | 'time': 0, 95 | }) 96 | 97 | 98 | def reloader(q, func, args, kwargs): 99 | t = ReloaderThread() 100 | t.start() 101 | try: 102 | RunnerThread(q, func, args, kwargs).run() 103 | except KeyboardInterrupt: 104 | t.quit() 105 | finally: 106 | t.join() 107 | if t.do_reload: 108 | sys.exit(MAGIC_RETURN_CODE) 109 | 110 | 111 | def main(ui_override, func, *args, **kwargs): 112 | p = None 113 | uis = list(load_ui(ui_override)) 114 | try: 115 | while True: 116 | q = Queue() 117 | p = Process(target=reloader, args=(q, func, args, kwargs)) 118 | p.daemon = True 119 | p.start() 120 | while True: 121 | try: 122 | cmd = q.get(True, _SLEEP_TIME) 123 | for ui in uis: 124 | ui.handle(**cmd) 125 | except Empty: 126 | # Timed out, check if we need to restart 127 | if not p.is_alive(): 128 | if p.exitcode == MAGIC_RETURN_CODE: 129 | break # This means we need to restart it 130 | else: 131 | return p.exitcode # Any other return code should be considered real 132 | finally: 133 | for ui in uis: 134 | ui.shutdown() 135 | if p is not None: 136 | p.terminate() 137 | -------------------------------------------------------------------------------- /pyzen/discover.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # Copyright Michael Foord 2009-2010 3 | # discover.py 4 | # Test discovery for unittest 5 | # Compatible with Python 2.4-2.6 and 3.0-3.1 6 | # Licensed under the BSD License 7 | # See: http://pypi.python.org/pypi/discover 8 | 9 | import optparse 10 | import os 11 | import re 12 | import sys 13 | import traceback 14 | import types 15 | import unittest 16 | 17 | from fnmatch import fnmatch 18 | 19 | __version__ = '0.4.0' 20 | __all__ = ['DiscoveringTestLoader', 'main', 'defaultTestLoader'] 21 | 22 | 23 | if hasattr(types, 'ClassType'): 24 | class_types = (types.ClassType, type) 25 | else: 26 | # for Python 3.0 compatibility 27 | class_types = type 28 | 29 | def _CmpToKey(mycmp): 30 | 'Convert a cmp= function into a key= function' 31 | class K(object): 32 | def __init__(self, obj): 33 | self.obj = obj 34 | def __lt__(self, other): 35 | return mycmp(self.obj, other.obj) == -1 36 | return K 37 | 38 | try: 39 | from types import UnboundMethodType 40 | except ImportError: 41 | # Python 3 compatibility 42 | UnboundMethodType = types.FunctionType 43 | 44 | # what about .pyc or .pyo (etc) 45 | # we would need to avoid loading the same tests multiple times 46 | # from '.py', '.pyc' *and* '.pyo' 47 | VALID_MODULE_NAME = re.compile(r'[_a-z]\w*\.py$', re.IGNORECASE) 48 | 49 | 50 | def _make_failed_import_test(name, suiteClass): 51 | message = 'Failed to import test module: %s' % name 52 | if hasattr(traceback, 'format_exc'): 53 | # Python 2.3 compatibility 54 | # format_exc returns two frames of discover.py as well 55 | message += '\n%s' % traceback.format_exc() 56 | return _make_failed_test('ModuleImportFailure', name, ImportError(message), 57 | suiteClass) 58 | 59 | def _make_failed_load_tests(name, exception, suiteClass): 60 | return _make_failed_test('LoadTestsFailure', name, exception, suiteClass) 61 | 62 | def _make_failed_test(classname, methodname, exception, suiteClass): 63 | def testFailure(self): 64 | raise exception 65 | attrs = {methodname: testFailure} 66 | TestClass = type(classname, (unittest.TestCase,), attrs) 67 | return suiteClass((TestClass(methodname),)) 68 | 69 | try: 70 | cmp 71 | except NameError: 72 | @staticmethod 73 | def cmp(x, y): 74 | """Return -1 if x < y, 0 if x == y and 1 if x > y""" 75 | return (x > y) - (x < y) 76 | 77 | 78 | class DiscoveringTestLoader(unittest.TestLoader): 79 | """ 80 | This class is responsible for loading tests according to various criteria 81 | and returning them wrapped in a TestSuite 82 | """ 83 | testMethodPrefix = 'test' 84 | sortTestMethodsUsing = cmp 85 | suiteClass = unittest.TestSuite 86 | _top_level_dir = None 87 | 88 | def loadTestsFromTestCase(self, testCaseClass): 89 | """Return a suite of all tests cases contained in testCaseClass""" 90 | if issubclass(testCaseClass, unittest.TestSuite): 91 | raise TypeError("Test cases should not be derived from TestSuite." 92 | " Maybe you meant to derive from TestCase?") 93 | testCaseNames = self.getTestCaseNames(testCaseClass) 94 | if not testCaseNames and hasattr(testCaseClass, 'runTest'): 95 | testCaseNames = ['runTest'] 96 | loaded_suite = self.suiteClass(map(testCaseClass, testCaseNames)) 97 | return loaded_suite 98 | 99 | def loadTestsFromModule(self, module, use_load_tests=True): 100 | """Return a suite of all tests cases contained in the given module""" 101 | tests = [] 102 | for name in dir(module): 103 | obj = getattr(module, name) 104 | if isinstance(obj, type) and issubclass(obj, unittest.TestCase): 105 | tests.append(self.loadTestsFromTestCase(obj)) 106 | 107 | load_tests = getattr(module, 'load_tests', None) 108 | tests = self.suiteClass(tests) 109 | if use_load_tests and load_tests is not None: 110 | try: 111 | return load_tests(self, tests, None) 112 | except: 113 | ExceptionClass, e = sys.exc_info()[:2] 114 | if not isinstance(e, Exception): 115 | # for BaseException exceptions 116 | raise 117 | return _make_failed_load_tests(module.__name__, e, 118 | self.suiteClass) 119 | return tests 120 | 121 | def loadTestsFromName(self, name, module=None): 122 | """Return a suite of all tests cases given a string specifier. 123 | 124 | The name may resolve either to a module, a test case class, a 125 | test method within a test case class, or a callable object which 126 | returns a TestCase or TestSuite instance. 127 | 128 | The method optionally resolves the names relative to a given module. 129 | """ 130 | parts = name.split('.') 131 | if module is None: 132 | parts_copy = parts[:] 133 | while parts_copy: 134 | try: 135 | module = __import__('.'.join(parts_copy)) 136 | break 137 | except ImportError: 138 | del parts_copy[-1] 139 | if not parts_copy: 140 | raise 141 | parts = parts[1:] 142 | obj = module 143 | for part in parts: 144 | parent, obj = obj, getattr(obj, part) 145 | 146 | if isinstance(obj, types.ModuleType): 147 | return self.loadTestsFromModule(obj) 148 | elif isinstance(obj, type) and issubclass(obj, unittest.TestCase): 149 | return self.loadTestsFromTestCase(obj) 150 | elif (isinstance(obj, UnboundMethodType) and 151 | isinstance(parent, type) and 152 | issubclass(parent, unittest.TestCase)): 153 | name = obj.__name__ 154 | inst = parent(name) 155 | # static methods follow a different path 156 | if not isinstance(getattr(inst, name), types.FunctionType): 157 | return self.suiteClass([inst]) 158 | elif isinstance(obj, unittest.TestSuite): 159 | return obj 160 | if hasattr(obj, '__call__'): 161 | test = obj() 162 | if isinstance(test, unittest.TestSuite): 163 | return test 164 | elif isinstance(test, unittest.TestCase): 165 | return self.suiteClass([test]) 166 | else: 167 | raise TypeError("calling %s returned %s, not a test" % 168 | (obj, test)) 169 | else: 170 | raise TypeError("don't know how to make test from: %s" % obj) 171 | 172 | def loadTestsFromNames(self, names, module=None): 173 | """Return a suite of all tests cases found using the given sequence 174 | of string specifiers. See 'loadTestsFromName()'. 175 | """ 176 | suites = [self.loadTestsFromName(name, module) for name in names] 177 | return self.suiteClass(suites) 178 | 179 | def getTestCaseNames(self, testCaseClass): 180 | """Return a sorted sequence of method names found within testCaseClass 181 | """ 182 | def isTestMethod(attrname, testCaseClass=testCaseClass, 183 | prefix=self.testMethodPrefix): 184 | return attrname.startswith(prefix) and \ 185 | hasattr(getattr(testCaseClass, attrname), '__call__') 186 | testFnNames = list(filter(isTestMethod, dir(testCaseClass))) 187 | if self.sortTestMethodsUsing: 188 | testFnNames.sort(key=_CmpToKey(self.sortTestMethodsUsing)) 189 | return testFnNames 190 | 191 | def discover(self, start_dir, pattern='test*.py', top_level_dir=None): 192 | """Find and return all test modules from the specified start 193 | directory, recursing into subdirectories to find them. Only test files 194 | that match the pattern will be loaded. (Using shell style pattern 195 | matching.) 196 | 197 | All test modules must be importable from the top level of the project. 198 | If the start directory is not the top level directory then the top 199 | level directory must be specified separately. 200 | 201 | If a test package name (directory with '__init__.py') matches the 202 | pattern then the package will be checked for a 'load_tests' function. If 203 | this exists then it will be called with loader, tests, pattern. 204 | 205 | If load_tests exists then discovery does *not* recurse into the package, 206 | load_tests is responsible for loading all tests in the package. 207 | 208 | The pattern is deliberately not stored as a loader attribute so that 209 | packages can continue discovery themselves. top_level_dir is stored so 210 | load_tests does not need to pass this argument in to loader.discover(). 211 | """ 212 | set_implicit_top = False 213 | if top_level_dir is None and self._top_level_dir is not None: 214 | # make top_level_dir optional if called from load_tests in a package 215 | top_level_dir = self._top_level_dir 216 | elif top_level_dir is None: 217 | set_implicit_top = True 218 | top_level_dir = start_dir 219 | 220 | top_level_dir = os.path.abspath(top_level_dir) 221 | 222 | if not top_level_dir in sys.path: 223 | # all test modules must be importable from the top level directory 224 | # should we *unconditionally* put the start directory in first 225 | # in sys.path to minimise likelihood of conflicts between installed 226 | # modules and development versions? 227 | sys.path.insert(0, top_level_dir) 228 | self._top_level_dir = top_level_dir 229 | 230 | is_not_importable = False 231 | if os.path.isdir(os.path.abspath(start_dir)): 232 | start_dir = os.path.abspath(start_dir) 233 | if start_dir != top_level_dir: 234 | is_not_importable = not os.path.isfile(os.path.join(start_dir, '__init__.py')) 235 | else: 236 | # support for discovery from dotted module names 237 | try: 238 | __import__(start_dir) 239 | except ImportError: 240 | is_not_importable = True 241 | else: 242 | the_module = sys.modules[start_dir] 243 | top_part = start_dir.split('.')[0] 244 | start_dir = os.path.abspath(os.path.dirname((the_module.__file__))) 245 | if set_implicit_top: 246 | self._top_level_dir = os.path.abspath(os.path.dirname(os.path.dirname(sys.modules[top_part].__file__))) 247 | sys.path.remove(top_level_dir) 248 | 249 | if is_not_importable: 250 | raise ImportError('Start directory is not importable: %r' % start_dir) 251 | 252 | tests = list(self._find_tests(start_dir, pattern)) 253 | return self.suiteClass(tests) 254 | 255 | def _get_name_from_path(self, path): 256 | path = os.path.splitext(os.path.normpath(path))[0] 257 | 258 | _relpath = relpath(path, self._top_level_dir) 259 | assert not os.path.isabs(_relpath), "Path must be within the project" 260 | assert not _relpath.startswith('..'), "Path must be within the project" 261 | 262 | name = _relpath.replace(os.path.sep, '.') 263 | return name 264 | 265 | def _get_module_from_name(self, name): 266 | __import__(name) 267 | return sys.modules[name] 268 | 269 | def _match_path(self, path, full_path, pattern): 270 | # override this method to use alternative matching strategy 271 | return fnmatch(path, pattern) 272 | 273 | def _find_tests(self, start_dir, pattern): 274 | """Used by discovery. Yields test suites it loads.""" 275 | paths = os.listdir(start_dir) 276 | 277 | for path in paths: 278 | full_path = os.path.join(start_dir, path) 279 | if os.path.isfile(full_path): 280 | if not VALID_MODULE_NAME.match(path): 281 | # valid Python identifiers only 282 | continue 283 | if not self._match_path(path, full_path, pattern): 284 | continue 285 | # if the test file matches, load it 286 | name = self._get_name_from_path(full_path) 287 | try: 288 | module = self._get_module_from_name(name) 289 | except: 290 | yield _make_failed_import_test(name, self.suiteClass) 291 | else: 292 | mod_file = os.path.abspath(getattr(module, '__file__', full_path)) 293 | realpath = os.path.splitext(mod_file)[0] 294 | fullpath_noext = os.path.splitext(full_path)[0] 295 | if realpath.lower() != fullpath_noext.lower(): 296 | module_dir = os.path.dirname(realpath) 297 | mod_name = os.path.splitext(os.path.basename(full_path))[0] 298 | expected_dir = os.path.dirname(full_path) 299 | msg = ("%r module incorrectly imported from %r. Expected %r. " 300 | "Is this module globally installed?") 301 | raise ImportError(msg % (mod_name, module_dir, expected_dir)) 302 | yield self.loadTestsFromModule(module) 303 | elif os.path.isdir(full_path): 304 | if not os.path.isfile(os.path.join(full_path, '__init__.py')): 305 | continue 306 | 307 | load_tests = None 308 | tests = None 309 | if fnmatch(path, pattern): 310 | # only check load_tests if the package directory itself matches the filter 311 | name = self._get_name_from_path(full_path) 312 | package = self._get_module_from_name(name) 313 | load_tests = getattr(package, 'load_tests', None) 314 | tests = self.loadTestsFromModule(package, use_load_tests=False) 315 | 316 | if load_tests is None: 317 | if tests is not None: 318 | # tests loaded from package file 319 | yield tests 320 | # recurse into the package 321 | for test in self._find_tests(full_path, pattern): 322 | yield test 323 | else: 324 | try: 325 | yield load_tests(self, tests, pattern) 326 | except: 327 | ExceptionClass, e = sys.exc_info()[:2] 328 | if not isinstance(e, Exception): 329 | # for BaseException exceptions 330 | raise 331 | yield _make_failed_load_tests(package.__name__, e, 332 | self.suiteClass) 333 | 334 | 335 | ############################################## 336 | # relpath implementation taken from Python 2.7 337 | 338 | if not hasattr(os.path, 'relpath'): 339 | if os.path is sys.modules.get('ntpath'): 340 | def relpath(path, start=os.path.curdir): 341 | """Return a relative version of a path""" 342 | 343 | if not path: 344 | raise ValueError("no path specified") 345 | start_list = os.path.abspath(start).split(os.path.sep) 346 | path_list = os.path.abspath(path).split(os.path.sep) 347 | if start_list[0].lower() != path_list[0].lower(): 348 | unc_path, rest = os.path.splitunc(path) 349 | unc_start, rest = os.path.splitunc(start) 350 | if bool(unc_path) ^ bool(unc_start): 351 | raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)" 352 | % (path, start)) 353 | else: 354 | raise ValueError("path is on drive %s, start on drive %s" 355 | % (path_list[0], start_list[0])) 356 | # Work out how much of the filepath is shared by start and path. 357 | for i in range(min(len(start_list), len(path_list))): 358 | if start_list[i].lower() != path_list[i].lower(): 359 | break 360 | else: 361 | i += 1 362 | 363 | rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] 364 | if not rel_list: 365 | return os.path.curdir 366 | return os.path.join(*rel_list) 367 | 368 | else: 369 | # default to posixpath definition 370 | def relpath(path, start=os.path.curdir): 371 | """Return a relative version of a path""" 372 | 373 | if not path: 374 | raise ValueError("no path specified") 375 | 376 | start_list = os.path.abspath(start).split(os.path.sep) 377 | path_list = os.path.abspath(path).split(os.path.sep) 378 | 379 | # Work out how much of the filepath is shared by start and path. 380 | i = len(os.path.commonprefix([start_list, path_list])) 381 | 382 | rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] 383 | if not rel_list: 384 | return os.path.curdir 385 | return os.path.join(*rel_list) 386 | else: 387 | from os.path import relpath 388 | 389 | ############################################# 390 | 391 | 392 | USAGE = """\ 393 | Usage: discover.py [options] 394 | 395 | Options: 396 | -v, --verbose Verbose output 397 | -s directory Directory to start discovery ('.' default) 398 | -p pattern Pattern to match test files ('test*.py' default) 399 | -t directory Top level directory of project (default to 400 | start directory) 401 | 402 | For test discovery all test modules must be importable from the top 403 | level directory of the project. 404 | """ 405 | 406 | def _usage_exit(msg=None): 407 | if msg: 408 | print (msg) 409 | print (USAGE) 410 | sys.exit(2) 411 | 412 | 413 | def _do_discovery(argv, verbosity, Loader): 414 | # handle command line args for test discovery 415 | parser = optparse.OptionParser() 416 | parser.add_option('-v', '--verbose', dest='verbose', default=False, 417 | help='Verbose output', action='store_true') 418 | parser.add_option('-s', '--start-directory', dest='start', default='.', 419 | help="Directory to start discovery ('.' default)") 420 | parser.add_option('-p', '--pattern', dest='pattern', default='test*.py', 421 | help="Pattern to match tests ('test*.py' default)") 422 | parser.add_option('-t', '--top-level-directory', dest='top', default=None, 423 | help='Top level directory of project (defaults to start directory)') 424 | 425 | options, args = parser.parse_args(argv) 426 | if len(args) > 3: 427 | _usage_exit() 428 | 429 | for name, value in zip(('start', 'pattern', 'top'), args): 430 | setattr(options, name, value) 431 | 432 | if options.verbose: 433 | verbosity = 2 434 | 435 | start_dir = options.start 436 | pattern = options.pattern 437 | top_level_dir = options.top 438 | 439 | loader = Loader() 440 | return loader.discover(start_dir, pattern, top_level_dir), verbosity 441 | 442 | 443 | def _run_tests(tests, testRunner, verbosity, exit): 444 | if isinstance(testRunner, class_types): 445 | try: 446 | testRunner = testRunner(verbosity=verbosity) 447 | except TypeError: 448 | # didn't accept the verbosity argument 449 | testRunner = testRunner() 450 | result = testRunner.run(tests) 451 | if exit: 452 | sys.exit(not result.wasSuccessful()) 453 | return result 454 | 455 | 456 | def main(argv=None, testRunner=None, testLoader=None, exit=True, verbosity=1): 457 | if testLoader is None: 458 | testLoader = DiscoveringTestLoader 459 | if testRunner is None: 460 | testRunner = unittest.TextTestRunner 461 | if argv is None: 462 | argv = sys.argv[1:] 463 | 464 | tests, verbosity = _do_discovery(argv, verbosity, testLoader) 465 | return _run_tests(tests, testRunner, verbosity, exit) 466 | 467 | defaultTestLoader = DiscoveringTestLoader() 468 | 469 | def collector(): 470 | # import __main__ triggers code re-execution 471 | __main__ = sys.modules['__main__'] 472 | setupDir = os.path.abspath(os.path.dirname(__main__.__file__)) 473 | return defaultTestLoader.discover(setupDir) 474 | 475 | if __name__ == '__main__': 476 | if sys.argv[0] is None: 477 | # fix for weird behaviour when run with python -m 478 | # from a zipped egg. 479 | sys.argv[0] = 'discover.py' 480 | main() 481 | -------------------------------------------------------------------------------- /pyzen/distutils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import types 4 | 5 | from setuptools.command.test import test 6 | from pkg_resources import * 7 | 8 | from pyzen.core import main 9 | from pyzen.runner import get_test_runner 10 | 11 | class ZenTestProgram(unittest.TestProgram): 12 | 13 | def runTests(self): 14 | testRunner = self.testRunner(verbosity=self.verbosity) 15 | self.result = testRunner.run(self.test) 16 | 17 | def run_tests(loader, args, path, nocolor): 18 | sys.path[:] = path 19 | loader_ep = EntryPoint.parse("x="+loader) 20 | loader_class = loader_ep.load(require=False) 21 | m = ZenTestProgram(None, None, [unittest.__file__]+args, testLoader=loader_class(), testRunner=get_test_runner(nocolor)) 22 | return m.result 23 | 24 | class zen(test): 25 | """Command to run test suite under PyZen.""" 26 | description = 'run unit tests under PyZen' 27 | 28 | user_options = test.user_options + [ 29 | ('ui=', 'u', 'Force the use of the given PyZen UI'), 30 | ('nocolor', 'c', 'Disable colored output'), 31 | ] 32 | boolean_options = ['nocolor'] 33 | 34 | def initialize_options(self): 35 | test.initialize_options(self) 36 | self.ui = None 37 | self.nocolor = False 38 | 39 | def run_tests(self): 40 | main(self.ui, run_tests, self.test_loader, self.test_args, sys.path[:], self.nocolor) 41 | -------------------------------------------------------------------------------- /pyzen/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/management/__init__.py -------------------------------------------------------------------------------- /pyzen/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/management/commands/__init__.py -------------------------------------------------------------------------------- /pyzen/management/commands/zen.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from optparse import make_option 3 | 4 | from django.conf import settings 5 | from django.core.management.base import BaseCommand, CommandError 6 | from django.test.utils import get_runner 7 | 8 | try: 9 | from south.management.commands import patch_for_test_db_setup 10 | except ImportError: 11 | patch_for_test_db_setup = lambda: None 12 | 13 | from pyzen.core import main 14 | from pyzen.runner import get_test_runner 15 | 16 | def run_tests(*test_labels, **options): 17 | patch_for_test_db_setup() 18 | verbosity = int(options.get('verbosity', 1)) 19 | interactive = options.get('interactive', True) 20 | failfast = options.get('failfast', False) 21 | nocolor = options.get('nocolor', False) 22 | TestSuiteRunner = get_runner(settings) 23 | 24 | class NewTestSuiteRunner(TestSuiteRunner): 25 | def run_suite(self, suite, **kwargs): 26 | return get_test_runner(nocolor)(verbosity=self.verbosity).run(suite) 27 | def suite_result(self, suite, result, **kwargs): 28 | return result 29 | 30 | test_runner = NewTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) 31 | result = test_runner.run_tests(test_labels) 32 | return result 33 | 34 | class Command(BaseCommand): 35 | 36 | option_list = BaseCommand.option_list + ( 37 | make_option('-u', '--ui', help='Force the use of the given PyZen UI'), 38 | make_option('--nocolor', action='store_true', default=False, help='Disable colored output.') 39 | ) 40 | 41 | def handle(self, *test_labels, **options): 42 | try: 43 | main(options.get('ui'), run_tests, *test_labels, **options) 44 | except KeyboardInterrupt: 45 | pass 46 | -------------------------------------------------------------------------------- /pyzen/models.py: -------------------------------------------------------------------------------- 1 | #This space left intentionally blank for Django -------------------------------------------------------------------------------- /pyzen/runner.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | 4 | try: 5 | import colorama 6 | except ImportError: 7 | from pyzen import _colorama as colorama 8 | 9 | COLOR_SUCCESS = colorama.Fore.GREEN 10 | COLOR_FAIL = colorama.Fore.RED 11 | COLOR_RESET = colorama.Fore.RESET 12 | 13 | class ColoredTextTestResult(unittest._TextTestResult): 14 | 15 | def addSuccess(self, test): 16 | self.stream.write(COLOR_SUCCESS) 17 | unittest._TextTestResult.addSuccess(self, test) 18 | self.stream.write(COLOR_RESET) 19 | 20 | def addError(self, test, err): 21 | self.stream.write(COLOR_FAIL) 22 | unittest._TextTestResult.addError(self, test, err) 23 | self.stream.write(COLOR_RESET) 24 | 25 | def addFailure(self, test, err): 26 | self.stream.write(COLOR_FAIL) 27 | unittest._TextTestResult.addFailure(self, test, err) 28 | self.stream.write(COLOR_RESET) 29 | 30 | def printErrorList(self, flavour, errors): 31 | for test, err in errors: 32 | self.stream.writeln(self.separator1) 33 | self.stream.write(COLOR_FAIL) 34 | self.stream.writeln("%s: %s" % (flavour,self.getDescription(test))) 35 | self.stream.write(COLOR_RESET) 36 | self.stream.writeln(self.separator2) 37 | self.stream.writeln("%s" % err) 38 | 39 | class ColoredTextTestRunner(unittest.TextTestRunner): 40 | 41 | def __init__(self, *args, **kwargs): 42 | unittest.TextTestRunner.__init__(self, *args, **kwargs) 43 | wrapper = colorama.AnsiToWin32(self.stream) 44 | if wrapper.should_wrap(): 45 | self.stream = wrapper.stream 46 | 47 | def _makeResult(self): 48 | return ColoredTextTestResult(self.stream, self.descriptions, self.verbosity) 49 | 50 | def run(self, test): 51 | "Run the given test case or test suite." 52 | result = self._makeResult() 53 | startTime = time.time() 54 | test(result) 55 | stopTime = time.time() 56 | timeTaken = stopTime - startTime 57 | result.printErrors() 58 | self.stream.writeln(result.separator2) 59 | run = result.testsRun 60 | self.stream.writeln("Ran %d test%s in %.3fs" % 61 | (run, run != 1 and "s" or "", timeTaken)) 62 | self.stream.writeln() 63 | if not result.wasSuccessful(): 64 | self.stream.write(COLOR_FAIL) 65 | self.stream.write("FAILED (") 66 | failed, errored = map(len, (result.failures, result.errors)) 67 | if failed: 68 | self.stream.write("failures=%d" % failed) 69 | if errored: 70 | if failed: self.stream.write(", ") 71 | self.stream.write("errors=%d" % errored) 72 | self.stream.writeln(")") 73 | else: 74 | self.stream.write(COLOR_SUCCESS) 75 | self.stream.writeln("OK") 76 | self.stream.write(COLOR_RESET) 77 | return result 78 | 79 | def get_test_runner(nocolor): 80 | if nocolor: 81 | return unittest.TextTestRunner 82 | else: 83 | return ColoredTextTestRunner -------------------------------------------------------------------------------- /pyzen/simple.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import copy 4 | import imp 5 | import unittest 6 | 7 | from pyzen import core 8 | 9 | def run_tests(argv): 10 | # Do this up here to avoid module errors w/ unittest, probably because 11 | # of my futzing with sys.modules below 12 | result = unittest.TestResult() 13 | result.total = -1 14 | 15 | # Munge sys.path and sys.argv 16 | path = argv[0] 17 | base = os.path.abspath(os.path.dirname(path)) 18 | sys.path.insert(0, base) 19 | sys.argv[:] = argv 20 | 21 | # Create a new __main__ module 22 | mod = imp.new_module('__main__') 23 | mod.__file__ = path 24 | mod.__mtime__ = os.stat(path).st_mtime 25 | compiled = compile(open(path).read(), mod.__file__, 'exec') 26 | sys.modules['__main__'] = mod 27 | 28 | # Run the code 29 | fail = False 30 | try: 31 | exec compiled in mod.__dict__ 32 | except SystemExit, e: 33 | if e.code: 34 | fail = True 35 | 36 | # len(failures)==1 indicates failure 37 | if fail: 38 | result.failures.append(None) 39 | return result 40 | 41 | def main(argv=None): 42 | if argv is None: 43 | argv = sys.argv[1:] 44 | try: 45 | core.main(None, run_tests, argv) 46 | except KeyboardInterrupt: 47 | pass 48 | 49 | if __name__ == '__main__': 50 | main() -------------------------------------------------------------------------------- /pyzen/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from pyzen.ui.base import load_ui 2 | from pyzen.ui import osx 3 | from pyzen.ui import win32 4 | from pyzen.ui import linux 5 | from pyzen.ui import sublime 6 | -------------------------------------------------------------------------------- /pyzen/ui/base.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | def img_path(name): 5 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'img', name) 6 | 7 | class PyZenUIMeta(type): 8 | uis = [] 9 | def __init__(self, name, bases, d): 10 | super(PyZenUIMeta, self).__init__(name, bases, d) 11 | if name != 'PyZenUI': 12 | self.uis.append(self) 13 | 14 | 15 | class PyZenUI(object): 16 | """Base class for PyZen UIs.""" 17 | 18 | __metaclass__ = PyZenUIMeta 19 | 20 | name = None 21 | platform = None 22 | 23 | @classmethod 24 | def enabled(cls): 25 | return sys.platform == cls.platform 26 | 27 | def handle(self, failures, errors, total, time): 28 | if failures or errors: 29 | self.fail(failures, errors, total, time) 30 | else: 31 | self.success(total, time) 32 | 33 | def success(self, total, time): 34 | if total == -1: 35 | msg = 'Ran tests in %0.3f seconds'%time 36 | else: 37 | msg = 'Ran %s test%s in %0.3f seconds'%(total, total==1 and '' or 's', time) 38 | self.notify(False, 'Test Successful', msg, 'green') 39 | 40 | def fail(self, failures, errors, total, time): 41 | if total == -1: 42 | msg = 'Ran tests in %0.3f seconds'%time 43 | else: 44 | submsg = [] 45 | if failures: 46 | submsg.append('failures=%s'%failures) 47 | if errors: 48 | submsg.append('errors=%s'%errors) 49 | msg = 'Ran %s test%s in %0.3f seconds (%s)'%(total, total==1 and '' or 's', time, ' '.join(submsg)) 50 | self.notify(True, 'Test Failure', msg, 'red') 51 | 52 | def notify(self, failure, title, msg, icon): 53 | raise NotImplementedError 54 | 55 | def shutdown(self): 56 | pass 57 | 58 | def load_ui(override): 59 | if override: 60 | if isinstance(override, basestring): 61 | override = set(override.split(',')) 62 | for ui in PyZenUIMeta.uis: 63 | if ui.name in override: 64 | yield ui() 65 | else: 66 | for ui in PyZenUIMeta.uis: 67 | if ui.enabled(): 68 | yield ui() 69 | -------------------------------------------------------------------------------- /pyzen/ui/img/README: -------------------------------------------------------------------------------- 1 | Logo image is from http://commons.wikimedia.org/wiki/File:Ouroboros-Zanaq.png 2 | and is licensed Creative Commons Attribution-Share Alike 2.5 Generic. 3 | 4 | I am unable to find the name of the author, but he goes by Zanaq on Wikipedia. -------------------------------------------------------------------------------- /pyzen/ui/img/green.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/ui/img/green.ico -------------------------------------------------------------------------------- /pyzen/ui/img/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/ui/img/green.png -------------------------------------------------------------------------------- /pyzen/ui/img/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/ui/img/logo.ico -------------------------------------------------------------------------------- /pyzen/ui/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/ui/img/logo.png -------------------------------------------------------------------------------- /pyzen/ui/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 33 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 126 | 127 | -------------------------------------------------------------------------------- /pyzen/ui/img/red.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/ui/img/red.ico -------------------------------------------------------------------------------- /pyzen/ui/img/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderanger/pyzen/87177acd03d2c19a58f6899c2e9c73fd50905807/pyzen/ui/img/red.png -------------------------------------------------------------------------------- /pyzen/ui/linux.py: -------------------------------------------------------------------------------- 1 | from pyzen.ui.base import img_path, PyZenUI 2 | 3 | try: 4 | import pynotify 5 | except ImportError: 6 | pynotify = None 7 | 8 | class LibnotifyUI(PyZenUI): 9 | """A PyZen UI that uses libnotify (via pynotify).""" 10 | 11 | name = 'libnotify' 12 | platform = 'linux2' 13 | 14 | def __init__(self): 15 | self.has_notify = pynotify.init('PyZen') 16 | 17 | @classmethod 18 | def enabled(cls): 19 | return super(LibnotifyUI, cls).enabled() and pynotify is not None 20 | 21 | def notify(self, failure, title, msg, icon): 22 | if self.has_notify: 23 | n = pynotify.Notification(title, msg, 'file://'+img_path(icon+'.png')) 24 | n.show() 25 | -------------------------------------------------------------------------------- /pyzen/ui/osx.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from pyzen.ui.base import img_path, PyZenUI 4 | 5 | class AppleScriptError(Exception): 6 | pass 7 | 8 | 9 | def run_script(script): 10 | proc = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 11 | out, err = proc.communicate(script) 12 | if proc.returncode != 0: 13 | raise AppleScriptError('osascript failure: %s'%err) 14 | out = out.strip() 15 | if out == 'true': 16 | return True 17 | elif out == 'false': 18 | return False 19 | elif out.isdigit(): 20 | return int(out) 21 | else: 22 | return out 23 | 24 | def is_growl_running(): 25 | """Check if Growl is running. Run this before any other Growl functions.""" 26 | script = """ 27 | tell application "System Events" 28 | return count of (every process whose name is "GrowlHelperApp") > 0 29 | end tell 30 | """ 31 | return run_script(script) 32 | 33 | def register_app(): 34 | script= """ 35 | tell application "GrowlHelperApp" 36 | -- Make a list of all the notification types 37 | -- that this script will ever send: 38 | set the notificationsList to {"Test Successful" , "Test Failure"} 39 | 40 | -- Register our script with growl. 41 | -- You can optionally (as here) set a default icon 42 | -- for this script's notifications. 43 | register as application "ZenTest" all notifications notificationsList default notifications notificationsList 44 | end tell""" 45 | run_script(script) 46 | 47 | def notify(type, title, msg, img='logo.png'): 48 | script= """ 49 | tell application "GrowlHelperApp" 50 | notify with name "%s" title "%s" description "%s" application name "ZenTest" image from location "file://%s" 51 | end tell"""%(type, title, msg, img_path(img)) 52 | run_script(script) 53 | 54 | class GrowlUI(PyZenUI): 55 | """A PyZen UI that uses Growl. Only supported on OS X.""" 56 | 57 | name = 'osx' 58 | platform = 'darwin' 59 | 60 | def __init__(self): 61 | self.has_growl = is_growl_running() 62 | if self.has_growl: 63 | register_app() 64 | 65 | def notify(self, failure, title, msg, icon): 66 | if self.has_growl: 67 | type = failure and 'Test Failure' or 'Test Successful' 68 | notify(type, title, msg, icon+'.png') 69 | 70 | 71 | # Random test stuff 72 | if __name__ == '__main__': 73 | if is_growl_running(): 74 | register_app() 75 | notify('Test Successful', 'Test Successful', 'Run 1 test(s) in 0.001 seconds', 'red.png') -------------------------------------------------------------------------------- /pyzen/ui/sublime.py: -------------------------------------------------------------------------------- 1 | import multiprocessing.connection 2 | import os 3 | import sys 4 | 5 | from pyzen.ui.base import PyZenUI 6 | 7 | class SublimeUI(PyZenUI): 8 | """A PyZen UI that talks to Sublime Text 2.""" 9 | 10 | name = 'sublime' 11 | 12 | @classmethod 13 | def _read_pidfile(cls): 14 | if sys.platform == 'darwin': 15 | base_path = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'Sublime Text 2') 16 | else: 17 | return None 18 | pid_path = os.path.join(base_path, 'sublime2.pid') 19 | if not os.path.exists(pid_path): 20 | return None 21 | pid_file = open(pid_path, 'r') 22 | pid = int(pid_file.read().strip()) 23 | pid_file.close() 24 | return pid 25 | 26 | @classmethod 27 | def _conn_path(cls): 28 | pid = cls._read_pidfile() 29 | if not pid: 30 | return None 31 | return '/tmp/.sublimepyzen.%s'%pid 32 | 33 | @classmethod 34 | def enabled(cls): 35 | path = cls._conn_path() 36 | if not path: 37 | return False 38 | try: 39 | client = multiprocessing.connection.Client(path) 40 | client.send(['PING', None]) 41 | client.close() 42 | return True 43 | except Exception: 44 | return False 45 | 46 | def notify(self, failure, title, msg, icon): 47 | path = self._conn_path() 48 | if path: 49 | client = multiprocessing.connection.Client(path) 50 | client.send([os.getcwd(), (failure, title, msg)]) 51 | client.close() 52 | -------------------------------------------------------------------------------- /pyzen/ui/win32/__init__.py: -------------------------------------------------------------------------------- 1 | from pyzen.ui.base import PyZenUI 2 | 3 | class Win32UI(PyZenUI): 4 | """A PyZen UI that uses the Win32 system tray.""" 5 | 6 | name = 'win32' 7 | platform = 'win32' 8 | 9 | def __init__(self): 10 | from pyzen.ui.win32.wrappers import SystrayIconThread 11 | self.thread = SystrayIconThread() 12 | self.thread.start() 13 | 14 | def notify(self, failure, title, msg, icon): 15 | self.thread.notify(title, msg, icon+'.ico') 16 | 17 | def shutdown(self): 18 | self.thread.quit() 19 | self.thread.join() 20 | 21 | 22 | def main(): 23 | import time 24 | t = SystrayIconThread() 25 | t.start() 26 | time.sleep(2) 27 | t.post_message(WM_APP, 1, 2) 28 | time.sleep(1) 29 | print 'Sending quit' 30 | t.quit() 31 | 32 | if __name__ == '__main__': 33 | main() 34 | -------------------------------------------------------------------------------- /pyzen/ui/win32/types.py: -------------------------------------------------------------------------------- 1 | from win32con import * 2 | from ctypes import * 3 | from ctypes.wintypes import * 4 | from pyzen.ui.base import img_path 5 | 6 | NULL = c_void_p() 7 | 8 | class errcheck(object): 9 | 10 | def __init__(self, error_value=None): 11 | self.error_value = error_value 12 | 13 | def __call__(self, result, func, arguments): 14 | if result == self.error_value: 15 | raise WinError() 16 | return result 17 | 18 | # Some basic Windows types 19 | TCHAR = c_char 20 | LPCTSTR = LPCSTR 21 | LPTSTR = LPSTR 22 | HCURSOR = c_ulong 23 | 24 | class GUID(Structure): 25 | _fields_ = [ 26 | ('Data1', c_ulong), 27 | ('Data2', c_ushort), 28 | ('Data3', c_ushort), 29 | ('Data4', c_byte*8), 30 | ] 31 | 32 | # Shell_NotifyIcon 33 | class NOTIFYICONDATA(Structure): 34 | _fields_ = [ 35 | ('cbSize', DWORD), 36 | ('hWnd', HWND), 37 | ('uID', UINT), 38 | ('uFlags', UINT), 39 | ('uCallbackMessage', UINT), 40 | ('hIcon', HICON), 41 | ('szTip', TCHAR*128), 42 | ('dwState', DWORD), 43 | ('dwSTateMask', DWORD), 44 | ('szInfo', TCHAR*256), 45 | ('uTimeout', UINT), # Also is uVersion 46 | ('szInfoTitle', TCHAR*64), 47 | ('dwInfoFlags', DWORD), 48 | ('guidItem', GUID), 49 | ('hBalloonIcon', HICON), 50 | ] 51 | 52 | NIM_ADD = 0 53 | NIM_MODIFY = 1 54 | NIM_DELETE = 2 55 | NIM_SETFOCUS = 3 56 | NIM_SETVERSION = 4 57 | 58 | NIF_MESSAGE = 0x00000001 59 | NIF_ICON = 0x00000002 60 | NIF_TIP = 0x00000004 61 | NIF_STATE = 0x00000008 62 | NIF_INFO = 0x00000010 63 | NIF_GUID = 0x00000020 64 | NIF_REALTIME = 0x00000040 65 | NIF_SHOWTIP = 0x00000080 66 | 67 | NIIF_NONE = 0x0000 68 | NIIF_INFO = 0x0001 69 | NIIF_WARNING = 0x0002 70 | NIIF_ERROR = 0x0003 71 | NIIF_USER = 0x0004 72 | 73 | Shell_NotifyIcon = windll.shell32.Shell_NotifyIcon 74 | Shell_NotifyIcon.argtypes = [DWORD, POINTER(NOTIFYICONDATA)] 75 | Shell_NotifyIcon.restype = BOOL 76 | 77 | # MAKEINTRESOURCE 78 | MAKEINTRESOURCE = lambda n: cast(n, c_char_p) 79 | 80 | # LoadImage 81 | LoadImage = windll.user32.LoadImageA 82 | LoadImage.argtypes = [HINSTANCE, LPCTSTR, UINT, c_int, c_int, UINT] 83 | LoadImage.restype = c_void_p 84 | LoadImage.errcheck = errcheck() 85 | 86 | IMAGE_BITMAP = 0 87 | IMAGE_CURSOR = 2 88 | IMAGE_ICON = 1 89 | 90 | LR_LOADFROMFILE = 16 91 | 92 | # LoadCursor 93 | LoadCursor = windll.user32.LoadCursorA 94 | #LoadCursor.argtypes = [HINSTANCE, LPCTSTR] 95 | LoadCursor.restype = HCURSOR 96 | LoadCursor.errcheck = errcheck() 97 | 98 | IDC_ARROW = MAKEINTRESOURCE(32512) 99 | 100 | # CreateWindowEx 101 | CreateWindowEx = windll.user32.CreateWindowExA 102 | CreateWindowEx.argtypes = [DWORD, LPCTSTR, LPCTSTR, DWORD, c_int, c_int, c_int, c_int, HWND, HMENU, HINSTANCE, LPVOID] 103 | CreateWindowEx.restype = HWND 104 | CreateWindowEx.errcheck = errcheck() 105 | 106 | # RegisterClassEx 107 | LRESULT = c_long 108 | WPARAM = c_int 109 | LPARAM = c_void_p 110 | WNDPROC = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) 111 | #WNDPROC = CFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) 112 | #WNDPROC = WINFUNCTYPE(c_long, c_int, c_uint, c_int, c_int) 113 | 114 | class WNDCLASSEX(Structure): 115 | _fields_ = [ 116 | ('cbSize', UINT), #UINT cbSize; 117 | ('style', UINT), #UINT style; 118 | ('lpfnWndProc', WNDPROC), #WNDPROC lpfnWndProc; 119 | ('cbClsExtra', c_int), #int cbClsExtra; 120 | ('cbWndExtra', c_int), #int cbWndExtra; 121 | ('hInstance', HINSTANCE), #HINSTANCE hInstance; 122 | ('hIcon', HICON), #HICON hIcon; 123 | ('hCursor', HCURSOR), #HCURSOR hCursor; 124 | ('hbrBackground', HBRUSH), #HBRUSH hbrBackground; 125 | ('lpszMenuName', LPCTSTR), #LPCTSTR lpszMenuName; 126 | ('lpszClassName', LPCTSTR), #LPCTSTR lpszClassName; 127 | ('hIconSm', HICON), #HICON hIconSm; 128 | ] 129 | 130 | RegisterClassEx = windll.user32.RegisterClassExA 131 | RegisterClassEx.argtypes = [POINTER(WNDCLASSEX)] 132 | RegisterClassEx.restype = ATOM 133 | RegisterClassEx.errcheck = errcheck() 134 | 135 | # GetClassInfoEx 136 | GetClassInfoEx = windll.user32.GetClassInfoExA 137 | GetClassInfoEx.argtypes = [HINSTANCE, LPCTSTR, POINTER(WNDCLASSEX)] 138 | GetClassInfoEx.restype = BOOL 139 | 140 | # GetModuleHandle 141 | GetModuleHandle = windll.kernel32.GetModuleHandleA 142 | GetModuleHandle.argtypes = [LPCTSTR] 143 | GetModuleHandle.restype = HMODULE 144 | 145 | # GetSystemMetrics 146 | GetSystemMetrics = windll.user32.GetSystemMetrics 147 | GetSystemMetrics.argtypes = [c_int] 148 | GetSystemMetrics.restype = c_int 149 | 150 | SM_CXICON = 11 151 | SM_CYICON = 12 152 | SM_CXSMICON = 49 153 | SM_CYSMICON = 50 154 | 155 | # DefWindowProc 156 | DefWindowProc = windll.user32.DefWindowProcA 157 | DefWindowProc.argtypes = [HWND, UINT, WPARAM, LPARAM] 158 | DefWindowProc.restype = LRESULT 159 | 160 | # GetMessage 161 | class POINT(Structure): 162 | _fields_ = [ 163 | ('x', c_long), # LONG x; 164 | ('y', c_long), # LONG y; 165 | ] 166 | 167 | class MSG(Structure): 168 | _fields_ = [ 169 | ('hwnd', HWND), # HWND hwnd; 170 | ('message', UINT), # UINT message; 171 | ('wParam', WPARAM), # WPARAM wParam; 172 | ('lParam', LPARAM), # LPARAM lParam; 173 | ('time', DWORD), # DWORD time; 174 | ('pt', POINT), # POINT pt; 175 | ] 176 | 177 | GetMessage = windll.user32.GetMessageA 178 | GetMessage.argtypes = [POINTER(MSG), HWND, UINT, UINT] 179 | GetMessage.restype = BOOL 180 | 181 | # TranslateMessage 182 | TranslateMessage = windll.user32.TranslateMessage 183 | TranslateMessage.argtypes = [POINTER(MSG)] 184 | TranslateMessage.restype = BOOL 185 | 186 | # DispatchMessage 187 | DispatchMessage = windll.user32.DispatchMessageA 188 | DispatchMessage.argtypes = [POINTER(MSG)] 189 | DispatchMessage.restype = LRESULT 190 | 191 | # IsDialogMessage 192 | IsDialogMessage = windll.user32.IsDialogMessageA 193 | IsDialogMessage.argtypes = [HWND, POINTER(MSG)] 194 | IsDialogMessage.restype = BOOL 195 | 196 | # SendMessage 197 | SendMessage = windll.user32.SendMessageA 198 | SendMessage.argtype = [HWND, UINT, WPARAM, LPARAM] 199 | SendMessage.restype = LRESULT 200 | 201 | # PostMessage 202 | PostMessage = windll.user32.PostMessageA 203 | PostMessage.argtype = [HWND, UINT, WPARAM, LPARAM] 204 | PostMessage.restype = BOOL 205 | PostMessage.errcheck = errcheck(0) 206 | 207 | # PostQuitMessage 208 | PostQuitMessage = windll.user32.PostQuitMessage 209 | PostQuitMessage.argtypes = [c_int] 210 | 211 | # PostThreadMessage 212 | PostThreadMessage = windll.user32.PostThreadMessageA 213 | PostThreadMessage.argtypes = [DWORD, UINT, WPARAM, LPARAM] 214 | PostThreadMessage.restype = BOOL 215 | PostThreadMessage.errcheck = errcheck(0) 216 | 217 | # UpdateWindow 218 | UpdateWindow = windll.user32.UpdateWindow 219 | UpdateWindow.argtypes = [HWND] 220 | UpdateWindow.restype = BOOL 221 | 222 | # GetWindowThreadProcessId 223 | GetWindowThreadProcessId = windll.user32.GetWindowThreadProcessId 224 | GetWindowThreadProcessId.argtypes = [HWND, POINTER(DWORD)] 225 | GetWindowThreadProcessId.restype = DWORD 226 | 227 | # GetCurrentThreadId 228 | GetCurrentThreadId = windll.kernel32.GetCurrentThreadId 229 | GetCurrentThreadId.argtypes = [] 230 | GetCurrentThreadId.restype = DWORD 231 | 232 | # GetLastError 233 | GetLastError = windll.kernel32.GetLastError 234 | GetLastError.argtypes = [] 235 | GetLastError.restype = DWORD 236 | 237 | # FormatMessage 238 | FormatMessage = windll.kernel32.FormatMessageA 239 | FormatMessage.argtypes = [DWORD, LPCVOID, DWORD, DWORD, POINTER(LPTSTR), DWORD, c_void_p] 240 | FormatMessage.restype = DWORD 241 | 242 | FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 243 | FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000 244 | FORMAT_MESSAGE_FROM_HMODULE = 0x00000800 245 | FORMAT_MESSAGE_FROM_STRING = 0x00000400 246 | FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 247 | FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200 248 | FORMAT_MESSAGE_MAX_WIDTH_MASK = 0x000000FF 249 | 250 | # Misc constants 251 | WM_CREATE = 0x0001 252 | WM_QUIT = 0x0012 253 | WM_INITDIALOG = 0x0110 254 | WM_APP = 0x8000 255 | -------------------------------------------------------------------------------- /pyzen/ui/win32/wrappers.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from pyzen.ui.win32.types import * 4 | 5 | def load_icon(name): 6 | return LoadImage(None, img_path(name), IMAGE_ICON, 16, 16, LR_LOADFROMFILE) 7 | 8 | def load_cursor(name): 9 | return LoadCursor(None, name) 10 | 11 | def systray_add(name, hwnd): 12 | nid = NOTIFYICONDATA() 13 | nid.cbSize = sizeof(NOTIFYICONDATA) 14 | nid.uID = 1 15 | nid.uFlags = NIF_ICON | NIF_TIP 16 | nid.hIcon = load_icon(name) 17 | nid.hWnd = hwnd 18 | tip = 'PyZen' 19 | memmove(addressof(nid)+NOTIFYICONDATA.szTip.offset, tip, min(255, len(tip))) 20 | Shell_NotifyIcon(NIM_ADD, byref(nid)) 21 | 22 | def systray_delete(hwnd): 23 | nid = NOTIFYICONDATA() 24 | nid.cbSize = sizeof(NOTIFYICONDATA) 25 | nid.uID = 1 26 | nid.hWnd = hwnd 27 | Shell_NotifyIcon(NIM_DELETE, byref(nid)) 28 | 29 | def systray_modify(name, title, msg, hwnd): 30 | nid = NOTIFYICONDATA() 31 | nid.cbSize = sizeof(NOTIFYICONDATA) 32 | nid.uID = 1 33 | nid.uFlags = NIF_ICON | NIF_INFO | NIF_TIP 34 | nid.dwInfoFlags = NIIF_USER 35 | nid.hIcon = load_icon(name) 36 | tip = 'PyZen: '+title 37 | memmove(addressof(nid)+NOTIFYICONDATA.szTip.offset, tip, min(255, len(tip))) 38 | memmove(addressof(nid)+NOTIFYICONDATA.szInfo.offset, msg, min(255, len(msg))) 39 | memmove(addressof(nid)+NOTIFYICONDATA.szInfoTitle.offset, title, min(63, len(title))) 40 | nid.uTimeout = 10000 41 | nid.hWnd = hwnd 42 | Shell_NotifyIcon(NIM_MODIFY, byref(nid)) 43 | 44 | def create_window(name, wndproc): 45 | wc = WNDCLASSEX() 46 | wc.cbSize = sizeof(WNDCLASSEX) 47 | wc.lpfnWndProc = WNDPROC(wndproc) 48 | wc.lpszClassName = name + 'Window' 49 | wc.hInstance = GetModuleHandle(None) 50 | wc.hIcon = load_icon('logo.ico') 51 | RegisterClassEx(pointer(wc)) 52 | return CreateWindowEx(0, wc.lpszClassName, name, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, wc.hInstance, 0) 53 | 54 | def message_loop(hwnd, wndproc): 55 | msg = MSG() 56 | while 1: 57 | got_message = GetMessage(byref(msg), None, 0, 0) 58 | if got_message == 0 or got_message == -1: 59 | wndproc(hwnd, WM_QUIT, msg.wParam, msg.lParam) 60 | break 61 | try: 62 | if IsDialogMessage(hwnd, byref(msg)): 63 | continue 64 | TranslateMessage(byref(msg)) 65 | if msg.message == WM_APP: 66 | wndproc(hwnd, msg.message, msg.wParam, msg.lParam) 67 | else: 68 | DispatchMessage(byref(msg)) 69 | except WindowsError: 70 | print 'Error processing msg(%s, %s, %s)'%(msg.message, msg.wParam, msg.lParam) 71 | continue 72 | 73 | class NotifyData(Structure): 74 | _fields_ = [ 75 | ('title', c_char_p), 76 | ('msg', c_char_p), 77 | ('icon', c_char_p), 78 | ] 79 | 80 | class SystrayIconThread(threading.Thread): 81 | 82 | def run(self): 83 | self.hwnd = create_window('PyZen', self.window_proc) 84 | message_loop(self.hwnd, self.window_proc) 85 | 86 | def window_proc(self, hwnd, msg, wparam, lparam): 87 | if msg == WM_CREATE: 88 | systray_add('green.ico', hwnd) 89 | #systray_modify('green.ico', 'Message', 'Testing', hwnd) 90 | return True 91 | if msg == WM_QUIT: 92 | systray_delete(hwnd) 93 | return True 94 | if msg == WM_APP: 95 | pnd = cast(lparam, POINTER(NotifyData)) 96 | nd = pnd.contents 97 | systray_modify(nd.icon, nd.title, nd.msg, hwnd) 98 | return True 99 | return DefWindowProc(hwnd, msg, wparam, lparam) 100 | 101 | def post_message(self, msg, wparam, lparam): 102 | PostThreadMessage(self.ident, msg, wparam, lparam) 103 | 104 | def quit(self): 105 | self.post_message(WM_QUIT, 0, 0) 106 | 107 | def notify(self, title, msg, icon): 108 | nd = NotifyData() 109 | nd.title = title 110 | nd.msg = msg 111 | nd.icon = icon 112 | self.post_message(WM_APP, 0, cast(pointer(nd), LPARAM)) 113 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: iso-8859-1 -*- 3 | import os 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name = 'PyZen', 9 | version = '0.3.2', 10 | packages = find_packages(), 11 | package_data = {'pyzen.ui': ['img/*']}, 12 | author = 'Noah Kantrowitz', 13 | author_email = 'noah@coderanger.net', 14 | description = 'Continuous testing for paranoid developers.', 15 | long_description = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), 16 | license = 'BSD', 17 | keywords = 'test unittest continuous django', 18 | url = 'http://github.com/coderanger/pyzen', 19 | classifiers = [ 20 | #'Development Status :: 1 - Planning', 21 | #'Development Status :: 2 - Pre-Alpha', 22 | #'Development Status :: 3 - Alpha', 23 | 'Development Status :: 4 - Beta', 24 | #'Development Status :: 5 - Production/Stable', 25 | #'Development Status :: 6 - Mature', 26 | #'Development Status :: 7 - Inactive', 27 | 'Environment :: Console', 28 | 'Environment :: Win32 (MS Windows)', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Natural Language :: English', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | ], 34 | zip_safe = False, 35 | entry_points = { 36 | 'distutils.commands': [ 37 | 'zen = pyzen.distutils:zen', 38 | ], 39 | 'console_scripts': [ 40 | 'pyzen = pyzen.simple:main', 41 | ], 42 | }, 43 | ) 44 | --------------------------------------------------------------------------------