Commit 936be9d0 authored by Nathan Graule's avatar Nathan Graule 💻

Merge branch 'release/0.1.1'

parents 7d923a7c de41a1df
Pipeline #27365543 failed with stages
in 2 minutes and 10 seconds
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
*.whl
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Code editors
.idea
.vscode
\ No newline at end of file
image: python:3.7-alpine
cache:
key: $CI_COMMIT_REF
paths:
- .cache
variables:
PIP_DOWNLOAD_CACHE: .cache
stages:
- build
- dist
build:py2:
stage: build
image: python:2.7.15-alpine
before_script:
- pip install wheel
- python -V
script:
- pip wheel .
- mkdir -p dist
- mv *.whl dist
artifacts:
paths:
- dist
name: Python 2
build:py3:
stage: build
before_script:
- pip install wheel
script:
- pip wheel .
- mkdir -p dist
- mv *.whl dist
artifacts:
paths:
- dist
name: Python 3
test:py2:
stage: build
image: python:2.7-alpine
before_script:
- pip install nose typing
- python -V
script:
- nosetests -v tests
test:py3:
stage: build
before_script:
- pip install nose coverage
script:
- nosetests -v --with-coverage --cover-package=call tests
artifacts:
paths:
- .coverage
expire_in: 1 hour
dist-test:
stage: dist
dependencies:
- build:py2
- build:py3
variables:
GIT_STRATEGY: none
before_script:
- pip install twine
script:
- >
twine upload dist/*.whl
--repository-url https://test.pypi.org/legacy/
-u $PIP_UPLOAD_USER
-p $PIP_UPLOAD_TEST_PASSWD
except:
- master
dist:
stage: dist
dependencies:
- build:py2
- build:py3
variables:
GIT_STRATEGY: none
before_script:
- pip install twine
script:
- twine upload dist/*.whl -u $PIP_UPLOAD_USER -p $PIP_UPLOAD_PASSWD
only:
- master
pages:
stage: dist
dependencies:
- test:py3
before_script:
- pip install coverage
script:
- coverage html
after_script:
- mv htmlcov public
artifacts:
paths:
- public
expire_in: 1 hour
codacy:
stage: dist
dependencies:
- test:py3
before_script:
- apk add git
- pip install codacy-coverage coverage
script:
- coverage xml
- python-codacy-coverage -r coverage.xml
\ No newline at end of file
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Asynchronous process of callback functions
- Ability to chain calls
- Ability to catch in chains
- Ability to block until resolve/reject
- Ability to join the underlying threads
\ No newline at end of file
include README.rst
\ No newline at end of file
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
nose = "*"
[requires]
python_version = "3.6"
{
"_meta": {
"hash": {
"sha256": "0b3dbf9bb3856f8e27371fe3d067e7848bea03d79c1f5f29dc10401629c19b5e"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {},
"develop": {
"nose": {
"hashes": [
"sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac",
"sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a",
"sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"
],
"index": "pypi",
"version": "==1.3.7"
}
}
}
Call
====
.. image:: https://api.codacy.com/project/badge/Grade/91959f98ff34469884415e96ba2ff763 :target: https://www.codacy.com/app/solarliner/call?utm_source=gitlab.com&utm_medium=referral&utm_content=solarliner/call&utm_campaign=Badge_Grade
Thread-based, JS-like asynchronous calls for Python. Works in both
Python 2.7 and Python 3.5+.
Install
-------
Release version:
.. code:: bash
pip install call
Development version
.. code:: bash
git clone https://gitlab.com/solarliner/call.git
cd call
# Activate virtualenv if needed
python setup.py install
The library requires no other dependencies, and (will soon) support
Python's ``await`` keyword.
Use
---
Create a call:
.. code:: python
def cb(resolve, reject):
result = factorial(100)
resolve(result)
call = Call(cb)
Wrap a synchronous function in a Call:
.. code:: python
call = Call.from_function(factorial, 10)
Chain calls with the ``then`` keyword
.. code:: python
call = Call(cb).then(lambda val: print(val))
Catch errors:
.. code:: python
call = Call(cb)\
.then(lambda val: raise Exception())\
.catch(lambda err: print('Whoops'))
Compose calls:
.. code:: python
results = Call.all([Call(cb) for _ in range(10)])
Block thread until resolved (or raises on failure):
.. code:: python
result = call.wait()
Wait for call to either resolve or reject. Note that it is not recommended to get the data directly, as it may be
``None``, which may or may not indicate that an error has occurred.
.. code:: python
call.join()
result = call.data # Not recommended
Contribute
----------
The repository follows the ``git flow`` standards. Create a feature branch, then ask for a pull/merge request.
The main repository is on GitLab, however the GitHub mirror is functional and you should be able to ask for pull
requests. However, they will be processed in GitLab.
\ No newline at end of file
from threading import Thread
from typing import Callable, Any, TypeVar, Optional
from typing import Iterable
__all__ = ['Call']
T = TypeVar('T')
TT = TypeVar('TT')
E = TypeVar('E')
Thenable = Callable[[T], TT]
Resolvable = Callable[[T], None]
Rejectable = Callable[[E], None]
Callback = Callable[[Callable, Callable], Any]
class Call:
"""Asynchronously run code, letting further code subscribe to resolved values or failed exceptions."""
PENDING = 'PENDING'
RESOLVED = 'RESOLVED'
REJECTED = 'REJECTED'
def __init__(self, callback):
# type: (Callback) -> Call
"""Initialize a new asynchronous Call.
The callback must have signature (resolve, reject), which are two callback functions of their own; the first one
is to be called with the resulting value, while the second one is to be called with an error.
It is vividly recommended that the value in reject be an Exception.
:param callback: Callback function. Must have (resolve, reject) functions."""
self.status = self.PENDING
self.data = None # type: T
self.error = None # type: E
self.t = Thread(target=callback, args=(self._on_resolve, self._on_rejected))
self.t.start()
def then(self, callback):
# type: (Thenable) -> Call
"""Chain callback, called with the resolved value of the previous Call.
:param callback: Callback function to be called with the resolved value of the current Call."""
def cb(resolve, reject):
# type: (Callable, Callable) -> None
self.t.join()
if self.status == self.REJECTED:
reject(self.error)
else:
try:
result = callback(self.data)
resolve(result)
except Exception as e:
reject(e)
return Call(cb)
def catch(self, callback):
# type: (Thenable) -> Call
"""Chain callback, called if a failure occured somewhere in the chain before this.
:param callback: Callback function, called on error further up the chain."""
def cb(resolve, reject):
self.t.join()
if self.status == self.REJECTED:
try:
result = callback(self.error)
resolve(result)
except Exception as e:
reject(e)
return Call(cb)
def wait(self):
# type: () -> T
"""Wait until call has resolved a value to return, or rejected to raise the exception."""
self.t.join()
if self.status == self.RESOLVED:
return self.data
else:
if isinstance(self.error, Exception):
raise self.error
else:
raise Exception(self.error)
def join(self):
# type: () -> None
"""Wait until the value has been resolved or rejected, but does not return the value nor raise."""
self.t.join()
@classmethod
def resolve(cls, value=None):
# type: (Optional[T]) -> Call
"""Create a Call that immediately resolves with the value
:param value: Value to be resolved to"""
return Call(lambda res, rej: res(value))
@classmethod
def reject(cls, error):
# type: (E) -> Call
"""Create a Call that immediately rejects with the error
:param error: Error to be passed. If not an exception, will be turned into one."""
if not isinstance(error, Exception):
error = Exception(error)
return Call(lambda res, rej: rej(error))
@classmethod
def all(cls, calls):
# type: (Iterable[Call]) -> Call
"""Resolve a list of calls' resolved values, or fail with the first exception
:param calls: List of calls to resolve, in the same order than the Calls list"""
def func():
values = []
try:
for call in calls:
values.append(call.wait())
except Exception:
raise
return values
return Call.from_function(func)
@classmethod
def from_function(cls, func, *args, **kwargs):
# type: (Callable[[Any], T], *Any, **Any) -> Call
"""Create a Call from a synchronous function. The function will then be called asynchronously, its return
value used as the resolved value, and any exception raised as a reject error value.
:param func: Synchronous function to be called
:param args: Positional arguments to be passed to the function func
:param kwargs: Dictionary arguments to be passed to the function func"""
def cb(resolve, reject):
# type: (Callable, Callable) -> None
try:
resolve(func(*args, **kwargs))
except Exception as e:
reject(e)
return Call(cb)
def _on_resolve(self, data):
# type: (T) -> None
"""DO NOT USE. IS INTERNAL"""
self.data = data
self.error = None
self.status = self.RESOLVED
def _on_rejected(self, error):
# type: (E) -> None
"""DO NOT USE. IS INTERNAL"""
self.error = error
self.data = None
self.status = self.REJECTED
import sys
import unittest
from setuptools import setup
def get_requirements():
requirements = [] # TODO: Get from Pipfile
if sys.version_info[0] == 2:
requirements.append('typing')
return requirements
def readme():
with open('README.rst', mode='r') as f:
return f.read()
def test_suite():
loader = unittest.TestLoader()
return loader.discover('tests', pattern='test_*.py')
setup(
name='call',
version='0.1.1',
packages=['call'],
url='https://gitlab.com/solarliner/call',
license='MIT',
author='Nathan Graule',
author_email='solarliner@gmail.com',
description='Thread-based, JS-like asynchronous calls for Python.',
install_requires=get_requirements(),
long_description=readme(),
classifiers=[
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Software Development :: Libraries :: Python Modules'
],
test_suite='setup.test_suite'
)
{
"app-id": "org.sindresorhus.Caprine",
"branch": "stable",
"base": "io.atom.electron.BaseApp",
"base-version": "stable",
"runtime": "org.freedesktop.Platform",
"runtime-version": "1.6",
"sdk": "org.freedesktop.Sdk",
"command": "caprine",
"separate-locales": false,
"tags": ["messenger", "electron", "open-source"],
"finish-args": [
"--share=ipc",
"--socket=pulseaudio",
"--share=network",
"--device=dri",
"--talk-name=org.gnome.SettingsDaemon",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.gnome.SessionManager",
"--env=LD_LIBRARY_PATH=/app/lib"
],
"modules": [
{
"name": "caprine",
"sources": {
"type": "git",
"url":
}
}
]
}
\ No newline at end of file
from __future__ import print_function
import json
import os
import random
import time
import unittest
from call import Call
# noinspection PyMethodMayBeStatic
class TestCall(unittest.TestCase):
def callback_succeeds(self, resolve, _):
resolve('Data')