├── .gitignore ├── LICENSE ├── README.md ├── demo.gif ├── example_test.py ├── pytest_osxnotify └── __init__.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Daniel Bader 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pytest-osxnotify 2 | ================ 3 | 4 | [![PyPI](https://img.shields.io/pypi/v/pytest-osxnotify.svg)](https://pypi.python.org/pypi/pytest-osxnotify) 5 | [![PyPI](https://img.shields.io/pypi/dm/pytest-osxnotify.svg)](https://pypi.python.org/pypi/pytest-osxnotify)
6 | ![Demo](https://raw.github.com/dbader/pytest-osxnotify/master/demo.gif) 7 | 8 | A py.test plugin that displays test results using native Mac OS X 9 | notifications (`NSUserNotification`). Works with Python 2.7 and 3.3+ on 10 | Mountain Lion or better. 11 | 12 | 13 | Usage 14 | ----- 15 | 16 | ```shell 17 | $ pip install pytest-osxnotify 18 | $ py.test 19 | ``` 20 | 21 | How to test a change to the plugin 22 | ---------------------------------- 23 | 24 | ```shell 25 | $ virtualenv venv && . venv/bin/activate 26 | $ pip install pytest -r requirements.txt 27 | $ python setup.py install 28 | $ venv/bin/py.test --traceconfig example_test.py -p pytest_osxnotify 29 | ``` 30 | 31 | How to submit a new release to PyPi 32 | ----------------------------------- 33 | 34 | ```shell 35 | $ git tag X.Y.Z -m "Release X.Y.Z" 36 | $ git push --tags 37 | $ python setup.py sdist upload -r pypi 38 | ``` 39 | 40 | Changelog 41 | --------- 42 | 43 | - **0.1.7** 44 | - Tweaked the notification delay to ensure that success/error messages 45 | are displayed. 46 | 47 | Meta 48 | ---- 49 | 50 | Daniel Bader – [@dbader_org](https://twitter.com/dbader_org>) – mail@dbader.org 51 | 52 | Distributed under the MIT license. See ``LICENSE`` for more information. 53 | 54 | https://github.com/dbader/pytest-osxnotify 55 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbader/pytest-osxnotify/bbfed78261dae2090887fcc7de1b36f2678c1869/demo.gif -------------------------------------------------------------------------------- /example_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is just a test file you can use to test the plugin for development. 3 | See README.md for more. 4 | 5 | """ 6 | 7 | def test_example1(): 8 | assert True 9 | 10 | def test_example2(): 11 | assert True 12 | 13 | def test_example3(): 14 | assert True 15 | -------------------------------------------------------------------------------- /pytest_osxnotify/__init__.py: -------------------------------------------------------------------------------- 1 | # pytest-osxnotify 2 | # Mac OS X notification center support for py.test 3 | # Requirements: pyobjc-core 4 | import time 5 | 6 | # Lazy-import pyobjc to work around a conflict with pytest-xdist 7 | # looponfail on Python 3.3 8 | objc = None 9 | 10 | 11 | def pytest_addoption(parser): 12 | """ 13 | Adds options to control notifications. 14 | 15 | """ 16 | group = parser.getgroup('terminal reporting') 17 | group.addoption( 18 | '--osxnotify', 19 | dest='osxnotify', 20 | default=True, 21 | help='Enable Mac OS X notification center notifications.' 22 | ) 23 | 24 | 25 | def pytest_sessionstart(session): 26 | if session.config.option.osxnotify: 27 | notify('py.test', 'Running tests...') 28 | 29 | 30 | def pytest_terminal_summary(terminalreporter): 31 | if not terminalreporter.config.option.osxnotify: 32 | return 33 | tr = terminalreporter 34 | passes = len(tr.stats.get('passed', [])) 35 | fails = len(tr.stats.get('failed', [])) 36 | skips = len(tr.stats.get('deselected', [])) 37 | errors = len(tr.stats.get('error', [])) 38 | if errors + passes + fails + skips == 0: 39 | msg = 'No tests ran' 40 | elif passes and not (fails or errors): 41 | msg = 'Success - %i Passed' % passes 42 | elif not (skips or errors): 43 | msg = '%s Passed %s Failed' % (passes, fails) 44 | else: 45 | msg = '%s Passed %s Failed %s Errors %s Skipped' % ( 46 | passes, fails, errors, skips 47 | ) 48 | notify('py.test', msg) 49 | # Delay a bit to ensure that all notifications get displayed 50 | # even if py.test finishes very quickly. 51 | # It's unfortunate that this is a magic value for now. 52 | time.sleep(0.3) 53 | 54 | 55 | def swizzle(cls, SEL, func): 56 | old_IMP = getattr(cls, SEL, None) 57 | if old_IMP is None: 58 | # This will work on OS X <= 10.9 59 | old_IMP = cls.instanceMethodForSelector_(SEL) 60 | 61 | def wrapper(self, *args, **kwargs): 62 | return func(self, old_IMP, *args, **kwargs) 63 | 64 | new_IMP = objc.selector( 65 | wrapper, 66 | selector=old_IMP.selector, 67 | signature=old_IMP.signature 68 | ) 69 | objc.classAddMethod(cls, SEL.encode(), new_IMP) 70 | 71 | 72 | def notify(title, subtitle=None): 73 | """ 74 | Display a NSUserNotification on Mac OS X >= 10.8 75 | 76 | """ 77 | global objc 78 | if not objc: 79 | objc = __import__('objc') 80 | swizzle( 81 | objc.lookUpClass('NSBundle'), 82 | 'bundleIdentifier', 83 | swizzled_bundleIdentifier 84 | ) 85 | 86 | NSUserNotification = objc.lookUpClass('NSUserNotification') 87 | NSUserNotificationCenter = objc.lookUpClass('NSUserNotificationCenter') 88 | if not NSUserNotification or not NSUserNotificationCenter: 89 | print('NSUserNotifcation is not supported by your version of Mac OS X') 90 | return 91 | 92 | notification = NSUserNotification.alloc().init() 93 | notification.setTitle_(str(title)) 94 | if subtitle: 95 | notification.setSubtitle_(str(subtitle)) 96 | 97 | notification_center = NSUserNotificationCenter.defaultUserNotificationCenter() 98 | notification_center.deliverNotification_(notification) 99 | 100 | 101 | def swizzled_bundleIdentifier(self, original): 102 | """ 103 | Swizzle [NSBundle bundleIdentifier] to make NSUserNotifications work. 104 | 105 | To post NSUserNotifications OS X requires the binary to be packaged 106 | as an application bundle. To circumvent this restriction, we modify 107 | `bundleIdentifier` to return a fake bundle identifier. 108 | 109 | Original idea for this approach by Norio Numura: 110 | https://github.com/norio-nomura/usernotification 111 | 112 | """ 113 | return 'com.apple.terminal' 114 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyobjc-core 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | __VERSION__ = '0.1.7' 4 | __URL__ = 'https://github.com/dbader/pytest-osxnotify' 5 | __DOWNLOAD_URL__ = (__URL__ + '/tarball/' + __VERSION__) 6 | 7 | setup( 8 | author='Daniel Bader', 9 | author_email='mail@dbader.org', 10 | version=__VERSION__, 11 | description='OS X notifications for py.test results.', 12 | url=__URL__, 13 | download_url=__DOWNLOAD_URL__, 14 | name='pytest-osxnotify', 15 | keywords=[ 16 | 'pytest', 'pytest-', 'osx', 'notifications', 'mountainlion', 17 | 'notificationcenter', 'py.test'], 18 | packages=['pytest_osxnotify'], 19 | entry_points={'pytest11': ['pytest_osxnotify = pytest_osxnotify', ]}, 20 | install_requires=[ 21 | 'pyobjc-core', 22 | ], 23 | classifiers=[ 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3.3', 28 | 'Programming Language :: Python :: 3.4', 29 | ] 30 | ) 31 | --------------------------------------------------------------------------------