Metadata-Version: 2.1
Name: psygnal
Version: 0.1.0rc2
Summary: Pure python implementation of Qt Signals
Home-page: https://github.com/tlambert03/psygnal
Author: Talley Lambert
Author-email: talley.lambert@gmail.com
License: BSD-3-Clause
Project-URL: Source Code, https://github.com/tlambert03/psygnal
Platform: UNKNOWN
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typing-extensions
Provides-Extra: build
Requires-Dist: Cython ; (sys_platform != "win32") and extra == 'build'
Provides-Extra: dev
Requires-Dist: black ; extra == 'dev'
Requires-Dist: flake8 ; extra == 'dev'
Requires-Dist: flake8-docstrings ; extra == 'dev'
Requires-Dist: ipython ; extra == 'dev'
Requires-Dist: isort ; extra == 'dev'
Requires-Dist: jedi (<0.18.0) ; extra == 'dev'
Requires-Dist: mypy ; extra == 'dev'
Requires-Dist: pre-commit ; extra == 'dev'
Requires-Dist: pydocstyle ; extra == 'dev'
Requires-Dist: pytest ; extra == 'dev'
Provides-Extra: testing
Requires-Dist: pytest ; extra == 'testing'
Requires-Dist: pytest-cov ; extra == 'testing'

# psygnal

