├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── scripts │ │ ├── release.sh │ │ └── setup.sh └── workflows │ ├── create.yml │ └── push.yml ├── .gitignore ├── AUTHORS.txt ├── CHANGELOG ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── RELEASE.md ├── oscpy ├── __init__.py ├── cli.py ├── client.py ├── parser.py ├── server.py └── stats.py ├── setup.cfg ├── setup.py ├── tests ├── performances.py ├── test_cli.py ├── test_client.py ├── test_parser.py ├── test_server.py └── test_stats.py └── tools └── hooks └── pre-commit /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | open_collective: kivy 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Code example showing the issue: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Logs/output** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Platform (please complete the following information):** 23 | - OS: [e.g. windows 10 /OSX 10.12 /linux/android 8/IOS 12…] 24 | - Python version. 25 | - release or git branch/commit 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | if [[ ! $GITHUB_REF =~ ^refs/tags/ ]]; then 5 | exit 0 6 | fi 7 | 8 | owner=kivy 9 | repository=oscpy 10 | access_token=$GITHUB_TOKEN 11 | 12 | changelog_lines=$(grep -n "===" CHANGELOG | head -n2 | tail -n1 | cut -d':' -f1) 13 | 14 | changelog=$(head -n $(( $changelog_lines - 2)) CHANGELOG | awk '{printf("%s\\n",$0)} END {print ""}') 15 | 16 | tag=${GITHUB_REF#refs/tags/} 17 | draft="false" 18 | prerelease="false" 19 | version_name="$tag" 20 | message="Release $tag" 21 | 22 | pip install -U setuptools wheel twine 23 | python setup.py sdist bdist_wheel 24 | python -m twine check dist/* 25 | 26 | twine="python -m twine upload --disable-progress-bar" 27 | 28 | if [[ $GITHUB_REF =~ -test$ ]]; then 29 | twine="$twine --repository-url https://test.pypi.org/legacy/" 30 | draft="true" 31 | prerelease="true" 32 | message="test release $tag" 33 | fi 34 | 35 | API_JSON="{ 36 | \"tag_name\": \"$tag\", 37 | \"name\": \"$version_name\", 38 | \"body\": \"$message\n$changelog\", 39 | \"draft\": $draft, 40 | \"prerelease\": $prerelease 41 | }" 42 | 43 | echo $API_JSON 44 | 45 | $twine dist/* 46 | curl --data "$API_JSON"\ 47 | Https://api.github.com/repos/$owner/$repository/releases?access_token=$access_token 48 | -------------------------------------------------------------------------------- /.github/actions/scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | pip install .[dev] 4 | pytest 5 | -------------------------------------------------------------------------------- /.github/workflows/create.yml: -------------------------------------------------------------------------------- 1 | on: create 2 | name: on create 3 | jobs: 4 | Release: 5 | name: base 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: release 10 | run: .github/actions/scripts/release.sh 11 | env: 12 | TWINE_USERNAME: oscpy 13 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_OAUTH_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: on push 3 | jobs: 4 | Tests: 5 | name: base 6 | strategy: 7 | matrix: 8 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.9', 'pypy3.10'] 9 | # os: ['ubuntu-latest', 'windows-latest', 'macOs-latest'] 10 | os: ['ubuntu-latest', 'windows-latest'] 11 | 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Setup python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python }} 19 | allow-prereleases: true 20 | - name: prepare 21 | run: pip install -U setuptools wheel 22 | - name: install 23 | run: pip install .[dev,ci] 24 | - name: test 25 | run: python -m pytest --reruns 5 tests/ --cov oscpy/ --cov-branch 26 | - name: coveralls 27 | run: python -m coveralls 28 | env: 29 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.c 4 | *.pyd 5 | *.egg-info 6 | .pytest_cache 7 | build 8 | dist 9 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Gabriel Pettier 2 | Andre Miras 3 | Armin Sebastian 4 | Ray Chang 5 | Tamas Levai 6 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.6.1 2 | ====== 3 | 4 | Fix header mismatch that cause the listener thread to terminate 5 | Add a flag to disable message address validation 6 | Fix binary parser to handle correctly handle blob parsing 7 | Add support for t typehint of TUIO 2.0 protocol, used by displax tag 8 | 9 | v0.6.0 10 | ====== 11 | 12 | Allow intercepting (and logging) errors in callback instead of letting listener crash. 13 | 14 | v0.5.0 15 | ====== 16 | 17 | Allow accessing sender's ip/port from outside of answer() 18 | Fix get_sender() error on IOS/Android. 19 | Fix encoding issue in OSCThreadServer.unbind(). 20 | 21 | v0.4.0 22 | ====== 23 | 24 | Unicode support, servers and clients can declare an encoding for strings. 25 | Fix timeout bug after 15mn without any activity. 26 | Allow answering to a specific port. 27 | Allow callbacks to get the addresses they were called with. 28 | Add default_handler option for a server, to get all messages that didn't match any known address. 29 | Allow using default socket implicitly in OSCThreadServer.stop() 30 | Add statistics collections (messages/bytes sent/received) 31 | Add default routes to probe the server about existing routes and usage statistics. 32 | Improve reliability of stopping server. 33 | Add support for Midi messages 34 | Add support for True/False/Nil/Infinitum messages 35 | Add support for Symbol messages (treated as strings) 36 | Test/Coverage of more python versions and OSs in CI 37 | Improve documentation (README, CHANGELOG, and CONTRIBUTING) 38 | 39 | v0.3.0 40 | ====== 41 | 42 | increase test coverage 43 | remove notice about WIP status 44 | add test/fix for refusing to send unicode strings 45 | use bytearray for blobs, since object catch wrong types 46 | fix client code example in readme 47 | allow binding methods with the @address_method decorator 48 | add test for @address decorator 49 | clarify that @address decorator won't work for methods 50 | add test and warnings about AF_UNIX not working on windows 51 | fix negative letter matching 52 | check exception is raised when no default socket 53 | add test and implementation for advanced_matching 54 | 55 | 56 | v0.2.0 57 | ====== 58 | 59 | ignore build and dist dirs 60 | fix inet/unix comparison in performance test 61 | cleanup & documentation & performance test 62 | first minor version 63 | 64 | 65 | v0.1.3, v0.1.2, v0.1.1 66 | ====================== 67 | fix setup.py classifiers 68 | 69 | 70 | v0.1.0 Initial release 71 | ====================== 72 | 73 | OSCThreadServer implementation and basic tests 74 | OSCClient implementation 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### CONTRIBUTING 2 | 3 | This software is open source and welcomes open contributions, there are just 4 | a few guidelines, if you are unsure about them, please ask and guidance will be 5 | provided. 6 | 7 | - The code is [hosted on GitHub](https://github.com/kivy/oscpy) and 8 | development happens here, using the tools provided by the platform. 9 | Contributions are accepted in the form of Pull Requests. Bugs are to be 10 | reported in the issue tracker provided there. 11 | 12 | - Please follow [PEP8](https://www.python.org/dev/peps/pep-0008/), hopefully 13 | your editor can be configured to automatically enforce it, but you can also 14 | install (using pip) and run `pycodestyle` from the command line, 15 | to get a report about it. 16 | 17 | - Avoid lowering the test coverage, it's hard to achieve 100%, but staying as 18 | close to it as possible is a good way to improve quality by catching bugs as 19 | early as possible. Tests are ran by Travis, and the coverage is 20 | evaluated by Coveralls, so you'll get a report about your contribution 21 | breaking any test, and the evolution of coverage, but you can also check that 22 | locally before sending the contribution, by using `pytest --cov-report 23 | term-missing --cov oscpy`, you can also use `pytest --cov-report html --cov 24 | oscpy` to get an html report that you can open in your browser. 25 | 26 | - Please try to conform to the style of the codebase, if you have a question, 27 | just ask. 28 | 29 | - Please keep performance in mind when editing the code, if you 30 | see room for improvement, share your suggestions by opening an issue, 31 | or open a pull request directly. 32 | 33 | - Please keep in mind that the code you contribute will be subject to the MIT 34 | license, don't include code if it's not under a compatible license, and you 35 | are not the copyright holder. 36 | 37 | #### Tips 38 | 39 | You can install the package in `editable` mode, with the `dev` option, 40 | to easily have all the required tools to check your edits. 41 | 42 | pip install --editable .[dev] 43 | 44 | You can make sure the tests are ran before pushing by using the git hook. 45 | 46 | cp tools/hooks/pre-commit .git/hooks/ 47 | 48 | If you are unsure of the meaning of the pycodestyle output, you can use the 49 | --show-pep8 flag to learn more about the errors. 50 | 51 | pycodestyle --show-pep8 52 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Gabriel Pettier & al 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### OSCPy 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/kivy/oscpy/badge.svg?branch=master)](https://coveralls.io/github/kivy/oscpy?branch=master) 4 | CI is done by Github Checks, see the current commit for build status. 5 | 6 | 7 | A modern implementation of OSC for python2/3. 8 | 9 | #### What is OSC. 10 | 11 | OpenSoundControl is an UDP based network protocol, that is designed for fast 12 | dispatching of time-sensitive messages, as the name suggests, it was designed 13 | as a replacement for MIDI, but applies well to other situations. The protocol is 14 | simple to use, OSC addresses look like http URLs, and accept various basic 15 | types, such as string, float, int, etc. You can think of it basically as an 16 | http POST, with less overhead. 17 | 18 | You can learn more about OSC on [OpenSoundControl.org](http://opensoundcontrol.org/) 19 | 20 | #### Goals 21 | 22 | - python2.7/3.6+ compatibility (can be relaxed more on the python3 side 23 | if needed, but nothing before 2.7 will be supported) 24 | - fast 25 | - easy to use 26 | - robust (returns meaningful errors in case of malformed messages, 27 | always do the right thing on correct messages, and by default intercept+log 28 | the exceptions raised by callbacks) 29 | - separation of concerns (message parsing vs communication) 30 | - sync and async compatibility (threads, asyncio, trio…) 31 | - clean and easy to read code 32 | 33 | #### Features 34 | 35 | - serialize and parse OSC data types/Messages/Bundles 36 | - a thread based udp server to open sockets (INET or UNIX) and bind callbacks on osc addresses on them 37 | - a simple client 38 | 39 | #### Install 40 | ```sh 41 | pip install oscpy 42 | ``` 43 | 44 | #### Usage 45 | 46 | Server (thread) 47 | 48 | ```python 49 | from oscpy.server import OSCThreadServer 50 | from time import sleep 51 | 52 | def callback(*values): 53 | print("got values: {}".format(values)) 54 | 55 | osc = OSCThreadServer() # See sources for all the arguments 56 | 57 | # You can also use an \*nix socket path here 58 | sock = osc.listen(address='0.0.0.0', port=8000, default=True) 59 | osc.bind(b'/address', callback) 60 | sleep(1000) 61 | osc.stop() # Stop the default socket 62 | 63 | osc.stop_all() # Stop all sockets 64 | 65 | # Here the server is still alive, one might call osc.listen() again 66 | 67 | osc.terminate_server() # Request the handler thread to stop looping 68 | 69 | osc.join_server() # Wait for the handler thread to finish pending tasks and exit 70 | ``` 71 | 72 | or you can use the decorator API. 73 | 74 | Server (thread) 75 | 76 | ```python 77 | from oscpy.server import OSCThreadServer 78 | from time import sleep 79 | 80 | osc = OSCThreadServer() 81 | sock = osc.listen(address='0.0.0.0', port=8000, default=True) 82 | 83 | @osc.address(b'/address') 84 | def callback(*values): 85 | print("got values: {}".format(values)) 86 | 87 | sleep(1000) 88 | osc.stop() 89 | ``` 90 | 91 | Servers are also client, in the sense they can send messages and answer to 92 | messages from other servers 93 | 94 | ```python 95 | from oscpy.server import OSCThreadServer 96 | from time import sleep 97 | 98 | osc_1 = OSCThreadServer() 99 | osc_1.listen(default=True) 100 | 101 | @osc_1.address(b'/ping') 102 | def ping(*values): 103 | print("ping called") 104 | if True in values: 105 | cont.append(True) 106 | else: 107 | osc_1.answer(b'/pong') 108 | 109 | osc_2 = OSCThreadServer() 110 | osc_2.listen(default=True) 111 | 112 | @osc_2.address(b'/pong') 113 | def pong(*values): 114 | print("pong called") 115 | osc_2.answer(b'/ping', [True]) 116 | 117 | osc_2.send_message(b'/ping', [], *osc_1.getaddress()) 118 | 119 | timeout = time() + 1 120 | while not cont: 121 | if time() > timeout: 122 | raise OSError('timeout while waiting for success message.') 123 | ``` 124 | 125 | 126 | Server (async) (TODO!) 127 | 128 | ```python 129 | from oscpy.server import OSCThreadServer 130 | 131 | with OSCAsyncServer(port=8000) as OSC: 132 | for address, values in OSC.listen(): 133 | if address == b'/example': 134 | print("got {} on /example".format(values)) 135 | else: 136 | print("unknown address {}".format(address)) 137 | ``` 138 | 139 | Client 140 | 141 | ```python 142 | from oscpy.client import OSCClient 143 | 144 | address = "127.0.0.1" 145 | port = 8000 146 | 147 | osc = OSCClient(address, port) 148 | for i in range(10): 149 | osc.send_message(b'/ping', [i]) 150 | ``` 151 | 152 | #### Unicode 153 | 154 | By default, the server and client take bytes (encoded strings), not unicode 155 | strings, for osc addresses as well as osc strings. However, you can pass an 156 | `encoding` parameter to have your strings automatically encoded and decoded by 157 | them, so your callbacks will get unicode strings (unicode in python2, str in 158 | python3). 159 | 160 | ```python 161 | osc = OSCThreadServer(encoding='utf8') 162 | osc.listen(default=True) 163 | 164 | values = [] 165 | 166 | @osc.address(u'/encoded') 167 | def encoded(*val): 168 | for v in val: 169 | assert not isinstance(v, bytes) 170 | values.append(val) 171 | 172 | send_message( 173 | u'/encoded', 174 | [u'hello world', u'ééééé ààààà'], 175 | *osc.getaddress(), encoding='utf8') 176 | ``` 177 | 178 | (`u` literals added here for clarity). 179 | 180 | #### CLI 181 | 182 | OSCPy provides an "oscli" util, to help with debugging: 183 | - `oscli dump` to listen for messages and dump them 184 | - `oscli send` to send messages or bundles to a server 185 | 186 | See `oscli -h` for more information. 187 | 188 | #### GOTCHAS 189 | 190 | - `None` values are not allowed in serialization 191 | - Unix-type sockets must not already exist when you listen() on them 192 | 193 | #### TODO 194 | 195 | - real support for timetag (currently only supports optionally 196 | dropping late bundles, not delaying those with timetags in the future) 197 | - support for additional argument types 198 | - an asyncio-oriented server implementation 199 | - examples & documentation 200 | 201 | #### Contributing 202 | 203 | Check out our [contribution guide](CONTRIBUTING.md) and feel free to improve OSCPy. 204 | 205 | #### License 206 | 207 | OSCPy is released under the terms of the MIT License. 208 | Please see the [LICENSE.txt](LICENSE.txt) file. 209 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ### How to release 2 | 3 | For test releases, use a -test suffix to the tag, for example "v0.6.0-test", 4 | for actual releases, just use the normal version name, for example "v0.6.0". 5 | 6 | 1. Update `__version__` in `oscpy/__init__.py` 7 | 1. Update `CHANGELOG.md` 8 | 1. Call `git commit oscpy/__init__.py CHANGELOG.md` 9 | 1. Call `git tag --sign [version]` 10 | 1. Call `git push --tags` 11 | -------------------------------------------------------------------------------- /oscpy/__init__.py: -------------------------------------------------------------------------------- 1 | """See README.md for package information.""" 2 | 3 | __version__ = "0.6.2-dev" 4 | -------------------------------------------------------------------------------- /oscpy/cli.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | """OSCPy command line tools""" 3 | 4 | from argparse import ArgumentParser 5 | from time import sleep 6 | from sys import exit, stderr 7 | from ast import literal_eval 8 | 9 | from oscpy.client import send_message 10 | from oscpy.server import OSCThreadServer 11 | from oscpy.stats import Stats 12 | 13 | 14 | def _send(options): 15 | def _parse(s): 16 | try: 17 | return literal_eval(s) 18 | except: 19 | return s 20 | 21 | stats = Stats() 22 | for i in range(options.repeat): 23 | stats += send_message( 24 | options.address, 25 | [_parse(x) for x in options.message], 26 | options.host, 27 | options.port, 28 | safer=options.safer, 29 | encoding=options.encoding, 30 | encoding_errors=options.encoding_errors 31 | ) 32 | print(stats) 33 | 34 | 35 | def __dump(options): 36 | def dump(address, *values): 37 | print(u'{}: {}'.format( 38 | address.decode('utf8'), 39 | ', '.join( 40 | '{}'.format( 41 | v.decode(options.encoding or 'utf8') 42 | if isinstance(v, bytes) 43 | else v 44 | ) 45 | for v in values if values 46 | ) 47 | )) 48 | 49 | osc = OSCThreadServer( 50 | encoding=options.encoding, 51 | encoding_errors=options.encoding_errors, 52 | default_handler=dump 53 | ) 54 | osc.listen( 55 | address=options.host, 56 | port=options.port, 57 | default=True 58 | ) 59 | return osc 60 | 61 | 62 | def _dump(options): # pragma: no cover 63 | osc = __dump(options) 64 | try: 65 | while True: 66 | sleep(10) 67 | finally: 68 | osc.stop() 69 | 70 | 71 | def init_parser(): 72 | parser = ArgumentParser(description='OSCPy command line interface') 73 | parser.set_defaults(func=lambda *x: parser.print_usage(stderr)) 74 | 75 | subparser = parser.add_subparsers() 76 | 77 | send = subparser.add_parser('send', help='send an osc message to a server') 78 | send.set_defaults(func=_send) 79 | send.add_argument('--host', '-H', action='store', default='localhost', 80 | help='host (ip or name) to send message to.') 81 | send.add_argument('--port', '-P', action='store', type=int, default='8000', 82 | help='port to send message to.') 83 | send.add_argument('--encoding', '-e', action='store', default='utf-8', 84 | help='how to encode the strings') 85 | send.add_argument('--encoding_errors', '-E', action='store', default='replace', 86 | help='how to treat string encoding issues') 87 | send.add_argument('--safer', '-s', action='store_true', 88 | help='wait a little after sending message') 89 | send.add_argument('--repeat', '-r', action='store', type=int, default=1, 90 | help='how many times to send the message') 91 | 92 | send.add_argument('address', action='store', 93 | help='OSC address to send the message to.') 94 | send.add_argument('message', nargs='*', 95 | help='content of the message, separated by spaces.') 96 | 97 | dump = subparser.add_parser('dump', help='listen for messages and print them') 98 | dump.set_defaults(func=_dump) 99 | dump.add_argument('--host', '-H', action='store', default='localhost', 100 | help='host (ip or name) to send message to.') 101 | dump.add_argument('--port', '-P', action='store', type=int, default='8000', 102 | help='port to send message to.') 103 | dump.add_argument('--encoding', '-e', action='store', default='utf-8', 104 | help='how to encode the strings') 105 | dump.add_argument('--encoding_errors', '-E', action='store', default='replace', 106 | help='how to treat string encoding issues') 107 | 108 | # bridge = parser.add_parser('bridge', help='listen for messages and redirect them to a server') 109 | return parser 110 | 111 | 112 | def main(): # pragma: no cover 113 | parser = init_parser() 114 | options = parser.parse_args() 115 | exit(options.func(options)) 116 | -------------------------------------------------------------------------------- /oscpy/client.py: -------------------------------------------------------------------------------- 1 | """Client API. 2 | 3 | This module provides both a functional and an object oriented API. 4 | 5 | You can use directly `send_message`, `send_bundle` and the `SOCK` socket 6 | that is created by default, or use `OSCClient` to store parameters common 7 | to your requests and avoid repeating them in your code. 8 | """ 9 | 10 | import socket 11 | from time import sleep 12 | from sys import platform 13 | 14 | from oscpy.parser import format_message, format_bundle 15 | from oscpy.stats import Stats 16 | 17 | SOCK = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 18 | 19 | 20 | def send_message( 21 | osc_address, values, ip_address, port, sock=SOCK, safer=False, 22 | encoding='', encoding_errors='strict' 23 | ): 24 | """Send an osc message to a socket address. 25 | 26 | - `osc_address` is the osc endpoint to send the data to (e.g b'/test') 27 | it should be a bytestring 28 | - `values` is the list of values to send, they can be any supported osc 29 | type (bytestring, float, int, blob...) 30 | - `ip_address` can either be an ip address if the used socket is of 31 | the AF_INET family, or a filename if the socket is of type AF_UNIX 32 | - `port` value will be ignored if socket is of type AF_UNIX 33 | - `sock` should be a socket object, the client's default socket can be 34 | used as default 35 | - the `safer` parameter allows to wait a little after sending, to make 36 | sure the message is actually sent before doing anything else, 37 | should only be useful in tight loop or cpu-busy code. 38 | - `encoding` if defined, will be used to encode/decode all 39 | strings sent/received to/from unicode/string objects, if left 40 | empty, the interface will only accept bytes and return bytes 41 | to callback functions. 42 | - `encoding_errors` if `encoding` is set, this value will be 43 | used as `errors` parameter in encode/decode calls. 44 | 45 | examples: 46 | send_message(b'/test', [b'hello', 1000, 1.234], 'localhost', 8000) 47 | send_message(b'/test', [], '192.168.0.1', 8000, safer=True) 48 | 49 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 50 | send_message(b'/test', [], '192.168.0.1', 8000, sock=sock, safer=True) 51 | 52 | # unix sockets work on linux and osx, and over unix platforms, 53 | # but not windows 54 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) 55 | send_message(b'/some/address', [1, 2, 3], b'/tmp/sock') 56 | 57 | """ 58 | if platform != 'win32' and sock.family == socket.AF_UNIX: 59 | address = ip_address 60 | else: 61 | address = (ip_address, port) 62 | 63 | message, stats = format_message( 64 | osc_address, values, encoding=encoding, 65 | encoding_errors=encoding_errors 66 | ) 67 | 68 | sock.sendto(message, address) 69 | if safer: 70 | sleep(10e-9) 71 | 72 | return stats 73 | 74 | 75 | def send_bundle( 76 | messages, ip_address, port, timetag=None, sock=None, safer=False, 77 | encoding='', encoding_errors='strict' 78 | ): 79 | """Send a bundle built from the `messages` iterable. 80 | 81 | each item in the `messages` list should be a two-tuple of the form: 82 | (address, values). 83 | 84 | example: 85 | ( 86 | ('/create', ['name', 'value']), 87 | ('/select', ['name']), 88 | ('/update', ['name', 'value2']), 89 | ('/delete', ['name']), 90 | ) 91 | 92 | `timetag` is optional but can be a float of the number of seconds 93 | since 1970 when the events described in the bundle should happen. 94 | 95 | See `send_message` documentation for the other parameters. 96 | """ 97 | if not sock: 98 | sock = SOCK 99 | bundle, stats = format_bundle( 100 | messages, timetag=timetag, encoding=encoding, 101 | encoding_errors=encoding_errors 102 | ) 103 | sock.sendto(bundle, (ip_address, port)) 104 | if safer: 105 | sleep(10e-9) 106 | 107 | return stats 108 | 109 | 110 | class OSCClient(object): 111 | """Class wrapper for the send_message and send_bundle functions. 112 | 113 | Allows to define `address`, `port` and `sock` parameters for all calls. 114 | If encoding is provided, all string values will be encoded 115 | into this encoding before being sent. 116 | """ 117 | 118 | def __init__( 119 | self, address, port, sock=None, encoding='', encoding_errors='strict' 120 | ): 121 | """Create an OSCClient. 122 | 123 | `address` and `port` are the destination of messages sent 124 | by this client. See `send_message` and `send_bundle` documentation 125 | for more information. 126 | """ 127 | self.address = address 128 | self.port = port 129 | self.sock = sock or SOCK 130 | self.encoding = encoding 131 | self.encoding_errors = encoding_errors 132 | self.stats = Stats() 133 | 134 | def send_message(self, address, values, safer=False): 135 | """Wrap the module level `send_message` function.""" 136 | stats = send_message( 137 | address, values, self.address, self.port, self.sock, 138 | safer=safer, encoding=self.encoding, 139 | encoding_errors=self.encoding_errors 140 | ) 141 | self.stats += stats 142 | return stats 143 | 144 | def send_bundle(self, messages, timetag=None, safer=False): 145 | """Wrap the module level `send_bundle` function.""" 146 | stats = send_bundle( 147 | messages, self.address, self.port, timetag=timetag, 148 | sock=self.sock, safer=safer, encoding=self.encoding, 149 | encoding_errors=self.encoding_errors 150 | ) 151 | self.stats += stats 152 | return stats 153 | -------------------------------------------------------------------------------- /oscpy/parser.py: -------------------------------------------------------------------------------- 1 | """Parse and format data types, from and to packets that can be sent. 2 | 3 | types are automatically inferred using the `PARSERS` and `WRITERS` members. 4 | 5 | Allowed types are: 6 | int (but not *long* ints) -> osc int 7 | floats -> osc float 8 | bytes (encoded strings) -> osc strings 9 | bytearray (raw data) -> osc blob 10 | 11 | """ 12 | 13 | __all__ = ( 14 | 'parse', 15 | 'read_packet', 'read_message', 'read_bundle', 16 | 'format_bundle', 'format_message', 17 | 'MidiTuple', 18 | ) 19 | 20 | 21 | from struct import Struct, pack, unpack_from, calcsize 22 | from time import time 23 | import sys 24 | from collections import Counter, namedtuple 25 | from oscpy.stats import Stats 26 | 27 | if sys.version_info.major > 2: # pragma: no cover 28 | UNICODE = str 29 | izip = zip 30 | else: # pragma: no cover 31 | UNICODE = unicode 32 | from itertools import izip 33 | 34 | INT = Struct('>i') 35 | FLOAT = Struct('>f') 36 | STRING = Struct('>s') 37 | TIME_TAG = Struct('>II') 38 | 39 | TP_PACKET_FORMAT = "!12I" 40 | # 1970-01-01 00:00:00 41 | NTP_DELTA = 2208988800 42 | 43 | NULL = b'\0' 44 | EMPTY = tuple() 45 | INF = float('inf') 46 | 47 | MidiTuple = namedtuple('MidiTuple', 'port_id status_byte data1 data2') 48 | 49 | def padded(l, n=4): 50 | """Return the size to pad a thing to. 51 | 52 | - `l` being the current size of the thing. 53 | - `n` being the desired divisor of the thing's padded size. 54 | """ 55 | return n * (min(1, divmod(l, n)[1]) + l // n) 56 | 57 | 58 | def parse_int(value, offset=0, **kwargs): 59 | """Return an int from offset in value.""" 60 | return INT.unpack_from(value, offset)[0], INT.size 61 | 62 | 63 | def parse_float(value, offset=0, **kwargs): 64 | """Return a float from offset in value.""" 65 | return FLOAT.unpack_from(value, offset)[0], FLOAT.size 66 | 67 | 68 | def parse_string(value, offset=0, encoding='', encoding_errors='strict'): 69 | """Return a string from offset in value. 70 | 71 | If encoding is defined, the string will be decoded. `encoding_errors` 72 | will be used to manage encoding errors in decoding. 73 | """ 74 | result = [] 75 | count = 0 76 | ss = STRING.size 77 | while True: 78 | c = STRING.unpack_from(value, offset + count)[0] 79 | count += ss 80 | 81 | if c == NULL: 82 | break 83 | result.append(c) 84 | 85 | r = b''.join(result) 86 | if encoding: 87 | return r.decode(encoding, errors=encoding_errors), padded(count) 88 | else: 89 | return r, padded(count) 90 | 91 | 92 | def parse_blob(value, offset=0, **kwargs): 93 | """Return a blob from offset in value.""" 94 | size = INT.size 95 | length = INT.unpack_from(value, offset)[0] 96 | data = unpack_from('>%is' % length, value, offset + size)[0] 97 | return data, padded(length) 98 | 99 | 100 | def parse_midi(value, offset=0, **kwargs): 101 | """Return a MIDI tuple from offset in value. 102 | A valid MIDI message: (port id, status byte, data1, data2). 103 | """ 104 | val = unpack_from('>I', value, offset)[0] 105 | args = tuple((val & 0xFF << 8 * i) >> 8 * i for i in range(3, -1, -1)) 106 | midi = MidiTuple(*args) 107 | return midi, len(midi) 108 | 109 | 110 | def parse_timeage(value, offset=0, **kwargs): 111 | """Return the 64bit OSC value as sec/nsec""" 112 | sec = unpack_from('>Q', value, offset)[0] 113 | return sec, 8 114 | 115 | 116 | def format_midi(value): 117 | return sum((val & 0xFF) << 8 * (3 - pos) for pos, val in enumerate(value)) 118 | 119 | 120 | def parse_true(*args, **kwargs): 121 | return True, 0 122 | 123 | 124 | def format_true(value): 125 | return EMPTY 126 | 127 | 128 | def parse_false(*args, **kwargs): 129 | return False, 0 130 | 131 | 132 | def format_false(value): 133 | return EMPTY 134 | 135 | 136 | def parse_nil(*args, **kwargs): 137 | return None, 0 138 | 139 | 140 | def format_nil(value): 141 | return EMPTY 142 | 143 | 144 | def parse_infinitum(*args, **kwargs): 145 | return INF, 0 146 | 147 | 148 | def format_infinitum(value): 149 | return EMPTY 150 | 151 | 152 | PARSERS = { 153 | b'i': parse_int, 154 | b'f': parse_float, 155 | b's': parse_string, 156 | b'S': parse_string, 157 | b'b': parse_blob, 158 | b'm': parse_midi, 159 | b'T': parse_true, 160 | b'F': parse_false, 161 | b'N': parse_nil, 162 | b'I': parse_infinitum, 163 | b't': parse_timeage, 164 | # TODO 165 | # b'h': parse_long, 166 | # b't': parse_timetage, 167 | # b'd': parse_double, 168 | # b'c': parse_char, 169 | # b'r': parse_rgba, 170 | # b'[': parse_array_start, 171 | # b']': parse_array_end, 172 | } 173 | 174 | 175 | PARSERS.update({ 176 | ord(k): v 177 | for k, v in PARSERS.items() 178 | }) 179 | 180 | 181 | WRITERS = ( 182 | (float, (b'f', b'f')), 183 | (int, (b'i', b'i')), 184 | (bytes, (b's', b'%is')), 185 | (UNICODE, (b's', b'%is')), 186 | (bytearray, (b'b', b'%ib')), 187 | (True, (b'T', b'')), 188 | (False, (b'F', b'')), 189 | (None, (b'N', b'')), 190 | (MidiTuple, (b'm', b'I')), 191 | ) 192 | 193 | 194 | PADSIZES = { 195 | bytes: 4, 196 | bytearray: 8 197 | } 198 | 199 | 200 | def parse(hint, value, offset=0, encoding='', encoding_errors='strict'): 201 | """Call the correct parser function for the provided hint. 202 | 203 | `hint` will be used to determine the correct parser, other parameters 204 | will be passed to this parser. 205 | """ 206 | parser = PARSERS.get(hint) 207 | 208 | if not parser: 209 | raise ValueError( 210 | "no known parser for type hint: {}, value: {}".format(hint, value) 211 | ) 212 | 213 | return parser( 214 | value, offset=offset, encoding=encoding, 215 | encoding_errors=encoding_errors 216 | ) 217 | 218 | 219 | def format_message(address, values, encoding='', encoding_errors='strict'): 220 | """Create a message.""" 221 | tags = [b','] 222 | fmt = [] 223 | 224 | encode_cache = {} 225 | 226 | lv = 0 227 | count = Counter() 228 | 229 | for value in values: 230 | lv += 1 231 | cls_or_value, writer = None, None 232 | for cls_or_value, writer in WRITERS: 233 | if ( 234 | cls_or_value is value 235 | or isinstance(cls_or_value, type) 236 | and isinstance(value, cls_or_value) 237 | ): 238 | break 239 | else: 240 | raise TypeError( 241 | u'unable to find a writer for value {}, type not in: {}.' 242 | .format(value, [x[0] for x in WRITERS]) 243 | ) 244 | 245 | if cls_or_value == UNICODE: 246 | if not encoding: 247 | raise TypeError(u"Can't format unicode string without encoding") 248 | 249 | cls_or_value = bytes 250 | value = ( 251 | encode_cache[value] 252 | if value in encode_cache else 253 | encode_cache.setdefault( 254 | value, value.encode(encoding, errors=encoding_errors) 255 | ) 256 | ) 257 | 258 | assert cls_or_value, writer 259 | 260 | tag, v_fmt = writer 261 | if b'%i' in v_fmt: 262 | v_fmt = v_fmt % padded(len(value) + 1, PADSIZES[cls_or_value]) 263 | 264 | tags.append(tag) 265 | fmt.append(v_fmt) 266 | count[tag.decode('utf8')] += 1 267 | 268 | fmt = b''.join(fmt) 269 | tags = b''.join(tags + [NULL]) 270 | 271 | if encoding and isinstance(address, UNICODE): 272 | address = address.encode(encoding, errors=encoding_errors) 273 | 274 | if not address.endswith(NULL): 275 | address += NULL 276 | 277 | fmt = b'>%is%is%s' % (padded(len(address)), padded(len(tags)), fmt) 278 | message = pack( 279 | fmt, 280 | address, 281 | tags, 282 | *( 283 | ( 284 | encode_cache.get(v) + NULL if isinstance(v, UNICODE) and encoding 285 | else (v + NULL) if t in (b's', b'b') 286 | else format_midi(v) if isinstance(v, MidiTuple) 287 | else v 288 | ) 289 | for t, v in 290 | izip(tags[1:], values) 291 | ) 292 | ) 293 | return message, Stats(1, len(message), lv, count) 294 | 295 | 296 | def read_message(data, offset=0, encoding='', encoding_errors='strict', validate_message_address=True): 297 | """Return address, tags, values, and length of a decoded message. 298 | 299 | Can be called either on a standalone message, or on a message 300 | extracted from a bundle. 301 | """ 302 | address, size = parse_string(data, offset=offset) 303 | index = size 304 | if not address.startswith(b'/') and validate_message_address: 305 | raise ValueError("address {} doesn't start with a '/'".format(address)) 306 | 307 | tags, size = parse_string(data, offset=offset + index) 308 | if not tags.startswith(b','): 309 | raise ValueError("tag string {} doesn't start with a ','".format(tags)) 310 | tags = tags[1:] 311 | 312 | index += size 313 | 314 | values = [] 315 | for tag in tags: 316 | value, off = parse( 317 | tag, data, offset=offset + index, encoding=encoding, 318 | encoding_errors=encoding_errors 319 | ) 320 | values.append(value) 321 | index += off 322 | 323 | return address, tags, values, index 324 | 325 | 326 | def time_to_timetag(value): 327 | """Create a timetag from a time. 328 | 329 | `time` is an unix timestamp (number of seconds since 1/1/1970). 330 | result is the equivalent time using the NTP format. 331 | """ 332 | if value is None: 333 | return (0, 1) 334 | seconds, fract = divmod(value, 1) 335 | seconds += NTP_DELTA 336 | seconds = int(seconds) 337 | fract = int(fract * 2**32) 338 | return (seconds, fract) 339 | 340 | 341 | def timetag_to_time(timetag): 342 | """Decode a timetag to a time. 343 | 344 | `timetag` is an NTP formated time. 345 | retult is the equivalent unix timestamp (number of seconds since 1/1/1970). 346 | """ 347 | if timetag == (0, 1): 348 | return time() 349 | 350 | seconds, fract = timetag 351 | return seconds + fract / 2. ** 32 - NTP_DELTA 352 | 353 | 354 | def format_bundle(data, timetag=None, encoding='', encoding_errors='strict'): 355 | """Create a bundle from a list of (address, values) tuples. 356 | 357 | String values will be encoded using `encoding` or must be provided 358 | as bytes. 359 | `encoding_errors` will be used to manage encoding errors. 360 | """ 361 | timetag = time_to_timetag(timetag) 362 | bundle = [pack('8s', b'#bundle\0')] 363 | bundle.append(TIME_TAG.pack(*timetag)) 364 | 365 | stats = Stats() 366 | for address, values in data: 367 | msg, st = format_message( 368 | address, values, encoding='', 369 | encoding_errors=encoding_errors 370 | ) 371 | bundle.append(pack('>i', len(msg))) 372 | bundle.append(msg) 373 | stats += st 374 | 375 | return b''.join(bundle), stats 376 | 377 | 378 | def read_bundle(data, encoding='', encoding_errors='strict'): 379 | """Decode a bundle into a (timestamp, messages) tuple.""" 380 | length = len(data) 381 | 382 | header = unpack_from('7s', data, 0)[0] 383 | offset = 8 * STRING.size 384 | if header != b'#bundle': 385 | raise ValueError( 386 | "the message doesn't start with '#bundle': {}".format(header)) 387 | 388 | timetag = timetag_to_time(TIME_TAG.unpack_from(data, offset)) 389 | offset += TIME_TAG.size 390 | 391 | messages = [] 392 | while offset < length: 393 | # NOTE, we don't really care about the size of the message, our 394 | # parsing will compute it anyway 395 | # size = Int.unpack_from(data, offset) 396 | offset += INT.size 397 | address, tags, values, off = read_message( 398 | data, offset, encoding=encoding, encoding_errors=encoding_errors 399 | ) 400 | offset += off 401 | messages.append((address, tags, values, offset)) 402 | 403 | return (timetag, messages) 404 | 405 | 406 | def read_packet(data, drop_late=False, encoding='', encoding_errors='strict', validate_message_address=True): 407 | """Detect if the data received is a simple message or a bundle, read it. 408 | 409 | Always return a list of messages. 410 | If drop_late is true, and the received data is an expired bundle, 411 | then returns an empty list. 412 | """ 413 | header = unpack_from('>c', data, 0)[0] 414 | 415 | if header == b'#': 416 | timetag, messages = read_bundle( 417 | data, encoding=encoding, encoding_errors=encoding_errors 418 | ) 419 | if drop_late: 420 | if time() > timetag: 421 | return [] 422 | return messages 423 | 424 | elif header == b'/' or not validate_message_address: 425 | return [ 426 | read_message( 427 | data, encoding=encoding, 428 | encoding_errors=encoding_errors, 429 | validate_message_address=validate_message_address 430 | ) 431 | ] 432 | 433 | else: 434 | raise ValueError('packet is not a message or a bundle') 435 | -------------------------------------------------------------------------------- /oscpy/server.py: -------------------------------------------------------------------------------- 1 | """Server API. 2 | 3 | This module currently only implements `OSCThreadServer`, a thread based server. 4 | """ 5 | import logging 6 | from threading import Thread, Event 7 | 8 | import os 9 | import re 10 | import inspect 11 | from sys import platform 12 | from time import sleep, time 13 | from functools import partial 14 | from select import select 15 | import socket 16 | 17 | from oscpy import __version__ 18 | from oscpy.parser import read_packet, UNICODE 19 | from oscpy.client import send_bundle, send_message 20 | from oscpy.stats import Stats 21 | 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def ServerClass(cls): 27 | """Decorate classes with for methods implementing OSC endpoints. 28 | 29 | This decorator is necessary on your class if you want to use the 30 | `address_method` decorator on its methods, see 31 | `:meth:OSCThreadServer.address_method`'s documentation. 32 | """ 33 | cls_init = cls.__init__ 34 | 35 | def __init__(self, *args, **kwargs): 36 | cls_init(self, *args, **kwargs) 37 | 38 | for m in dir(self): 39 | meth = getattr(self, m) 40 | if hasattr(meth, '_address'): 41 | server, address, sock, get_address = meth._address 42 | server.bind(address, meth, sock, get_address=get_address) 43 | 44 | cls.__init__ = __init__ 45 | return cls 46 | 47 | 48 | __FILE__ = inspect.getfile(ServerClass) 49 | 50 | 51 | class OSCThreadServer(object): 52 | """A thread-based OSC server. 53 | 54 | Listens for osc messages in a thread, and dispatches the messages 55 | values to callbacks from there. 56 | 57 | The '/_oscpy/' namespace is reserved for metadata about the OSCPy 58 | internals, please see package documentation for further details. 59 | """ 60 | 61 | def __init__( 62 | self, drop_late_bundles=False, timeout=0.01, advanced_matching=False, 63 | encoding='', encoding_errors='strict', default_handler=None, intercept_errors=True, 64 | validate_message_address=True 65 | ): 66 | """Create an OSCThreadServer. 67 | 68 | - `timeout` is a number of seconds used as a time limit for 69 | select() calls in the listening thread, optiomal, defaults to 70 | 0.01. 71 | - `drop_late_bundles` instruct the server not to dispatch calls 72 | from bundles that arrived after their timetag value. 73 | (optional, defaults to False) 74 | - `advanced_matching` (defaults to False), setting this to True 75 | activates the pattern matching part of the specification, let 76 | this to False if you don't need it, as it triggers a lot more 77 | computation for each received message. 78 | - `encoding` if defined, will be used to encode/decode all 79 | strings sent/received to/from unicode/string objects, if left 80 | empty, the interface will only accept bytes and return bytes 81 | to callback functions. 82 | - `encoding_errors` if `encoding` is set, this value will be 83 | used as `errors` parameter in encode/decode calls. 84 | - `default_handler` if defined, will be used to handle any 85 | message that no configured address matched, the received 86 | arguments will be (address, *values). 87 | - `intercept_errors`, if True, means that exception raised by 88 | callbacks will be intercepted and logged. If False, the handler 89 | thread will terminate mostly silently on such exceptions. 90 | - `validate_message_address`, if True, require received messages 91 | to have an address beginning with the specified OSC address 92 | pattern of '/'. Set to False to accept messages from 93 | implementations that ignore the address pattern specification. 94 | """ 95 | self._must_loop = True 96 | self._termination_event = Event() 97 | 98 | self.addresses = {} 99 | self.sockets = [] 100 | self.timeout = timeout 101 | self.default_socket = None 102 | self.drop_late_bundles = drop_late_bundles 103 | self.advanced_matching = advanced_matching 104 | self.encoding = encoding 105 | self.encoding_errors = encoding_errors 106 | self.default_handler = default_handler 107 | self.intercept_errors = intercept_errors 108 | self.validate_message_address = validate_message_address 109 | 110 | self.stats_received = Stats() 111 | self.stats_sent = Stats() 112 | 113 | t = Thread(target=self._run_listener) 114 | t.daemon = True 115 | t.start() 116 | self._thread = t 117 | 118 | self._smart_address_cache = {} 119 | self._smart_part_cache = {} 120 | 121 | def bind(self, address, callback, sock=None, get_address=False): 122 | """Bind a callback to an osc address. 123 | 124 | A socket in the list of existing sockets of the server can be 125 | given. If no socket is provided, the default socket of the 126 | server is used, if no default socket has been defined, a 127 | RuntimeError is raised. 128 | 129 | Multiple callbacks can be bound to the same address. 130 | """ 131 | if not sock and self.default_socket: 132 | sock = self.default_socket 133 | elif not sock: 134 | raise RuntimeError('no default socket yet and no socket provided') 135 | 136 | if isinstance(address, UNICODE) and self.encoding: 137 | address = address.encode( 138 | self.encoding, errors=self.encoding_errors) 139 | 140 | if self.advanced_matching: 141 | address = self.create_smart_address(address) 142 | 143 | callbacks = self.addresses.get((sock, address), []) 144 | cb = (callback, get_address) 145 | if cb not in callbacks: 146 | callbacks.append(cb) 147 | self.addresses[(sock, address)] = callbacks 148 | 149 | def create_smart_address(self, address): 150 | """Create an advanced matching address from a string. 151 | 152 | The address will be split by '/' and each part will be converted 153 | into a regexp, using the rules defined in the OSC specification. 154 | """ 155 | cache = self._smart_address_cache 156 | 157 | if address in cache: 158 | return cache[address] 159 | 160 | else: 161 | parts = address.split(b'/') 162 | smart_parts = tuple( 163 | re.compile(self._convert_part_to_regex(part)) for part in parts 164 | ) 165 | cache[address] = smart_parts 166 | return smart_parts 167 | 168 | def _convert_part_to_regex(self, part): 169 | cache = self._smart_part_cache 170 | 171 | if part in cache: 172 | return cache[part] 173 | 174 | else: 175 | r = [b'^'] 176 | for i, _ in enumerate(part): 177 | # getting a 1 char byte string instead of an int in 178 | # python3 179 | c = part[i:i + 1] 180 | if c == b'?': 181 | r.append(b'.') 182 | elif c == b'*': 183 | r.append(b'.*') 184 | elif c == b'[': 185 | r.append(b'[') 186 | elif c == b'!' and r and r[-1] == b'[': 187 | r.append(b'^') 188 | elif c == b']': 189 | r.append(b']') 190 | elif c == b'{': 191 | r.append(b'(') 192 | elif c == b',': 193 | r.append(b'|') 194 | elif c == b'}': 195 | r.append(b')') 196 | else: 197 | r.append(c) 198 | 199 | r.append(b'$') 200 | 201 | smart_part = re.compile(b''.join(r)) 202 | 203 | cache[part] = smart_part 204 | return smart_part 205 | 206 | def unbind(self, address, callback, sock=None): 207 | """Unbind a callback from an OSC address. 208 | 209 | See `bind` for `sock` documentation. 210 | """ 211 | if not sock and self.default_socket: 212 | sock = self.default_socket 213 | elif not sock: 214 | raise RuntimeError('no default socket yet and no socket provided') 215 | 216 | if isinstance(address, UNICODE) and self.encoding: 217 | address = address.encode( 218 | self.encoding, errors=self.encoding_errors) 219 | 220 | callbacks = self.addresses.get((sock, address), []) 221 | to_remove = [] 222 | for cb in callbacks: 223 | if cb[0] == callback: 224 | to_remove.append(cb) 225 | 226 | while to_remove: 227 | callbacks.remove(to_remove.pop()) 228 | 229 | self.addresses[(sock, address)] = callbacks 230 | 231 | def listen( 232 | self, address='localhost', port=0, default=False, family='inet' 233 | ): 234 | """Start listening on an (address, port). 235 | 236 | - if `port` is 0, the system will allocate a free port 237 | - if `default` is True, the instance will save this socket as the 238 | default one for subsequent calls to methods with an optional socket 239 | - `family` accepts the 'unix' and 'inet' values, a socket of the 240 | corresponding type will be created. 241 | If family is 'unix', then the address must be a filename, the 242 | `port` value won't be used. 'unix' sockets are not defined on 243 | Windows. 244 | 245 | The socket created to listen is returned, and can be used later 246 | with methods accepting the `sock` parameter. 247 | """ 248 | if family == 'unix': 249 | family_ = socket.AF_UNIX 250 | elif family == 'inet': 251 | family_ = socket.AF_INET 252 | else: 253 | raise ValueError( 254 | "Unknown socket family, accepted values are 'unix' and 'inet'" 255 | ) 256 | 257 | sock = socket.socket(family_, socket.SOCK_DGRAM) 258 | if family == 'unix': 259 | addr = address 260 | else: 261 | addr = (address, port) 262 | sock.bind(addr) 263 | self.sockets.append(sock) 264 | if default and not self.default_socket: 265 | self.default_socket = sock 266 | elif default: 267 | raise RuntimeError( 268 | 'Only one default socket authorized! Please set ' 269 | 'default=False to other calls to listen()' 270 | ) 271 | self.bind_meta_routes(sock) 272 | return sock 273 | 274 | def close(self, sock=None): 275 | """Close a socket opened by the server.""" 276 | if not sock and self.default_socket: 277 | sock = self.default_socket 278 | elif not sock: 279 | raise RuntimeError('no default socket yet and no socket provided') 280 | 281 | if platform != 'win32' and sock.family == socket.AF_UNIX: 282 | os.unlink(sock.getsockname()) 283 | else: 284 | sock.close() 285 | 286 | if sock == self.default_socket: 287 | self.default_socket = None 288 | 289 | def getaddress(self, sock=None): 290 | """Wrap call to getsockname. 291 | 292 | If `sock` is None, uses the default socket for the server. 293 | 294 | Returns (ip, port) for an inet socket, or filename for an unix 295 | socket. 296 | """ 297 | if not sock and self.default_socket: 298 | sock = self.default_socket 299 | elif not sock: 300 | raise RuntimeError('no default socket yet and no socket provided') 301 | 302 | return sock.getsockname() 303 | 304 | def stop(self, s=None): 305 | """Close and remove a socket from the server's sockets. 306 | 307 | If `sock` is None, uses the default socket for the server. 308 | 309 | """ 310 | if not s and self.default_socket: 311 | s = self.default_socket 312 | 313 | if s in self.sockets: 314 | read = select([s], [], [], 0) 315 | s.close() 316 | if s in read: 317 | s.recvfrom(65535) 318 | self.sockets.remove(s) 319 | else: 320 | raise RuntimeError('{} is not one of my sockets!'.format(s)) 321 | 322 | def stop_all(self): 323 | """Call stop on all the existing sockets.""" 324 | for s in self.sockets[:]: 325 | self.stop(s) 326 | sleep(10e-9) 327 | 328 | def terminate_server(self): 329 | """Request the inner thread to finish its tasks and exit. 330 | 331 | May be called from an event, too. 332 | """ 333 | self._must_loop = False 334 | 335 | def join_server(self, timeout=None): 336 | """Wait for the server to exit (`terminate_server()` must have been called before). 337 | 338 | Returns True if and only if the inner thread exited before timeout.""" 339 | return self._termination_event.wait(timeout=timeout) 340 | 341 | def _run_listener(self): 342 | """Wrapper just ensuring that the handler thread cleans up on exit.""" 343 | try: 344 | self._listen() 345 | finally: 346 | self._termination_event.set() 347 | 348 | def _listen(self): 349 | """(internal) Busy loop to listen for events. 350 | 351 | This method is called in a thread by the `listen` method, and 352 | will be the one actually listening for messages on the server's 353 | sockets, and calling the callbacks when messages are received. 354 | """ 355 | 356 | match = self._match_address 357 | advanced_matching = self.advanced_matching 358 | addresses = self.addresses 359 | stats = self.stats_received 360 | 361 | def _execute_callbacks(_callbacks_list): 362 | for cb, get_address in _callbacks_list: 363 | try: 364 | if get_address: 365 | cb(address, *values) 366 | else: 367 | cb(*values) 368 | except Exception as exc: 369 | if self.intercept_errors: 370 | logger.error("Unhandled exception caught in oscpy server", exc_info=True) 371 | else: 372 | raise 373 | 374 | while self._must_loop: 375 | 376 | drop_late = self.drop_late_bundles 377 | if not self.sockets: 378 | sleep(.01) 379 | continue 380 | else: 381 | try: 382 | read, write, error = select(self.sockets, [], [], self.timeout) 383 | except (ValueError, socket.error): 384 | continue 385 | 386 | for sender_socket in read: 387 | try: 388 | data, sender = sender_socket.recvfrom(65535) 389 | except ConnectionResetError: 390 | continue 391 | 392 | try: 393 | for address, tags, values, offset in read_packet( 394 | data, drop_late=drop_late, encoding=self.encoding, 395 | encoding_errors=self.encoding_errors, 396 | validate_message_address=self.validate_message_address 397 | ): 398 | stats.calls += 1 399 | stats.bytes += offset 400 | stats.params += len(values) 401 | stats.types.update(tags) 402 | 403 | matched = False 404 | if advanced_matching: 405 | for sock, addr in addresses: 406 | if sock == sender_socket and match(addr, address): 407 | callbacks_list = addresses.get((sock, addr), []) 408 | if callbacks_list: 409 | matched = True 410 | _execute_callbacks(callbacks_list) 411 | else: 412 | callbacks_list = addresses.get((sender_socket, address), []) 413 | if callbacks_list: 414 | matched = True 415 | _execute_callbacks(callbacks_list) 416 | 417 | if not matched and self.default_handler: 418 | self.default_handler(address, *values) 419 | except ValueError: 420 | if self.intercept_errors: 421 | logger.error("Unhandled ValueError caught in oscpy server", exc_info=True) 422 | else: 423 | raise 424 | 425 | @staticmethod 426 | def _match_address(smart_address, target_address): 427 | """(internal) Check if provided `smart_address` matches address. 428 | 429 | A `smart_address` is a list of regexps to match 430 | against the parts of the `target_address`. 431 | """ 432 | target_parts = target_address.split(b'/') 433 | if len(target_parts) != len(smart_address): 434 | return False 435 | 436 | return all( 437 | model.match(part) 438 | for model, part in 439 | zip(smart_address, target_parts) 440 | ) 441 | 442 | def send_message( 443 | self, osc_address, values, ip_address, port, sock=None, safer=False 444 | ): 445 | """Shortcut to the client's `send_message` method. 446 | 447 | Use the default_socket of the server by default. 448 | See `client.send_message` for more info about the parameters. 449 | """ 450 | if not sock and self.default_socket: 451 | sock = self.default_socket 452 | elif not sock: 453 | raise RuntimeError('no default socket yet and no socket provided') 454 | 455 | stats = send_message( 456 | osc_address, 457 | values, 458 | ip_address, 459 | port, 460 | sock=sock, 461 | safer=safer, 462 | encoding=self.encoding, 463 | encoding_errors=self.encoding_errors 464 | ) 465 | self.stats_sent += stats 466 | return stats 467 | 468 | def send_bundle( 469 | self, messages, ip_address, port, timetag=None, sock=None, safer=False 470 | ): 471 | """Shortcut to the client's `send_bundle` method. 472 | 473 | Use the `default_socket` of the server by default. 474 | See `client.send_bundle` for more info about the parameters. 475 | """ 476 | if not sock and self.default_socket: 477 | sock = self.default_socket 478 | elif not sock: 479 | raise RuntimeError('no default socket yet and no socket provided') 480 | 481 | stats = send_bundle( 482 | messages, 483 | ip_address, 484 | port, 485 | sock=sock, 486 | safer=safer, 487 | encoding=self.encoding, 488 | encoding_errors=self.encoding_errors 489 | ) 490 | self.stats_sent += stats 491 | return stats 492 | 493 | def get_sender(self): 494 | """Return the socket, ip and port of the message that is currently being managed. 495 | Warning:: 496 | 497 | this method should only be called from inside the handling 498 | of a message (i.e, inside a callback). 499 | """ 500 | frames = inspect.getouterframes(inspect.currentframe()) 501 | for frame, filename, _, function, _, _ in frames: 502 | if function == '_listen' and __FILE__.startswith(filename): 503 | break 504 | else: 505 | raise RuntimeError('get_sender() not called from a callback') 506 | 507 | sock = frame.f_locals.get('sender_socket') 508 | address, port = frame.f_locals.get('sender') 509 | return sock, address, port 510 | 511 | def answer( 512 | self, address=None, values=None, bundle=None, timetag=None, 513 | safer=False, port=None 514 | ): 515 | """Answers a message or bundle to a client. 516 | 517 | This method can only be called from a callback, it will lookup 518 | the sender of the packet that triggered the callback, and send 519 | the given message or bundle to it. 520 | 521 | `timetag` is only used if `bundle` is True. 522 | See `send_message` and `send_bundle` for info about the parameters. 523 | 524 | Only one of `values` or `bundle` should be defined, if `values` 525 | is defined, `send_message` is used with it, if `bundle` is 526 | defined, `send_bundle` is used with its value. 527 | """ 528 | if not values: 529 | values = [] 530 | 531 | sock, ip_address, response_port = self.get_sender() 532 | 533 | if port is not None: 534 | response_port = port 535 | 536 | if bundle: 537 | return self.send_bundle( 538 | bundle, ip_address, response_port, timetag=timetag, sock=sock, 539 | safer=safer 540 | ) 541 | else: 542 | return self.send_message( 543 | address, values, ip_address, response_port, sock=sock 544 | ) 545 | 546 | def address(self, address, sock=None, get_address=False): 547 | """Decorate functions to bind them from their definition. 548 | 549 | `address` is the osc address to bind to the callback. 550 | if `get_address` is set to True, the first parameter the 551 | callback will receive will be the address that matched (useful 552 | with advanced matching). 553 | 554 | example: 555 | server = OSCThreadServer() 556 | server.listen('localhost', 8000, default=True) 557 | 558 | @server.address(b'/printer') 559 | def printer(values): 560 | print(values) 561 | 562 | send_message(b'/printer', [b'hello world']) 563 | 564 | note: 565 | This won't work on methods as it'll call them as normal 566 | functions, and the callback won't get a `self` argument. 567 | 568 | To bind a method use the `address_method` decorator. 569 | """ 570 | def decorator(callback): 571 | self.bind(address, callback, sock, get_address=get_address) 572 | return callback 573 | 574 | return decorator 575 | 576 | def address_method(self, address, sock=None, get_address=False): 577 | """Decorate methods to bind them from their definition. 578 | 579 | The class defining the method must itself be decorated with the 580 | `ServerClass` decorator, the methods will be bound to the 581 | address when the class is instantiated. 582 | 583 | See `address` for more information about the parameters. 584 | 585 | example: 586 | 587 | osc = OSCThreadServer() 588 | osc.listen(default=True) 589 | 590 | @ServerClass 591 | class MyServer(object): 592 | 593 | @osc.address_method(b'/test') 594 | def success(self, *args): 595 | print("success!", args) 596 | """ 597 | def decorator(decorated): 598 | decorated._address = (self, address, sock, get_address) 599 | return decorated 600 | 601 | return decorator 602 | 603 | def bind_meta_routes(self, sock=None): 604 | """This module implements osc routes to probe the internal state of a 605 | live OSCPy server. These routes are placed in the /_oscpy/ namespace, 606 | and provide information such as the version, the existing routes, and 607 | usage statistics of the server over time. 608 | 609 | These requests will be sent back to the client's address/port that sent 610 | them, with the osc address suffixed with '/answer'. 611 | 612 | examples: 613 | '/_oscpy/version' -> '/_oscpy/version/answer' 614 | '/_oscpy/stats/received' -> '/_oscpy/stats/received/answer' 615 | 616 | messages to these routes require a port number as argument, to 617 | know to which port to send to. 618 | """ 619 | self.bind(b'/_oscpy/version', self._get_version, sock=sock) 620 | self.bind(b'/_oscpy/routes', self._get_routes, sock=sock) 621 | self.bind(b'/_oscpy/stats/received', self._get_stats_received, sock=sock) 622 | self.bind(b'/_oscpy/stats/sent', self._get_stats_sent, sock=sock) 623 | 624 | def _get_version(self, port, *args): 625 | self.answer( 626 | b'/_oscpy/version/answer', 627 | (__version__, ), 628 | port=port 629 | ) 630 | 631 | def _get_routes(self, port, *args): 632 | self.answer( 633 | b'/_oscpy/routes/answer', 634 | [a[1] for a in self.addresses], 635 | port=port 636 | ) 637 | 638 | def _get_stats_received(self, port, *args): 639 | self.answer( 640 | b'/_oscpy/stats/received/answer', 641 | self.stats_received.to_tuple(), 642 | port=port 643 | ) 644 | 645 | def _get_stats_sent(self, port, *args): 646 | self.answer( 647 | b'/_oscpy/stats/sent/answer', 648 | self.stats_sent.to_tuple(), 649 | port=port 650 | ) 651 | -------------------------------------------------------------------------------- /oscpy/stats.py: -------------------------------------------------------------------------------- 1 | "Simple utility class to gather stats about the volumes of data managed" 2 | 3 | from collections import Counter 4 | 5 | 6 | class Stats(object): 7 | def __init__(self, calls=0, bytes=0, params=0, types=None, **kwargs): 8 | self.calls = calls 9 | self.bytes = bytes 10 | self.params = params 11 | self.types = types or Counter() 12 | super(Stats, self).__init__(**kwargs) 13 | 14 | def to_tuple(self): 15 | types = self.types 16 | keys = types.keys() 17 | return ( 18 | self.calls, 19 | self.bytes, 20 | self.params, 21 | ''.join(keys), 22 | ) + tuple(types[k] for k in keys) 23 | 24 | def __iadd__(self, other): 25 | assert isinstance(other, Stats) 26 | self.calls += other.calls 27 | self.bytes += other.bytes 28 | self.params += other.params 29 | self.types += other.types 30 | return self 31 | 32 | def __add__(self, other): 33 | assert isinstance(other, Stats) 34 | return Stats( 35 | calls=self.calls + other.calls, 36 | bytes=self.bytes + other.bytes, 37 | params=self.params + other.params, 38 | types=self.types + other.types 39 | ) 40 | 41 | def __eq__(self, other): 42 | return other is self or ( 43 | isinstance(other, Stats) 44 | and self.calls == self.calls 45 | and self.bytes == self.bytes 46 | and self.params == self.params 47 | and self.types == other.types 48 | ) 49 | 50 | def __repr__(self): 51 | return 'Stats:\n' + '\n'.join( 52 | ' {}:{}{}'.format( 53 | k, 54 | '' if isinstance(v, str) and v.startswith('\n') else ' ', 55 | v 56 | ) 57 | for k, v in ( 58 | ('calls', self.calls), 59 | ('bytes', self.bytes), 60 | ('params', self.params), 61 | ( 62 | 'types', 63 | ''.join( 64 | '\n {}: {}'.format(k, self.types[k]) 65 | for k in sorted(self.types) 66 | ) 67 | ) 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """See README.md for package documentation.""" 2 | 3 | from setuptools import setup, find_packages 4 | 5 | from io import open 6 | from os import path 7 | 8 | from oscpy import __version__ 9 | 10 | here = path.abspath(path.dirname(__file__)) 11 | 12 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 13 | long_description = f.read() 14 | 15 | URL = 'https://github.com/kivy/oscpy' 16 | 17 | setup( 18 | name='oscpy', 19 | # https://packaging.python.org/en/latest/single_source_version.html 20 | version=__version__, 21 | description='A modern and efficient OSC Client/Server implementation', 22 | long_description=long_description, 23 | long_description_content_type='text/markdown', 24 | url=URL, 25 | author='Gabriel Pettier', 26 | author_email='gabriel@kivy.org', 27 | classifiers=[ 28 | 'Development Status :: 4 - Beta', 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Multimedia :: Sound/Audio', 31 | 'Topic :: Software Development :: Libraries', 32 | 'Topic :: System :: Networking', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | ], 42 | keywords='OSC network udp', 43 | 44 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 45 | install_requires=[], 46 | extras_require={ 47 | 'dev': ['pytest>=3.6', 'wheel', 'pytest-cov', 'pycodestyle'], 48 | 'ci': ['coveralls', 'pytest-rerunfailures'], 49 | }, 50 | package_data={}, 51 | data_files=[], 52 | entry_points={ 53 | 'console_scripts': ['oscli=oscpy.cli:main'], 54 | }, 55 | 56 | project_urls={ 57 | 'Bug Reports': URL + '/issues', 58 | 'Source': URL, 59 | }, 60 | ) 61 | -------------------------------------------------------------------------------- /tests/performances.py: -------------------------------------------------------------------------------- 1 | """A crude performance assessment of oscpy.""" 2 | from oscpy.server import OSCThreadServer 3 | from oscpy.client import send_message 4 | from oscpy.parser import format_message, read_message 5 | 6 | from time import time, sleep 7 | import socket 8 | import os 9 | 10 | DURATION = 1 11 | 12 | patterns = [ 13 | [], 14 | [b'B' * 65435], 15 | [b'test'], 16 | [b'test', b'auie nstau'], 17 | [b'test', b'auie nstau'] * 5, 18 | [1.2345], 19 | [1.2345, 1.2345, 10000000000.], 20 | list(range(500)), 21 | ] 22 | 23 | print("#" * 80) 24 | print("format/parse test") 25 | 26 | for i, pattern in enumerate(patterns): 27 | print("*" * 100) 28 | print(f"pattern: {i}") 29 | n = 0 30 | timeout = time() + DURATION 31 | 32 | while time() < timeout: 33 | n += 1 34 | p, s = format_message(b'/address', pattern) 35 | 36 | size = len(p) / 1000 37 | print( 38 | f"formated message {n} times ({n / DURATION}/s) " 39 | f"({n * size / DURATION:.2f}MB/s)" 40 | ) 41 | 42 | n = 0 43 | timeout = time() + DURATION 44 | 45 | while time() < timeout: 46 | n += 1 47 | read_message(p) 48 | 49 | print( 50 | f"parsed message {n} times ({n / DURATION}/s) " 51 | f"({n * size / DURATION:.2f}MB/s)" 52 | ) 53 | 54 | 55 | n = 0 56 | timeout = time() + DURATION 57 | 58 | while time() < timeout: 59 | n += 1 60 | read_message(format_message(b'/address', pattern)[0]) 61 | 62 | print( 63 | f"round-trip {n} times ({n / DURATION}/s) " 64 | f"({n * size / DURATION:.2f}MB/s)" 65 | ) 66 | 67 | 68 | print("#" * 80) 69 | print("sending/receiving test") 70 | # address, port = osc.getaddress() 71 | 72 | received = 0 73 | 74 | 75 | def count(*values): 76 | """Count calls.""" 77 | global received 78 | received += 1 79 | 80 | 81 | for family in 'unix', 'inet': 82 | osc = OSCThreadServer() 83 | print(f"family: {family}") 84 | if family == 'unix': 85 | address, port = '/tmp/test_sock', 0 86 | if os.path.exists(address): 87 | os.unlink(address) 88 | sock = SOCK = osc.listen(address=address, family='unix') 89 | else: 90 | SOCK = sock = osc.listen() 91 | address, port = osc.getaddress(sock) 92 | 93 | osc.bind(b'/count', count, sock=sock) 94 | for i, pattern in enumerate(patterns): 95 | for safer in (False, True): 96 | timeout = time() + DURATION 97 | sent = 0 98 | received = 0 99 | 100 | while time() < timeout: 101 | send_message( 102 | b'/count', pattern, address, port, sock=SOCK, safer=safer 103 | ) 104 | sent += 1 105 | sleep(10e-9) 106 | 107 | size = len(format_message(b'/count', pattern)[0]) / 1000. 108 | 109 | print( 110 | f"{i}: safe: {safer}\t", 111 | f"sent:\t{sent}\t({sent / DURATION}/s)\t({sent * size / DURATION:.2f}MB/s)\t" # noqa 112 | f"received:\t{received}\t({received / DURATION}/s)\t({received * size / DURATION:.2f}MB/s)\t" # noqa 113 | f"loss {((sent - received) / sent) * 100}%" 114 | ) 115 | 116 | if family == 'unix': 117 | os.unlink(address) 118 | osc.stop_all() 119 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | from random import randint 3 | from time import sleep 4 | from oscpy.cli import init_parser, main, _send, __dump 5 | from oscpy.client import send_message 6 | 7 | 8 | class Mock(object): 9 | pass 10 | 11 | 12 | def test_init_parser(): 13 | parser = init_parser() 14 | 15 | 16 | def test__send(capsys): 17 | options = Mock() 18 | options.repeat = 2 19 | options.host = 'localhost' 20 | options.port = 12345 21 | options.address = '/test' 22 | options.safer = False 23 | options.encoding = 'utf8' 24 | options.encoding_errors = 'strict' 25 | options.message = (1, 2, 3, 4, b"hello world") 26 | 27 | _send(options) 28 | captured = capsys.readouterr() 29 | out = captured.out 30 | assert out.startswith(dedent( 31 | ''' 32 | Stats: 33 | calls: 2 34 | bytes: 88 35 | params: 10 36 | types: 37 | ''' 38 | ).lstrip()) 39 | assert ' i: 8' in out 40 | assert ' s: 2' in out 41 | 42 | 43 | def test___dump(capsys): 44 | options = Mock() 45 | options.repeat = 2 46 | options.host = 'localhost' 47 | options.port = randint(60000, 65535) 48 | options.address = b'/test' 49 | options.safer = False 50 | options.encoding = None 51 | options.encoding_errors = 'strict' 52 | options.message = (1, 2, 3, 4, b"hello world") 53 | 54 | osc = __dump(options) 55 | out = capsys.readouterr().out 56 | assert out == '' 57 | 58 | send_message( 59 | options.address, 60 | options.message, 61 | options.host, 62 | options.port, 63 | safer=options.safer, 64 | encoding=options.encoding, 65 | encoding_errors=options.encoding_errors 66 | ) 67 | 68 | sleep(0.1) 69 | out, err = capsys.readouterr() 70 | assert err == '' 71 | lines = out.split('\n') 72 | assert lines[0] == u"/test: 1, 2, 3, 4, hello world" 73 | 74 | osc.stop() 75 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | from oscpy.client import send_message, send_bundle, OSCClient 4 | from oscpy.server import OSCThreadServer 5 | from time import time, sleep 6 | 7 | import pytest 8 | 9 | 10 | def test_send_message(): 11 | osc = OSCThreadServer() 12 | sock = osc.listen() 13 | port = sock.getsockname()[1] 14 | acc = [] 15 | 16 | def success(*values): 17 | acc.append(values[0]) 18 | 19 | osc.bind(b'/success', success, sock) 20 | 21 | timeout = time() + 5 22 | while len(acc) < 100: 23 | if time() > timeout: 24 | raise OSError('timeout while waiting for success message.') 25 | 26 | send_message(b'/success', [1], 'localhost', port) 27 | 28 | 29 | def test_send_message_safer(): 30 | osc = OSCThreadServer() 31 | sock = osc.listen() 32 | port = sock.getsockname()[1] 33 | acc = [] 34 | 35 | def success(*values): 36 | acc.append(values[0]) 37 | 38 | osc.bind(b'/success', success, sock) 39 | 40 | timeout = time() + 5 41 | while len(acc) < 100: 42 | if time() > timeout: 43 | raise OSError('timeout while waiting for success message.') 44 | 45 | send_message(b'/success', [1], 'localhost', port, safer=True) 46 | 47 | 48 | def test_send_bundle(): 49 | osc = OSCThreadServer() 50 | sock = osc.listen() 51 | port = sock.getsockname()[1] 52 | acc = [] 53 | 54 | def success(*values): 55 | acc.append(values[0]) 56 | 57 | osc.bind(b'/success', success, sock) 58 | 59 | timeout = time() + 5 60 | while len(acc) < 100: 61 | if time() > timeout: 62 | raise OSError('timeout while waiting for success message.') 63 | 64 | send_bundle( 65 | [ 66 | (b'/success', [i]) 67 | for i in range(10) 68 | ], 69 | 'localhost', port 70 | ) 71 | 72 | 73 | def test_send_bundle_safer(): 74 | osc = OSCThreadServer() 75 | sock = osc.listen() 76 | port = sock.getsockname()[1] 77 | acc = [] 78 | 79 | def success(*values): 80 | acc.append(values[0]) 81 | 82 | osc.bind(b'/success', success, sock) 83 | 84 | timeout = time() + 5 85 | while len(acc) < 100: 86 | if time() > timeout: 87 | raise OSError('timeout while waiting for success message.') 88 | 89 | send_bundle( 90 | [ 91 | (b'/success', [i]) 92 | for i in range(10) 93 | ], 94 | 'localhost', port, safer=True 95 | ) 96 | 97 | 98 | def test_oscclient(): 99 | osc = OSCThreadServer() 100 | sock = osc.listen() 101 | port = sock.getsockname()[1] 102 | acc = [] 103 | 104 | def success(*values): 105 | acc.append(values[0]) 106 | 107 | osc.bind(b'/success', success, sock) 108 | 109 | client = OSCClient('localhost', port) 110 | 111 | timeout = time() + 5 112 | while len(acc) < 50: 113 | if time() > timeout: 114 | raise OSError('timeout while waiting for success message.') 115 | 116 | client.send_message(b'/success', [1]) 117 | 118 | while len(acc) < 100: 119 | if time() > timeout: 120 | raise OSError('timeout while waiting for success message.') 121 | 122 | client.send_bundle( 123 | [ 124 | (b'/success', [i]) 125 | for i in range(10) 126 | ] 127 | ) 128 | 129 | 130 | def test_timetag(): 131 | osc = OSCThreadServer(drop_late_bundles=True) 132 | osc.drop_late_bundles = True 133 | sock = osc.listen() 134 | port = sock.getsockname()[1] 135 | acc = [] 136 | 137 | @osc.address(b'/success', sock) 138 | def success(*values): 139 | acc.append(True) 140 | 141 | @osc.address(b'/failure', sock) 142 | def failure(*values): 143 | acc.append(False) 144 | 145 | client = OSCClient('localhost', port) 146 | 147 | timeout = time() + 5 148 | while len(acc) < 50: 149 | if time() > timeout: 150 | raise OSError('timeout while waiting for success message.') 151 | 152 | client.send_message(b'/success', [1]) 153 | 154 | while len(acc) < 100: 155 | if time() > timeout: 156 | raise OSError('timeout while waiting for success message.') 157 | 158 | client.send_bundle( 159 | [ 160 | (b'/failure', [i]) 161 | for i in range(10) 162 | ], 163 | timetag=time() - 1, 164 | safer=True, 165 | ) 166 | 167 | client.send_bundle( 168 | [ 169 | (b'/success', [i]) 170 | for i in range(10) 171 | ], 172 | timetag=time() + .1, 173 | safer=True, 174 | ) 175 | 176 | assert True in acc 177 | assert False not in acc 178 | 179 | 180 | def test_encoding_errors_strict(): 181 | with pytest.raises(UnicodeEncodeError) as e_info: # noqa 182 | send_message( 183 | u'/encoded', 184 | [u'ééééé ààààà'], 185 | '', 9000, 186 | encoding='ascii', 187 | ) 188 | 189 | 190 | def test_encoding_errors_ignore(): 191 | send_message( 192 | u'/encoded', 193 | [u'ééééé ààààà'], 194 | 'localhost', 9000, 195 | encoding='ascii', 196 | encoding_errors='ignore' 197 | ) 198 | 199 | 200 | def test_encoding_errors_replace(): 201 | send_message( 202 | u'/encoded', 203 | [u'ééééé ààààà'], 204 | 'localhost', 9000, 205 | encoding='ascii', 206 | encoding_errors='replace' 207 | ) 208 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | 3 | from oscpy.parser import ( 4 | parse, padded, read_message, read_bundle, read_packet, 5 | format_message, format_bundle, timetag_to_time, time_to_timetag, 6 | format_midi, format_true, format_false, format_nil, format_infinitum, MidiTuple 7 | ) 8 | from pytest import approx, raises 9 | from time import time 10 | import struct 11 | from oscpy.stats import Stats 12 | 13 | # example messages from 14 | # http://opensoundcontrol.org/spec-1_0-examples#argument 15 | 16 | message_1 = ( 17 | (b'/oscillator/4/frequency', [440.0]), 18 | [ 19 | 0x2f, 0x6f, 0x73, 0x63, 20 | 0x69, 0x6c, 0x6c, 0x61, 21 | 0x74, 0x6f, 0x72, 0x2f, 22 | 0x34, 0x2f, 0x66, 0x72, 23 | 0x65, 0x71, 0x75, 0x65, 24 | 0x6e, 0x63, 0x79, 0x0, 25 | 0x2c, 0x66, 0x0, 0x0, 26 | 0x43, 0xdc, 0x0, 0x0, 27 | ], 28 | (b'/oscillator/4/frequency', [approx(440.0)]), 29 | ) 30 | 31 | message_2 = ( 32 | (b'/foo', [1000, -1, b'hello', 1.234, 5.678]), 33 | [ 34 | 0x2f, 0x66, 0x6f, 0x6f, 35 | 0x0, 0x0, 0x0, 0x0, 36 | 0x2c, 0x69, 0x69, 0x73, 37 | 0x66, 0x66, 0x0, 0x0, 38 | 0x0, 0x0, 0x3, 0xe8, 39 | 0xff, 0xff, 0xff, 0xff, 40 | 0x68, 0x65, 0x6c, 0x6c, 41 | 0x6f, 0x0, 0x0, 0x0, 42 | 0x3f, 0x9d, 0xf3, 0xb6, 43 | 0x40, 0xb5, 0xb2, 0x2d, 44 | ], 45 | (b'/foo', [1000, -1, b'hello', approx(1.234), approx(5.678)]), 46 | ) 47 | 48 | 49 | def test_parse_int(): 50 | assert parse(b'i', struct.pack('>i', 1))[0] == 1 51 | 52 | 53 | def test_parse_float(): 54 | assert parse(b'f', struct.pack('>f', 1.5))[0] == 1.5 55 | 56 | 57 | def test_padd_string(): 58 | for i in range(8): 59 | length = padded(i) 60 | assert length % 4 == 0 61 | assert length >= i 62 | 63 | 64 | def test_parse_string(): 65 | assert parse(b's', struct.pack('%is' % padded(len('t')), b't'))[0] == b't' 66 | s = b'test' 67 | # XXX should we need to add the null byte ourselves? 68 | assert parse( 69 | b's', struct.pack('%is' % padded(len(s) + 1), s + b'\0') 70 | )[0] == s 71 | 72 | 73 | def test_parse_string_encoded(): 74 | assert parse( 75 | b's', struct.pack('%is' % padded(len('t')), u'é'.encode('utf8')), 76 | encoding='utf8' 77 | )[0] == u'é' 78 | 79 | s = u'aééééààààa' 80 | s_ = s.encode('utf8') 81 | assert parse( 82 | b's', 83 | struct.pack('%is' % padded(len(s_) + 1), s_ + b'\0'), 84 | encoding='utf8' 85 | )[0] == s 86 | 87 | with raises(UnicodeDecodeError): 88 | parse( 89 | b's', 90 | struct.pack('%is' % padded(len(s_) + 1), s_ + b'\0'), 91 | encoding='ascii' 92 | )[0] == s 93 | 94 | assert parse( 95 | b's', 96 | struct.pack('%is' % padded(len(s_) + 1), s_ + b'\0'), 97 | encoding='ascii', 98 | encoding_errors='replace' 99 | )[0] == u'a����������������a' 100 | 101 | assert parse( 102 | b's', 103 | struct.pack('%is' % padded(len(s_) + 1), s_ + b'\0'), 104 | encoding='ascii', 105 | encoding_errors='ignore' 106 | )[0] == 'aa' 107 | 108 | 109 | def test_parse_blob(): 110 | length = 10 111 | data = bytes(range(length)) 112 | pad = padded(length) 113 | fmt = '>i%is' % pad 114 | s = struct.pack(fmt, length, data + bytes(pad - length)) 115 | result = parse(b'b', s)[0] 116 | assert result == data 117 | 118 | 119 | def test_parse_midi(): 120 | data = MidiTuple(0, 144, 72, 64) 121 | result = parse(b'm', struct.pack('>I', format_midi(data)))[0] 122 | assert result == data 123 | 124 | 125 | def test_parse_nil(): 126 | result = parse(b'N', '')[0] 127 | assert result == None 128 | 129 | 130 | def test_parse_true(): 131 | result = parse(b'T', '')[0] 132 | assert result == True 133 | 134 | 135 | def test_parse_false(): 136 | result = parse(b'F', '')[0] 137 | assert result == False 138 | 139 | 140 | def test_parse_inf(): 141 | result = parse(b'I', '')[0] 142 | assert result == float('inf') 143 | 144 | 145 | def test_parse_unknown(): 146 | with raises(ValueError): 147 | parse(b'H', struct.pack('>f', 1.5)) 148 | 149 | 150 | def test_read_message(): 151 | source, msg, result = message_1 152 | msg = struct.pack('>%iB' % len(msg), *msg) 153 | address, tags, values, size = read_message(msg) 154 | assert address == result[0] 155 | assert values == result[1] 156 | 157 | source, msg, result = message_2 158 | msg = struct.pack('>%iB' % len(msg), *msg) 159 | address, tags, values, size = read_message(msg) 160 | assert address == result[0] 161 | assert tags == b'iisff' 162 | assert values == result[1] 163 | 164 | 165 | def test_read_message_wrong_address(): 166 | msg, stat = format_message(b'test', []) 167 | with raises(ValueError, match="doesn't start with a '/'") as e: 168 | address, tags, values, size = read_message(msg) 169 | 170 | 171 | def test_read_broken_bundle(): 172 | s = b'not a bundle' 173 | data = struct.pack('>%is' % len(s), s) 174 | with raises(ValueError): 175 | read_bundle(data) 176 | 177 | 178 | def test_read_broken_message(): 179 | # a message where ',' starting the list of tags has been replaced 180 | # with \x00 181 | s = b'/tmp\x00\x00\x00\x00\x00i\x00\x00\x00\x00\x00\x01' 182 | with raises(ValueError): 183 | read_message(s) 184 | 185 | 186 | def test_read_bundle(): 187 | pad = padded(len('#bundle')) 188 | data = struct.pack('>%isQ' % pad, b'#bundle', 1) 189 | 190 | tests = ( 191 | message_1, 192 | message_2, 193 | message_1, 194 | message_2, 195 | ) 196 | 197 | for source, msg, result in tests: 198 | msg = struct.pack('>%iB' % len(msg), *msg) 199 | assert read_message(msg)[::2] == result 200 | data += struct.pack('>i', len(msg)) + msg 201 | 202 | timetag, messages = read_bundle(data) 203 | for test, r in zip(tests, messages): 204 | assert (r[0], r[2]) == test[2] 205 | 206 | 207 | def test_read_packet(): 208 | with raises(ValueError): 209 | read_packet(struct.pack('>%is' % len('test'), b'test')) 210 | 211 | 212 | def tests_format_message(): 213 | for message in message_1, message_2: 214 | source, msg, result = message 215 | msg = struct.pack('>%iB' % len(msg), *msg) 216 | assert format_message(*source)[0] == msg 217 | 218 | 219 | def tests_format_message_null_terminated_address(): 220 | for message in message_1, message_2: 221 | source, msg, result = message 222 | source = source[0] + b'\0', source[1] 223 | msg = struct.pack('>%iB' % len(msg), *msg) 224 | assert format_message(*source)[0] == msg 225 | 226 | 227 | def test_format_true(): 228 | assert format_true(True) == tuple() 229 | 230 | 231 | def test_format_false(): 232 | assert format_false(False) == tuple() 233 | 234 | 235 | def test_format_nil(): 236 | assert format_nil(None) == tuple() 237 | 238 | 239 | def test_format_inf(): 240 | assert format_infinitum(float('inf')) == tuple() 241 | 242 | 243 | def test_format_wrong_types(): 244 | with raises(TypeError): 245 | format_message(b'/test', values=[u'test']) 246 | 247 | 248 | def test_format_unknown_type(): 249 | with raises(TypeError): 250 | format_message(b'/test', values=[object]) 251 | 252 | 253 | def test_format_bundle(): 254 | bundle, stats = format_bundle((message_1[0], message_2[0]), timetag=None) 255 | 256 | assert struct.pack('>%iB' % len(message_1[1]), *message_1[1]) in bundle 257 | assert struct.pack('>%iB' % len(message_2[1]), *message_2[1]) in bundle 258 | 259 | timetag, messages = read_bundle(bundle) 260 | 261 | assert timetag == approx(time()) 262 | assert len(messages) == 2 263 | assert messages[0][::2] == message_1[2] 264 | assert messages[1][::2] == message_2[2] 265 | 266 | assert stats.calls == 2 267 | assert stats.bytes == 72 268 | assert stats.params == 6 269 | assert stats.types['f'] == 3 270 | assert stats.types['i'] == 2 271 | assert stats.types['s'] == 1 272 | 273 | 274 | def test_timetag(): 275 | assert time_to_timetag(None) == (0, 1) 276 | assert time_to_timetag(0)[1] == 0 277 | assert time_to_timetag(30155831.26845886) == (2239144631, 1153022032) 278 | assert timetag_to_time(time_to_timetag(30155831.26845886)) == approx(30155831.26845886) # noqa 279 | 280 | 281 | def test_format_encoding(): 282 | s = u'éééààà' 283 | with raises(TypeError): 284 | read_message(format_message('/test', [s])[0]) 285 | 286 | assert read_message(format_message('/test', [s], encoding='utf8')[0])[2][0] == s.encode('utf8') # noqa 287 | assert read_message(format_message('/test', [s], encoding='utf8')[0], encoding='utf8')[2][0] == s # noqa 288 | 289 | with raises(UnicodeEncodeError): 290 | format_message('/test', [s], encoding='ascii') # noqa 291 | 292 | with raises(UnicodeDecodeError): 293 | read_message(format_message('/test', [s], encoding='utf8')[0], encoding='ascii') # noqa 294 | 295 | assert read_message( 296 | format_message('/test', [s], encoding='utf8')[0], 297 | encoding='ascii', encoding_errors='ignore' 298 | )[2][0] == '' 299 | 300 | assert read_message( 301 | format_message('/test', [s], encoding='utf8')[0], 302 | encoding='ascii', encoding_errors='replace' 303 | )[2][0] == u'������������' 304 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | import pytest 3 | from time import time, sleep 4 | from sys import platform 5 | import socket 6 | from tempfile import mktemp 7 | from os.path import exists 8 | from os import unlink 9 | 10 | from oscpy.server import OSCThreadServer, ServerClass 11 | from oscpy.client import send_message, send_bundle, OSCClient 12 | from oscpy import __version__ 13 | 14 | 15 | def test_instance(): 16 | OSCThreadServer() 17 | 18 | 19 | def test_listen(): 20 | osc = OSCThreadServer() 21 | sock = osc.listen() 22 | osc.stop(sock) 23 | 24 | 25 | def test_getaddress(): 26 | osc = OSCThreadServer() 27 | sock = osc.listen() 28 | assert osc.getaddress(sock)[0] == '127.0.0.1' 29 | 30 | with pytest.raises(RuntimeError): 31 | osc.getaddress() 32 | 33 | sock2 = osc.listen(default=True) 34 | assert osc.getaddress(sock2)[0] == '127.0.0.1' 35 | osc.stop(sock) 36 | 37 | 38 | def test_listen_default(): 39 | osc = OSCThreadServer() 40 | sock = osc.listen(default=True) 41 | 42 | with pytest.raises(RuntimeError) as e_info: # noqa 43 | osc.listen(default=True) 44 | 45 | osc.close(sock) 46 | osc.listen(default=True) 47 | 48 | 49 | def test_close(): 50 | osc = OSCThreadServer() 51 | osc.listen(default=True) 52 | 53 | osc.close() 54 | with pytest.raises(RuntimeError) as e_info: # noqa 55 | osc.close() 56 | 57 | if platform != 'win32': 58 | filename = mktemp() 59 | unix = osc.listen(address=filename, family='unix') 60 | assert exists(filename) 61 | osc.close(unix) 62 | assert not exists(filename) 63 | 64 | 65 | def test_stop_unknown(): 66 | osc = OSCThreadServer() 67 | with pytest.raises(RuntimeError): 68 | osc.stop(socket.socket()) 69 | 70 | 71 | def test_stop_default(): 72 | osc = OSCThreadServer() 73 | osc.listen(default=True) 74 | assert len(osc.sockets) == 1 75 | osc.stop() 76 | assert len(osc.sockets) == 0 77 | 78 | 79 | def test_stop_all(): 80 | osc = OSCThreadServer() 81 | sock = osc.listen(default=True) 82 | host, port = sock.getsockname() 83 | osc.listen() 84 | assert len(osc.sockets) == 2 85 | osc.stop_all() 86 | assert len(osc.sockets) == 0 87 | sleep(.1) 88 | osc.listen(address=host, port=port) 89 | assert len(osc.sockets) == 1 90 | osc.stop_all() 91 | 92 | 93 | def test_terminate_server(): 94 | osc = OSCThreadServer() 95 | assert not osc.join_server(timeout=0.1) 96 | assert osc._thread.is_alive() 97 | osc.terminate_server() 98 | assert osc.join_server(timeout=0.1) 99 | assert not osc._thread.is_alive() 100 | 101 | 102 | def test_send_message_without_socket(): 103 | osc = OSCThreadServer() 104 | with pytest.raises(RuntimeError): 105 | osc.send_message(b'/test', [], 'localhost', 0) 106 | 107 | 108 | @pytest.mark.filterwarnings(pytest.PytestUnhandledThreadExceptionWarning) 109 | def test_intercept_errors(caplog): 110 | 111 | cont = [] 112 | 113 | def success(*values): 114 | cont.append(True) 115 | 116 | def broken_callback(*values): 117 | raise ValueError("some bad value") 118 | 119 | osc = OSCThreadServer() 120 | sock = osc.listen() 121 | port = sock.getsockname()[1] 122 | osc.bind(b'/broken_callback', broken_callback, sock) 123 | osc.bind(b'/success', success, sock) 124 | send_message(b'/broken_callback', [b'test'], 'localhost', port) 125 | sleep(0.01) 126 | send_message(b'/success', [b'test'], 'localhost', port) 127 | assert not osc.join_server(timeout=2) # Thread not stopped 128 | timeout = time() + 2 129 | while not cont: 130 | if time() > timeout: 131 | raise OSError('timeout while waiting for success message.') 132 | sleep(10e-9) 133 | 134 | assert len(caplog.records) == 1, caplog.records 135 | record = caplog.records[0] 136 | assert record.msg == "Unhandled exception caught in oscpy server" 137 | assert not record.args 138 | assert record.exc_info 139 | 140 | osc = OSCThreadServer(intercept_errors=False) 141 | sock = osc.listen() 142 | port = sock.getsockname()[1] 143 | osc.bind(b'/broken_callback', broken_callback, sock) 144 | send_message(b'/broken_callback', [b'test'], 'localhost', port) 145 | assert osc.join_server(timeout=2) # Thread properly sets termination event on crash 146 | 147 | assert len(caplog.records) == 1, caplog.records # Unchanged 148 | 149 | 150 | def test_send_bundle_without_socket(): 151 | osc = OSCThreadServer() 152 | with pytest.raises(RuntimeError): 153 | osc.send_bundle([], 'localhost', 0) 154 | 155 | osc.listen(default=True) 156 | osc.send_bundle( 157 | ( 158 | (b'/test', []), 159 | ), 160 | 'localhost', 1 161 | ) 162 | 163 | 164 | def test_bind(): 165 | osc = OSCThreadServer() 166 | sock = osc.listen() 167 | port = sock.getsockname()[1] 168 | cont = [] 169 | 170 | def success(*values): 171 | cont.append(True) 172 | 173 | osc.bind(b'/success', success, sock) 174 | 175 | send_message(b'/success', [b'test', 1, 1.12345], 'localhost', port) 176 | 177 | timeout = time() + 5 178 | while not cont: 179 | if time() > timeout: 180 | raise OSError('timeout while waiting for success message.') 181 | 182 | 183 | def test_bind_get_address(): 184 | osc = OSCThreadServer() 185 | sock = osc.listen() 186 | port = sock.getsockname()[1] 187 | cont = [] 188 | 189 | def success(address, *values): 190 | assert address == b'/success' 191 | cont.append(True) 192 | 193 | osc.bind(b'/success', success, sock, get_address=True) 194 | 195 | send_message(b'/success', [b'test', 1, 1.12345], 'localhost', port) 196 | 197 | timeout = time() + 5 198 | while not cont: 199 | if time() > timeout: 200 | raise OSError('timeout while waiting for success message.') 201 | 202 | 203 | def test_bind_get_address_smart(): 204 | osc = OSCThreadServer(advanced_matching=True) 205 | sock = osc.listen() 206 | port = sock.getsockname()[1] 207 | cont = [] 208 | 209 | def success(address, *values): 210 | assert address == b'/success/a' 211 | cont.append(True) 212 | 213 | osc.bind(b'/success/?', success, sock, get_address=True) 214 | 215 | send_message(b'/success/a', [b'test', 1, 1.12345], 'localhost', port) 216 | 217 | timeout = time() + 5 218 | while not cont: 219 | if time() > timeout: 220 | raise OSError('timeout while waiting for success message.') 221 | 222 | 223 | def test_reuse_callback(): 224 | osc = OSCThreadServer() 225 | sock = osc.listen() 226 | port = sock.getsockname()[1] 227 | cont = [] 228 | 229 | def success(*values): 230 | cont.append(True) 231 | 232 | osc.bind(b'/success', success, sock) 233 | osc.bind(b'/success', success, sock) 234 | osc.bind(b'/success2', success, sock) 235 | assert len(osc.addresses.get((sock, b'/success'))) == 1 236 | assert len(osc.addresses.get((sock, b'/success2'))) == 1 237 | 238 | 239 | def test_unbind(): 240 | osc = OSCThreadServer() 241 | sock = osc.listen() 242 | port = sock.getsockname()[1] 243 | cont = [] 244 | 245 | def failure(*values): 246 | cont.append(True) 247 | 248 | osc.bind(b'/failure', failure, sock) 249 | with pytest.raises(RuntimeError) as e_info: # noqa 250 | osc.unbind(b'/failure', failure) 251 | osc.unbind(b'/failure', failure, sock) 252 | 253 | send_message(b'/failure', [b'test', 1, 1.12345], 'localhost', port) 254 | 255 | timeout = time() + 1 256 | while time() > timeout: 257 | assert not cont 258 | sleep(10e-9) 259 | 260 | 261 | def test_unbind_default(): 262 | osc = OSCThreadServer() 263 | sock = osc.listen(default=True) 264 | port = sock.getsockname()[1] 265 | cont = [] 266 | 267 | def failure(*values): 268 | cont.append(True) 269 | 270 | osc.bind(b'/failure', failure) 271 | osc.unbind(b'/failure', failure) 272 | 273 | send_message(b'/failure', [b'test', 1, 1.12345], 'localhost', port) 274 | 275 | timeout = time() + 1 276 | while time() > timeout: 277 | assert not cont 278 | sleep(10e-9) 279 | 280 | 281 | def test_bind_multi(): 282 | osc = OSCThreadServer() 283 | sock1 = osc.listen() 284 | port1 = sock1.getsockname()[1] 285 | 286 | sock2 = osc.listen() 287 | port2 = sock2.getsockname()[1] 288 | cont = [] 289 | 290 | def success1(*values): 291 | cont.append(True) 292 | 293 | def success2(*values): 294 | cont.append(False) 295 | 296 | osc.bind(b'/success', success1, sock1) 297 | osc.bind(b'/success', success2, sock2) 298 | 299 | send_message(b'/success', [b'test', 1, 1.12345], 'localhost', port1) 300 | send_message(b'/success', [b'test', 1, 1.12345], 'localhost', port2) 301 | 302 | timeout = time() + 5 303 | while len(cont) < 2: 304 | if time() > timeout: 305 | raise OSError('timeout while waiting for success message.') 306 | 307 | assert True in cont and False in cont 308 | 309 | 310 | def test_bind_address(): 311 | osc = OSCThreadServer() 312 | osc.listen(default=True) 313 | result = [] 314 | 315 | @osc.address(b'/test') 316 | def success(*args): 317 | result.append(True) 318 | 319 | timeout = time() + 1 320 | 321 | send_message(b'/test', [], *osc.getaddress()) 322 | 323 | while len(result) < 1: 324 | if time() > timeout: 325 | raise OSError('timeout while waiting for success message.') 326 | sleep(10e-9) 327 | 328 | assert True in result 329 | 330 | 331 | def test_bind_address_class(): 332 | osc = OSCThreadServer() 333 | osc.listen(default=True) 334 | 335 | @ServerClass 336 | class Test(object): 337 | def __init__(self): 338 | self.result = [] 339 | 340 | @osc.address_method(b'/test') 341 | def success(self, *args): 342 | self.result.append(True) 343 | 344 | timeout = time() + 1 345 | 346 | test = Test() 347 | send_message(b'/test', [], *osc.getaddress()) 348 | 349 | while len(test.result) < 1: 350 | if time() > timeout: 351 | raise OSError('timeout while waiting for success message.') 352 | sleep(10e-9) 353 | 354 | assert True in test.result 355 | 356 | 357 | def test_bind_no_default(): 358 | osc = OSCThreadServer() 359 | 360 | def success(*values): 361 | pass 362 | 363 | with pytest.raises(RuntimeError) as e_info: # noqa 364 | osc.bind(b'/success', success) 365 | 366 | 367 | def test_bind_default(): 368 | osc = OSCThreadServer() 369 | osc.listen(default=True) 370 | port = osc.getaddress()[1] 371 | cont = [] 372 | 373 | def success(*values): 374 | cont.append(True) 375 | 376 | osc.bind(b'/success', success) 377 | 378 | send_message(b'/success', [b'test', 1, 1.12345], 'localhost', port) 379 | 380 | timeout = time() + 5 381 | while not cont: 382 | if time() > timeout: 383 | raise OSError('timeout while waiting for success message.') 384 | 385 | 386 | def test_smart_address_match(): 387 | osc = OSCThreadServer(advanced_matching=True) 388 | 389 | address = osc.create_smart_address(b'/test?') 390 | assert osc._match_address(address, b'/testa') 391 | assert osc._match_address(address, b'/testi') 392 | assert not osc._match_address(address, b'/test') 393 | assert not osc._match_address(address, b'/testaa') 394 | assert not osc._match_address(address, b'/atast') 395 | 396 | address = osc.create_smart_address(b'/?test') 397 | assert osc._match_address(address, b'/atest') 398 | assert osc._match_address(address, b'/etest') 399 | assert not osc._match_address(address, b'/test') 400 | assert not osc._match_address(address, b'/testb') 401 | assert not osc._match_address(address, b'/atast') 402 | 403 | address = osc.create_smart_address(b'/*test') 404 | assert osc._match_address(address, b'/aaaatest') 405 | assert osc._match_address(address, b'/test') 406 | assert not osc._match_address(address, b'/tast') 407 | assert not osc._match_address(address, b'/testb') 408 | assert not osc._match_address(address, b'/atesta') 409 | 410 | address = osc.create_smart_address(b'/t[ea]st') 411 | assert osc._match_address(address, b'/test') 412 | assert osc._match_address(address, b'/tast') 413 | assert not osc._match_address(address, b'/atast') 414 | assert not osc._match_address(address, b'/tist') 415 | assert not osc._match_address(address, b'/testb') 416 | assert not osc._match_address(address, b'/atesta') 417 | 418 | address = osc.create_smart_address(b'/t[^ea]st') 419 | assert osc._match_address(address, b'/tist') 420 | assert osc._match_address(address, b'/tost') 421 | assert not osc._match_address(address, b'/tast') 422 | assert not osc._match_address(address, b'/test') 423 | assert not osc._match_address(address, b'/tostb') 424 | assert not osc._match_address(address, b'/atosta') 425 | 426 | address = osc.create_smart_address(b'/t[^ea]/st') 427 | assert osc._match_address(address, b'/ti/st') 428 | assert osc._match_address(address, b'/to/st') 429 | assert not osc._match_address(address, b'/tist') 430 | assert not osc._match_address(address, b'/tost') 431 | assert not osc._match_address(address, b'/to/stb') 432 | assert not osc._match_address(address, b'/ato/sta') 433 | 434 | address = osc.create_smart_address(b'/t[a-j]t') 435 | assert osc._match_address(address, b'/tit') 436 | assert osc._match_address(address, b'/tat') 437 | assert not osc._match_address(address, b'/tot') 438 | assert not osc._match_address(address, b'/tiit') 439 | assert not osc._match_address(address, b'/tost') 440 | 441 | address = osc.create_smart_address(b'/test/*/stuff') 442 | assert osc._match_address(address, b'/test/blah/stuff') 443 | assert osc._match_address(address, b'/test//stuff') 444 | assert not osc._match_address(address, b'/teststuff') 445 | assert not osc._match_address(address, b'/test/stuffstuff') 446 | assert not osc._match_address(address, b'/testtest/stuff') 447 | 448 | address = osc.create_smart_address(b'/test/{str1,str2}/stuff') 449 | assert osc._match_address(address, b'/test/str1/stuff') 450 | assert osc._match_address(address, b'/test/str2/stuff') 451 | assert not osc._match_address(address, b'/test//stuff') 452 | assert not osc._match_address(address, b'/test/stuffstuff') 453 | assert not osc._match_address(address, b'/testtest/stuff') 454 | 455 | 456 | def test_smart_address_cache(): 457 | osc = OSCThreadServer(advanced_matching=True) 458 | assert osc.create_smart_address(b'/a') == osc.create_smart_address(b'/a') 459 | 460 | 461 | def test_advanced_matching(): 462 | osc = OSCThreadServer(advanced_matching=True) 463 | osc.listen(default=True) 464 | port = osc.getaddress()[1] 465 | result = {} 466 | 467 | def save_result(f): 468 | name = f.__name__ 469 | 470 | def wrapped(*args): 471 | r = result.get(name, []) 472 | r.append(args) 473 | result[name] = r 474 | return f(*args) 475 | return wrapped 476 | 477 | @osc.address(b'/?') 478 | @save_result 479 | def singlechar(*values): 480 | pass 481 | 482 | @osc.address(b'/??') 483 | @save_result 484 | def twochars(*values): 485 | pass 486 | 487 | @osc.address(b'/prefix*') 488 | @save_result 489 | def prefix(*values): 490 | pass 491 | 492 | @osc.address(b'/*suffix') 493 | @save_result 494 | def suffix(*values): 495 | pass 496 | 497 | @osc.address(b'/[abcd]') 498 | @save_result 499 | def somechars(*values): 500 | pass 501 | 502 | @osc.address(b'/{string1,string2}') 503 | @save_result 504 | def somestrings(*values): 505 | pass 506 | 507 | @osc.address(b'/part1/part2') 508 | @save_result 509 | def parts(*values): 510 | pass 511 | 512 | @osc.address(b'/part1/*/part3') 513 | @save_result 514 | def parts_star(*values): 515 | pass 516 | 517 | @osc.address(b'/part1/part2/?') 518 | @save_result 519 | def parts_prefix(*values): 520 | pass 521 | 522 | @osc.address(b'/part1/[abcd]/part3') 523 | @save_result 524 | def parts_somechars(*values): 525 | pass 526 | 527 | @osc.address(b'/part1/[c-f]/part3') 528 | @save_result 529 | def parts_somecharsrange(*values): 530 | pass 531 | 532 | @osc.address(b'/part1/[!abcd]/part3') 533 | @save_result 534 | def parts_notsomechars(*values): 535 | pass 536 | 537 | @osc.address(b'/part1/[!c-f]/part3') 538 | @save_result 539 | def parts_notsomecharsrange(*values): 540 | pass 541 | 542 | @osc.address(b'/part1/{string1,string2}/part3') 543 | @save_result 544 | def parts_somestrings(*values): 545 | pass 546 | 547 | @osc.address(b'/part1/part2/{string1,string2}') 548 | @save_result 549 | def parts_somestrings2(*values): 550 | pass 551 | 552 | @osc.address(b'/part1/part2/prefix-{string1,string2}') 553 | @save_result 554 | def parts_somestrings3(*values): 555 | pass 556 | 557 | send_bundle( 558 | ( 559 | (b'/a', [1]), 560 | (b'/b', [2]), 561 | (b'/z', [3]), 562 | (b'/1', [3]), 563 | (b'/?', [4]), 564 | 565 | (b'/ab', [5]), 566 | (b'/bb', [6]), 567 | (b'/z?', [7]), 568 | (b'/??', [8]), 569 | (b'/?*', [9]), 570 | 571 | (b'/prefixab', [10]), 572 | (b'/prefixbb', [11]), 573 | (b'/prefixz?', [12]), 574 | (b'/prefix??', [13]), 575 | (b'/prefix?*', [14]), 576 | 577 | (b'/absuffix', [15]), 578 | (b'/bbsuffix', [16]), 579 | (b'/z?suffix', [17]), 580 | (b'/??suffix', [18]), 581 | (b'/?*suffix', [19]), 582 | 583 | (b'/string1', [20]), 584 | (b'/string2', [21]), 585 | (b'/string1aa', [22]), 586 | (b'/string1b', [23]), 587 | (b'/string1?', [24]), 588 | (b'/astring1?', [25]), 589 | 590 | (b'/part1', [26]), 591 | (b'/part1/part', [27]), 592 | (b'/part1/part2', [28]), 593 | (b'/part1/part3/part2', [29]), 594 | (b'/part1/part2/part3', [30]), 595 | (b'/part1/part?/part2', [31]), 596 | 597 | (b'/part1', [32]), 598 | (b'/part1/a/part', [33]), 599 | (b'/part1/b/part2', [34]), 600 | (b'/part1/c/part3/part2', [35]), 601 | (b'/part1/d/part2/part3', [36]), 602 | (b'/part1/e/part?/part2', [37]), 603 | 604 | (b'/part1/test/part2', [38]), 605 | (b'/part1/a/part2', [39]), 606 | (b'/part1/b/part2', [40]), 607 | (b'/part1/c/part2/part2', [41]), 608 | (b'/part1/d/part2/part3', [42]), 609 | (b'/part1/0/part2', [43]), 610 | 611 | (b'/part1/string1/part', [45]), 612 | (b'/part1/string2/part3', [46]), 613 | (b'/part1/part2/string1', [47]), 614 | (b'/part1/part2/string2', [48]), 615 | (b'/part1/part2/prefix-string1', [49]), 616 | (b'/part1/part2/sprefix-tring2', [50]), 617 | ), 618 | 'localhost', port 619 | ) 620 | 621 | expected = { 622 | 'singlechar': [(1,), (2,), (3,), (3,), (4,)], 623 | 'twochars': [(5,), (6,), (7,), (8,), (9,)], 624 | 'prefix': [(10,), (11,), (12,), (13,), (14,)], 625 | 'suffix': [(15,), (16,), (17,), (18,), (19,)], 626 | 'somechars': [(1,), (2,)], 627 | 'somestrings': [(20,), (21,)], 'parts': [(28,)], 628 | 'parts_star': [(30,), (46,)], 629 | 'parts_somestrings': [(46,)], 630 | 'parts_somestrings2': [(47,), (48,)], 631 | 'parts_somestrings3': [(49,)] 632 | } 633 | 634 | timeout = time() + 5 635 | while result != expected: 636 | if time() > timeout: 637 | print("expected: {}\n result: {}\n".format(expected, result)) 638 | raise OSError('timeout while waiting for expected result.') 639 | sleep(10e-9) 640 | 641 | 642 | def test_decorator(): 643 | osc = OSCThreadServer() 644 | sock = osc.listen(default=True) 645 | port = sock.getsockname()[1] 646 | cont = [] 647 | 648 | @osc.address(b'/test1', sock) 649 | def test1(*values): 650 | print("test1 called") 651 | cont.append(True) 652 | 653 | @osc.address(b'/test2') 654 | def test2(*values): 655 | print("test1 called") 656 | cont.append(True) 657 | 658 | send_message(b'/test1', [], 'localhost', port) 659 | send_message(b'/test2', [], 'localhost', port) 660 | 661 | timeout = time() + 1 662 | while len(cont) < 2: 663 | if time() > timeout: 664 | raise OSError('timeout while waiting for success message.') 665 | 666 | 667 | def test_answer(): 668 | cont = [] 669 | 670 | osc_1 = OSCThreadServer() 671 | osc_1.listen(default=True) 672 | 673 | @osc_1.address(b'/ping') 674 | def ping(*values): 675 | if True in values: 676 | osc_1.answer(b'/zap', [True], port=osc_3.getaddress()[1]) 677 | else: 678 | osc_1.answer( 679 | bundle=[ 680 | (b'/pong', []) 681 | ] 682 | ) 683 | 684 | osc_2 = OSCThreadServer() 685 | osc_2.listen(default=True) 686 | 687 | @osc_2.address(b'/pong') 688 | def pong(*values): 689 | osc_2.answer(b'/ping', [True]) 690 | 691 | osc_3 = OSCThreadServer() 692 | osc_3.listen(default=True) 693 | 694 | @osc_3.address(b'/zap') 695 | def zap(*values): 696 | if True in values: 697 | cont.append(True) 698 | 699 | osc_2.send_message(b'/ping', [], *osc_1.getaddress()) 700 | 701 | with pytest.raises(RuntimeError) as e_info: # noqa 702 | osc_1.answer(b'/bing', []) 703 | 704 | timeout = time() + 2 705 | while not cont: 706 | if time() > timeout: 707 | raise OSError('timeout while waiting for success message.') 708 | sleep(10e-9) 709 | 710 | 711 | def test_socket_family(): 712 | osc = OSCThreadServer() 713 | assert osc.listen().family == socket.AF_INET 714 | filename = mktemp() 715 | if platform != 'win32': 716 | assert osc.listen(address=filename, family='unix').family == socket.AF_UNIX # noqa 717 | 718 | else: 719 | with pytest.raises(AttributeError) as e_info: 720 | osc.listen(address=filename, family='unix') 721 | 722 | if exists(filename): 723 | unlink(filename) 724 | 725 | with pytest.raises(ValueError) as e_info: # noqa 726 | osc.listen(family='') 727 | 728 | 729 | def test_encoding_send(): 730 | osc = OSCThreadServer() 731 | osc.listen(default=True) 732 | 733 | values = [] 734 | 735 | @osc.address(b'/encoded') 736 | def encoded(*val): 737 | for v in val: 738 | assert isinstance(v, bytes) 739 | values.append(val) 740 | 741 | send_message( 742 | u'/encoded', 743 | ['hello world', u'ééééé ààààà'], 744 | *osc.getaddress(), encoding='utf8') 745 | 746 | timeout = time() + 2 747 | while not values: 748 | if time() > timeout: 749 | raise OSError('timeout while waiting for success message.') 750 | sleep(10e-9) 751 | 752 | 753 | def test_encoding_receive(): 754 | osc = OSCThreadServer(encoding='utf8') 755 | osc.listen(default=True) 756 | 757 | values = [] 758 | 759 | @osc.address(u'/encoded') 760 | def encoded(*val): 761 | for v in val: 762 | assert not isinstance(v, bytes) 763 | values.append(val) 764 | 765 | send_message( 766 | b'/encoded', 767 | [ 768 | b'hello world', 769 | u'ééééé ààààà'.encode('utf8') 770 | ], 771 | *osc.getaddress()) 772 | 773 | timeout = time() + 2 774 | while not values: 775 | if time() > timeout: 776 | raise OSError('timeout while waiting for success message.') 777 | sleep(10e-9) 778 | 779 | 780 | def test_encoding_send_receive(): 781 | osc = OSCThreadServer(encoding='utf8') 782 | osc.listen(default=True) 783 | 784 | values = [] 785 | 786 | @osc.address(u'/encoded') 787 | def encoded(*val): 788 | for v in val: 789 | assert not isinstance(v, bytes) 790 | values.append(val) 791 | 792 | send_message( 793 | u'/encoded', 794 | ['hello world', u'ééééé ààààà'], 795 | *osc.getaddress(), encoding='utf8') 796 | 797 | timeout = time() + 2 798 | while not values: 799 | if time() > timeout: 800 | raise OSError('timeout while waiting for success message.') 801 | sleep(10e-9) 802 | 803 | 804 | def test_default_handler(): 805 | results = [] 806 | 807 | def test(address, *values): 808 | results.append((address, values)) 809 | 810 | osc = OSCThreadServer(default_handler=test) 811 | osc.listen(default=True) 812 | 813 | @osc.address(b'/passthrough') 814 | def passthrough(*values): 815 | pass 816 | 817 | osc.send_bundle( 818 | ( 819 | (b'/test', []), 820 | (b'/passthrough', []), 821 | (b'/test/2', [1, 2, 3]), 822 | ), 823 | *osc.getaddress() 824 | ) 825 | 826 | timeout = time() + 2 827 | while len(results) < 2: 828 | if time() > timeout: 829 | raise OSError('timeout while waiting for success message.') 830 | sleep(10e-9) 831 | 832 | expected = ( 833 | (b'/test', tuple()), 834 | (b'/test/2', (1, 2, 3)), 835 | ) 836 | 837 | for e, r in zip(expected, results): 838 | assert e == r 839 | 840 | 841 | def test_get_version(): 842 | osc = OSCThreadServer(encoding='utf8') 843 | osc.listen(default=True) 844 | 845 | values = [] 846 | 847 | @osc.address(u'/_oscpy/version/answer') 848 | def cb(val): 849 | print(val) 850 | values.append(val) 851 | 852 | send_message( 853 | b'/_oscpy/version', 854 | [ 855 | osc.getaddress()[1] 856 | ], 857 | *osc.getaddress(), 858 | encoding='utf8', 859 | encoding_errors='strict' 860 | ) 861 | 862 | timeout = time() + 2 863 | while not values: 864 | if time() > timeout: 865 | raise OSError('timeout while waiting for success message.') 866 | sleep(10e-9) 867 | 868 | assert __version__ in values 869 | 870 | 871 | def test_get_routes(): 872 | osc = OSCThreadServer(encoding='utf8') 873 | osc.listen(default=True) 874 | 875 | values = [] 876 | 877 | @osc.address(u'/test_route') 878 | def dummy(*val): 879 | pass 880 | 881 | @osc.address(u'/_oscpy/routes/answer') 882 | def cb(*routes): 883 | values.extend(routes) 884 | 885 | send_message( 886 | b'/_oscpy/routes', 887 | [ 888 | osc.getaddress()[1] 889 | ], 890 | *osc.getaddress(), 891 | encoding='utf8', 892 | encoding_errors='strict' 893 | ) 894 | 895 | timeout = time() + 2 896 | while not values: 897 | if time() > timeout: 898 | raise OSError('timeout while waiting for success message.') 899 | sleep(10e-9) 900 | 901 | assert u'/test_route' in values 902 | 903 | 904 | def test_get_sender(): 905 | osc = OSCThreadServer(encoding='utf8') 906 | osc.listen(default=True) 907 | 908 | values = [] 909 | 910 | @osc.address(u'/test_route') 911 | def callback(*val): 912 | values.append(osc.get_sender()) 913 | 914 | with pytest.raises(RuntimeError, 915 | match=r'get_sender\(\) not called from a callback'): 916 | osc.get_sender() 917 | 918 | send_message( 919 | b'/test_route', 920 | [ 921 | osc.getaddress()[1] 922 | ], 923 | *osc.getaddress(), 924 | encoding='utf8' 925 | ) 926 | 927 | timeout = time() + 2 928 | while not values: 929 | if time() > timeout: 930 | raise OSError('timeout while waiting for success message.') 931 | sleep(10e-9) 932 | 933 | 934 | def test_server_different_port(): 935 | # used for storing values received by callback_3000 936 | checklist = [] 937 | 938 | def callback_3000(*values): 939 | checklist.append(values[0]) 940 | 941 | # server, will be tested: 942 | server_3000 = OSCThreadServer(encoding='utf8') 943 | sock_3000 = server_3000.listen(address='0.0.0.0', port=3000, default=True) 944 | server_3000.bind(b'/callback_3000', callback_3000) 945 | 946 | # clients sending to different ports, used to test the server: 947 | client_3000 = OSCClient(address='localhost', port=3000, encoding='utf8') 948 | 949 | # server sends message to himself, should work: 950 | server_3000.send_message( 951 | b'/callback_3000', 952 | ["a"], 953 | ip_address='localhost', 954 | port=3000 955 | ) 956 | sleep(0.05) 957 | 958 | # client sends message to server, will be received properly: 959 | client_3000.send_message(b'/callback_3000', ["b"]) 960 | sleep(0.05) 961 | 962 | # sever sends message on different port, might crash the server on windows: 963 | server_3000.send_message( 964 | b'/callback_3000', 965 | ["nobody is going to receive this"], 966 | ip_address='localhost', 967 | port=3001 968 | ) 969 | sleep(0.05) 970 | 971 | # client sends message to server again. if server is dead, message 972 | # will not be received: 973 | client_3000.send_message(b'/callback_3000', ["c"]) 974 | sleep(0.1) 975 | 976 | # if 'c' is missing in the received checklist, the server thread 977 | # crashed and could not recieve the last message from the client: 978 | assert checklist == ['a', 'b', 'c'] 979 | 980 | server_3000.stop() # clean up 981 | 982 | 983 | def test_validate_message_address_disabled(): 984 | osc = OSCThreadServer(validate_message_address=False) 985 | osc.listen(default=True) 986 | 987 | received = [] 988 | 989 | @osc.address(b'malformed') 990 | def malformed(*val): 991 | received.append(val[0]) 992 | 993 | send_message( 994 | b'malformed', 995 | [ b'message' ], 996 | *osc.getaddress()) 997 | 998 | timeout = time() + 2 999 | while not received: 1000 | if time() > timeout: 1001 | raise OSError('timeout while waiting for success message.') 1002 | sleep(10e-9) 1003 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from textwrap import dedent 3 | 4 | from oscpy.stats import Stats 5 | 6 | 7 | def test_create_stats(): 8 | stats = Stats(calls=1, bytes=2, params=3, types=Counter('abc')) 9 | assert stats.calls == 1 10 | assert stats.bytes == 2 11 | assert stats.params == 3 12 | assert stats.types['a'] == 1 13 | 14 | 15 | def test_add_stats(): 16 | stats = Stats(calls=1) + Stats(calls=3, bytes=2) 17 | assert stats.calls == 4 18 | assert stats.bytes == 2 19 | 20 | 21 | def test_compare_stats(): 22 | assert Stats( 23 | calls=1, bytes=2, params=3, types=Counter('abc') 24 | ) == Stats( 25 | calls=1, bytes=2, params=3, types=Counter('abc') 26 | ) 27 | 28 | 29 | def test_to_tuple_stats(): 30 | tpl = Stats( 31 | calls=1, bytes=2, params=3, types=Counter("import antigravity") 32 | ).to_tuple() 33 | 34 | assert tpl[:3] == (1, 2, 3,) 35 | assert set(tpl[3]) == set('import antigravity') 36 | 37 | 38 | def test_repr_stats(): 39 | r = repr(Stats(calls=0, bytes=1, params=2, types=Counter('abc'))) 40 | assert r == dedent( 41 | ''' 42 | Stats: 43 | calls: 0 44 | bytes: 1 45 | params: 2 46 | types: 47 | a: 1 48 | b: 1 49 | c: 1 50 | ''').strip() 51 | -------------------------------------------------------------------------------- /tools/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | # exec git diff-index --check --cached $against -- 50 | 51 | git stash save --keep-index --quiet 52 | pytest && pycodestyle && pydocstyle 53 | err=$? 54 | git stash pop --quiet 55 | exit $err 56 | --------------------------------------------------------------------------------