[![License](https://img.shields.io/pypi/l/psygnal.svg?color=green)](https://github.com/tlambert03/psygnal/raw/master/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/psygnal.svg?color=green)](https://pypi.org/project/psygnal)
[![Python Version](https://img.shields.io/pypi/pyversions/psygnal.svg?color=green)](https://python.org)
[![CI](https://github.com/tlambert03/psygnal/actions/workflows/ci.yml/badge.svg)](https://github.com/tlambert03/psygnal/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/tlambert03/psygnal/branch/master/graph/badge.svg)](https://codecov.io/gh/tlambert03/psygnal)

Pure python implementation of Qt-style Signals, with (optional) signature and type checking, and support for threading.

## Usage

### install

```sh
pip install psygnal
```

### basic usage

If you are familiar with the Qt Signals & Slots API as implemented in PySide and
PyQt5, then you should be good to go!  `psygnal` aims to be a superset of those
APIs  (some functions do accept additional arguments, like
[`check_nargs`](#connection-safety-number-of-arguments) and
[`check_types`](#connection-safety-types)).

<small><em>
Note, the name `Signal` is used here instead of `pyqtSignal`, following the
`qtpy` and `PySide` convention.
</em></small>
```py
from psygnal import Signal

# create an object with class attribute Signals
class MyObj:

    # this signal will emit a single string
    value_changed = Signal(str)

    def __init__(self, value=0):
        self._value = value

    def set_value(self, value):
        if value != self._value:
            self._value = str(value)
            # emit the signal
            self.value_changed.emit(self._value)

def on_value_changed(new_value):
    print(f"The new value is {new_value!r}")

# instantiate the object with Signals
obj = MyObj()

# connect one or more callbacks with `connect`
obj.value_changed.connect(on_value_changed)

# callbacks are called when value changes
obj.set_value('hello!')  # prints: 'The new value is 'hello!'

# disconnect callbacks with `disconnect`
obj.value_changed.disconnect(on_value_changed)
```

### as a decorator

`.connect()` returns the object that it is passed, and so
can be used as a decorator.

```py
@obj.value_changed.connect
def some_other_callback(value):
    print(f"I also received: {value!r}")

obj.set_value('world!')
# prints:
# I also received: 'world!'
```

### connection safety (number of arguments)

`psygnal` prevents you from connecting a callback function that is ***guaranteed
to fail*** due to an incompatible number of positional arguments.  For example,
the following callback has too many arguments for our Signal (which we declared above as emitting a single argument: `Signal(str)`)

```py
def i_require_two_arguments(first, second):
    print(first, second)

obj.value_changed.connect(i_require_two_arguments)
```

raises:

```py
ValueError: Cannot connect slot 'i_require_two_arguments' with signature: (first, second):
- Slot requires at least 2 positional arguments, but spec only provides 1

Accepted signature: (p0: str, /)
```

<small><em>
Note: Positional argument checking can be disabled with <code>connect(...,
check_nargs=False)</code>
</em></small>

#### extra positional arguments ignored

While a callback may not require *more* positional arguments than the signature
of the `Signal` to which it is connecting, it *may* accept less.  Extra
arguments will be discarded when emitting the signal (so it
isn't necessary to create a `lambda` to swallow unnecessary arguments):

```py
obj = MyObj()

@obj.value_changed.connect
def no_args_please():
    print(locals())

# otherwise one might need
# obj.value_changed.connect(lambda a: no_args_please())

obj.value_changed.emit('hi')  # prints: "{}"
```


### connection safety (types)

For type safety when connecting slots, use `check_types=True` when connecting a callback.  Recall that our signal was declared as accepting a string `Signal(str)`.  The following function has an incompatible type annotation: `x: int`.

```py
# this would fail because you cannot concatenate a string and int
def i_expect_an_integer(x: int):
    print(f'{x} + 4 = {x + 4}')

# psygnal won't let you connect it
obj.value_changed.connect(i_expect_an_integer, check_types=True)
```

raises:

```py
ValueError: Cannot connect slot 'i_expect_an_integer' with signature: (x: int):
- Slot types (x: int) do not match types in signal.

Accepted signature: (p0: str, /)
```

<small><em>
Note: unlike Qt, `psygnal` does <strong>not</strong> perform any type coercion
when emitting a value.
</em></small>

### query the sender

Similar to Qt's [`QObject.sender()`](https://doc.qt.io/qt-5/qobject.html#sender) method, a callback can query the sender using the `Signal.sender()` class method.  (The implementation is of course different than Qt, since the receiver is not a `QObject`.)

```py
obj = MyObj()

def curious():
    print("Sent by", Signal.sender())
    assert Signal.sender() == obj

obj.value_changed.connect(curious)
obj.value_changed.emit(10)

# prints (and does not raise):
# Sent by <__main__.MyObj object at 0x1046a30d0>
```

<small><em>
If you want the actual signal instance that is emitting the signal (`obj.value_changed` in the above example), use `Signal.current_emitter()`.</em></small>

### emitting signals asynchronously (threading)

There is experimental support for calling all connected slots in another thread, using `emit(..., asynchronous=True)`

```py
obj = MyObj()

def slow_callback(arg):
    import time
    time.sleep(0.5)
    print(f"Hi {arg!r}, from another thread")

obj.value_changed.connect(slow_callback)
```

This one is called synchronously

```py
obj.value_changed.emit('friend')
print("Hi, from main thread.")

# after 0.5 seconds, prints:
# Hi 'friend', from another thread
# Hi, from main thread.
```

This one is called asynchronously

```py
thread = obj.value_changed.emit('friend', asynchronous=True)
print("Hi, from main thread.")

# immediately prints
# Hi, from main thread.

# then after 0.5 seconds this will print:
# Hi 'friend', from another thread
```

**Note:** The user is responsible for `joining` and managing the `threading.Thread` instance returned when calling `.emit(..., asynchronous=True)`.

**Experimental:**  While thread-safety is the goal, (`RLocks` are used during important state mutations) it is not guaranteed.  Please use at your own risk.  Issues/PRs welcome.

## Other similar libraries

There are other libraries that implement similar event-based signals, they may server your purposes better depending on what you are doing.

### [PySignal](https://github.com/dgovil/PySignal) (deprecated)

This package borrows inspiration from – and is most similar to – the now deprecated [PySignal](https://github.com/dgovil/PySignal) project, with a few notable new features in `psygnal` regarding signature and type checking, sender querying, and threading.

#### similarities with `PySignal`

- still a "Qt-style" signal implementation that doesn't depend on Qt
- supports class methods, functions, lambdas and partials

#### differences with `PySignal`

- the class attribute `pysignal.ClassSignal` is called simply `Signal` in `psygnal` (to more closely match the PyQt/Pyside syntax).  Correspondingly `pysignal.Signal` is similar to `psygnal.SignalInstance`.
- Whereas `PySignal` refrained from doing any signature and/or type checking
  either at slot-connection time, or at signal emission time, `psygnal` offers
  signature declaration similar to Qt with , for example, `Signal(int, int)`.
  along with opt-in signature compatibility (with `check_nargs=True`) and type
  checking (with `check_types=True`). `.connect(..., check_nargs=True)` in
  particular ensures that any slot to connected to a signal will at least be
  compatible with the emitted arguments.
- You *can* query the sender in `psygnal` by using the `Signal.sender()` or `Signal.current_emitter()` class methods. (The former returns the *instance* emitting the signal, similar to Qt's [`QObject.sender()`](https://doc.qt.io/qt-5/qobject.html#sender) method, whereas the latter returns the currently emitting `SignalInstance`.)
- There is basic threading support (calling all slots in another thread), using `emit(..., asynchronous=True)`.  This is experimental, and while thread-safety is the goal, it is not guaranteed.
- There are no `SignalFactory` classes here.

<em>The following two libraries implement django-inspired signals, they do not attempt to mimic the Qt API, but they have their own advantages.</em>

### [Blinker](https://github.com/jek/blinker)

Blinker provides a fast dispatching system that allows any number of interested parties to subscribe to events, or "signals".

### [SmokeSignal](https://github.com/shaunduncan/smokesignal/)

(This appears to be unmaintained)


