././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1686402236.6433682
obsub-0.2.1/ 0000755 0001752 0000144 00000000000 14441072275 012053 5 ustar 00emilia users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/CHANGELOG.rst 0000644 0001752 0000144 00000002474 14441070374 014101 0 ustar 00emilia users Changelog
---------
v0.2
~~~~
From a user perspective the preservation of function signatures and a couple of
bug fixes are probably most relevant. Python 2.5 is no longer tested by
continuous integration, though we try to avoid unnecessary changes that might
break backwards compatibility.
In addition, there are quite a number of changes that mostly concern
developers.
- Function signatures are now preserved correctly by the event decorator. This
is true only for python3. On python2 there is no support for default
arguments, currently
- Some fixes to memory handling and tests thereof. This includes a more generic
handling of the garbage collection process within the test suite to make it
pass on pypy, too.
- Massive refactoring of test suite from one very long doctest to more focused
unit tests.
- The documentation has been converted from Markdown to reStructuredText, since
it is compatible with both PyPI and GitHub.
- Various improvements and some streamlining of the documentation.
- Fix package name in license.
- Continuous integration now includes coveralls.io support.
- Support for Python 2.5 is no longer tested using Travis CI, since they have
dropped support for this version.
v0.1.1
~~~~~~
- Add __all__ attribute to module
- Fix a couple of documentation issues
v0.1
~~~~
*Initial release*
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401503.0
obsub-0.2.1/LICENSE.rst 0000644 0001752 0000144 00000016241 14441070737 013674 0 ustar 00emilia users This Python module is licensed under a `CC0
license `__.
To the extent possible under law, Emilia Bopp has waived all copyright
and related or neighboring rights to obsub. This work is published from:
Germany.
Full license text
=================
CC0 1.0 Universal
-----------------
*CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND
DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT
OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.*
Statement of Purpose
~~~~~~~~~~~~~~~~~~~~
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work
of authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without
fear of later claims of infringement build upon, modify, incorporate in
other works, reuse and redistribute as freely as possible in any form
whatsoever and for any purposes, including without limitation commercial
purposes. These owners may contribute to the Commons to promote the
ideal of a free culture and the further production of creative, cultural
and scientific works, or to gain reputation or greater distribution for
their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or
she is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under
its terms, with knowledge of his or her Copyright and Related Rights in
the Work and the meaning and intended legal effect of CC0 on those
rights.
1. Copyright and Related Rights.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A Work made available under CC0 may be protected by copyright and
related or neighboring rights ("Copyright and Related Rights").
Copyright and Related Rights include, but are not limited to, the
following:
- the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
- moral rights retained by the original author(s) and/or performer(s);
- publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
- rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
- rights protecting the extraction, dissemination, use and reuse of
data in a Work;
- database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
- other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver.
~~~~~~~~~~
To the greatest extent permitted by, but not in contravention of,
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably
and unconditionally waives, abandons, and surrenders all of Affirmer's
Copyright and Related Rights and associated claims and causes of action,
whether now known or unknown (including existing as well as future
claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or
future medium and for any number of copies, and (iv) for any purpose
whatsoever, including without limitation commercial, advertising or
promotional purposes (the "Waiver"). Affirmer makes the Waiver for the
benefit of each member of the public at large and to the detriment of
Affirmer's heirs and successors, fully intending that such Waiver shall
not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of
the Work by the public as contemplated by Affirmer's express Statement
of Purpose.
3. Public License Fallback.
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Should any part of the Waiver for any reason be judged legally invalid
or ineffective under applicable law, then the Waiver shall be preserved
to the maximum extent permitted taking into account Affirmer's express
Statement of Purpose. In addition, to the extent the Waiver is so judged
Affirmer hereby grants to each affected person a royalty-free, non
transferable, non sublicensable, non exclusive, irrevocable and
unconditional license to exercise Affirmer's Copyright and Related
Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any
number of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law,
such partial invalidity or ineffectiveness shall not invalidate the
remainder of the License, and in such case Affirmer hereby affirms that
he or she will not (i) exercise any of his or her remaining Copyright
and Related Rights in the Work or (ii) assert any associated claims and
causes of action with respect to the Work, in either case contrary to
Affirmer's express Statement of Purpose.
4. Limitations and Disclaimers.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document. Affirmer
offers the Work as-is and makes no representations or warranties of any
kind concerning the Work, express, implied, statutory or otherwise,
including without limitation warranties of title, merchantability,
fitness for a particular purpose, non infringement, or the absence of
latent or other defects, accuracy, or the present or absence of errors,
whether or not discoverable, all to the greatest extent permissible
under applicable law. Affirmer disclaims responsibility for clearing
rights of other persons that may apply to the Work or any use thereof,
including without limitation any person's Copyright and Related Rights
in the Work. Further, Affirmer disclaims responsibility for obtaining
any necessary consents, permissions or other rights required for any use
of the Work. Affirmer understands and acknowledges that Creative Commons
is not a party to this document and has no duty or obligation with
respect to this CC0 or use of the Work.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/MANIFEST.in 0000644 0001752 0000144 00000000125 14441070374 013605 0 ustar 00emilia users include README.rst CHANGELOG.rst
include LICENSE.rst
recursive-include test/py3 *.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1686402236.6433682
obsub-0.2.1/PKG-INFO 0000644 0001752 0000144 00000012742 14441072275 013156 0 ustar 00emilia users Metadata-Version: 2.1
Name: obsub
Version: 0.2.1
Summary: Implementation of the observer pattern via a decorator
Home-page: https://github.com/milibopp/obsub
Author: Emilia Bopp
Author-email: contact@ebopp.de
License: Public Domain
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Software Development
Description-Content-Type: text/x-rst
License-File: LICENSE.rst
obsub
=====
|Version| |License|
Small python module that implements the observer pattern via a
decorator.
**Deprecation notice**
This module has been unmaintained since around 2014. The authors have
moved on to other alternatives to event handling. There is also
`observed `_ by @DanielSank, which
was partially inspired by *obsub*, however, has not seen many updates lately.
@milibopp has been writing with functional reactive programming (FRP), but
not for Python.
FRP is a higher-level abstraction than the observer pattern, that essentially
is a purely functional approach to unidirectional dataflow, composing your
programs of event stream transformations. Experience has shown, that is easier
to compose and to test than the raw observer pattern. A solid implementation in
Python is `RxPY `_, part of the ReactiveX
project.
Description
-----------
This is based on a `thread on stackoverflow
`_
(the example of C#-like events by Jason Orendorff), so I don't take any
credit for the idea. I merely made a fancy module with documentation and
tests out of it, since I needed it in a bigger project. It is quite
handy and I've been using it in a couple of projects, which require some
sort of event handling.
Thus it is `licensed as
CC0 `__, so basically
do-whatever-you-want to the extent legally possible.
Installation
------------
*obsub* is available on PyPI, so you can simply install it using
``pip install obsub`` or you do it manually using ``setup.py`` as with
any python package.
Usage
-----
The ``event`` decorator from the ``obsub`` module is used as follows:
.. code:: python
from obsub import event
# Define a class with an event
class Subject(object):
@event
def on_stuff(self, arg):
print('Stuff {} happens'.format(arg))
# Now define an event handler, the observer
def handler(subject, arg):
print('Stuff {} is handled'.format(arg))
# Wire everything up...
sub = Subject()
sub.on_stuff += handler
# And try it!
sub.on_stuff('foo')
You should now get both print messages from the event itself and the
event handler function, like so:
::
Stuff foo happens
Stuff foo is handled
Contribution and feedback
-------------------------
*obsub* is developed on `github `__.
If you have any questions about this software or encounter bugs, you're welcome
to open a `new issue on github `__.
In case you do not want to use github for some reason, you can alternatively
send an email one of us:
- `Emilia Bopp `__
- `André-Patrick Bubel `__
- `Thomas Gläßle `__
Feel free to contribute patches as pull requests as you see fit. Try to be
consistent with PEP 8 guidelines as far as possible and test everything.
Otherwise, your commit messages should start with a capitalized verb for
consistency. Unless your modification is completely trivial, also add a message
body to your commit.
Credits
-------
Thanks to Jason Orendorff on for the idea on stackoverflow. I also want
to thank @coldfix and @Moredread for contributions and feedback.
.. |Version| image:: https://img.shields.io/pypi/v/obsub.svg
:target: https://pypi.python.org/pypi/obsub/
:alt: Latest Version
.. |License| image:: https://img.shields.io/pypi/l/obsub.svg
:target: https://pypi.python.org/pypi/obsub/
:alt: License
Changelog
---------
v0.2
~~~~
From a user perspective the preservation of function signatures and a couple of
bug fixes are probably most relevant. Python 2.5 is no longer tested by
continuous integration, though we try to avoid unnecessary changes that might
break backwards compatibility.
In addition, there are quite a number of changes that mostly concern
developers.
- Function signatures are now preserved correctly by the event decorator. This
is true only for python3. On python2 there is no support for default
arguments, currently
- Some fixes to memory handling and tests thereof. This includes a more generic
handling of the garbage collection process within the test suite to make it
pass on pypy, too.
- Massive refactoring of test suite from one very long doctest to more focused
unit tests.
- The documentation has been converted from Markdown to reStructuredText, since
it is compatible with both PyPI and GitHub.
- Various improvements and some streamlining of the documentation.
- Fix package name in license.
- Continuous integration now includes coveralls.io support.
- Support for Python 2.5 is no longer tested using Travis CI, since they have
dropped support for this version.
v0.1.1
~~~~~~
- Add __all__ attribute to module
- Fix a couple of documentation issues
v0.1
~~~~
*Initial release*
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686402185.0
obsub-0.2.1/README.rst 0000644 0001752 0000144 00000007126 14441072211 013536 0 ustar 00emilia users obsub
=====
|Version| |License|
Small python module that implements the observer pattern via a
decorator.
**Deprecation notice**
This module has been unmaintained since around 2014. The authors have
moved on to other alternatives to event handling. There is also
`observed `_ by @DanielSank, which
was partially inspired by *obsub*, however, has not seen many updates lately.
@milibopp has been writing with functional reactive programming (FRP), but
not for Python.
FRP is a higher-level abstraction than the observer pattern, that essentially
is a purely functional approach to unidirectional dataflow, composing your
programs of event stream transformations. Experience has shown, that is easier
to compose and to test than the raw observer pattern. A solid implementation in
Python is `RxPY `_, part of the ReactiveX
project.
Description
-----------
This is based on a `thread on stackoverflow
`_
(the example of C#-like events by Jason Orendorff), so I don't take any
credit for the idea. I merely made a fancy module with documentation and
tests out of it, since I needed it in a bigger project. It is quite
handy and I've been using it in a couple of projects, which require some
sort of event handling.
Thus it is `licensed as
CC0 `__, so basically
do-whatever-you-want to the extent legally possible.
Installation
------------
*obsub* is available on PyPI, so you can simply install it using
``pip install obsub`` or you do it manually using ``setup.py`` as with
any python package.
Usage
-----
The ``event`` decorator from the ``obsub`` module is used as follows:
.. code:: python
from obsub import event
# Define a class with an event
class Subject(object):
@event
def on_stuff(self, arg):
print('Stuff {} happens'.format(arg))
# Now define an event handler, the observer
def handler(subject, arg):
print('Stuff {} is handled'.format(arg))
# Wire everything up...
sub = Subject()
sub.on_stuff += handler
# And try it!
sub.on_stuff('foo')
You should now get both print messages from the event itself and the
event handler function, like so:
::
Stuff foo happens
Stuff foo is handled
Contribution and feedback
-------------------------
*obsub* is developed on `github `__.
If you have any questions about this software or encounter bugs, you're welcome
to open a `new issue on github `__.
In case you do not want to use github for some reason, you can alternatively
send an email one of us:
- `Emilia Bopp `__
- `André-Patrick Bubel `__
- `Thomas Gläßle `__
Feel free to contribute patches as pull requests as you see fit. Try to be
consistent with PEP 8 guidelines as far as possible and test everything.
Otherwise, your commit messages should start with a capitalized verb for
consistency. Unless your modification is completely trivial, also add a message
body to your commit.
Credits
-------
Thanks to Jason Orendorff on for the idea on stackoverflow. I also want
to thank @coldfix and @Moredread for contributions and feedback.
.. |Version| image:: https://img.shields.io/pypi/v/obsub.svg
:target: https://pypi.python.org/pypi/obsub/
:alt: Latest Version
.. |License| image:: https://img.shields.io/pypi/l/obsub.svg
:target: https://pypi.python.org/pypi/obsub/
:alt: License
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1686402236.6423683
obsub-0.2.1/obsub.egg-info/ 0000755 0001752 0000144 00000000000 14441072275 014657 5 ustar 00emilia users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686402236.0
obsub-0.2.1/obsub.egg-info/PKG-INFO 0000644 0001752 0000144 00000012742 14441072274 015761 0 ustar 00emilia users Metadata-Version: 2.1
Name: obsub
Version: 0.2.1
Summary: Implementation of the observer pattern via a decorator
Home-page: https://github.com/milibopp/obsub
Author: Emilia Bopp
Author-email: contact@ebopp.de
License: Public Domain
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Software Development
Description-Content-Type: text/x-rst
License-File: LICENSE.rst
obsub
=====
|Version| |License|
Small python module that implements the observer pattern via a
decorator.
**Deprecation notice**
This module has been unmaintained since around 2014. The authors have
moved on to other alternatives to event handling. There is also
`observed `_ by @DanielSank, which
was partially inspired by *obsub*, however, has not seen many updates lately.
@milibopp has been writing with functional reactive programming (FRP), but
not for Python.
FRP is a higher-level abstraction than the observer pattern, that essentially
is a purely functional approach to unidirectional dataflow, composing your
programs of event stream transformations. Experience has shown, that is easier
to compose and to test than the raw observer pattern. A solid implementation in
Python is `RxPY `_, part of the ReactiveX
project.
Description
-----------
This is based on a `thread on stackoverflow
`_
(the example of C#-like events by Jason Orendorff), so I don't take any
credit for the idea. I merely made a fancy module with documentation and
tests out of it, since I needed it in a bigger project. It is quite
handy and I've been using it in a couple of projects, which require some
sort of event handling.
Thus it is `licensed as
CC0 `__, so basically
do-whatever-you-want to the extent legally possible.
Installation
------------
*obsub* is available on PyPI, so you can simply install it using
``pip install obsub`` or you do it manually using ``setup.py`` as with
any python package.
Usage
-----
The ``event`` decorator from the ``obsub`` module is used as follows:
.. code:: python
from obsub import event
# Define a class with an event
class Subject(object):
@event
def on_stuff(self, arg):
print('Stuff {} happens'.format(arg))
# Now define an event handler, the observer
def handler(subject, arg):
print('Stuff {} is handled'.format(arg))
# Wire everything up...
sub = Subject()
sub.on_stuff += handler
# And try it!
sub.on_stuff('foo')
You should now get both print messages from the event itself and the
event handler function, like so:
::
Stuff foo happens
Stuff foo is handled
Contribution and feedback
-------------------------
*obsub* is developed on `github `__.
If you have any questions about this software or encounter bugs, you're welcome
to open a `new issue on github `__.
In case you do not want to use github for some reason, you can alternatively
send an email one of us:
- `Emilia Bopp `__
- `André-Patrick Bubel `__
- `Thomas Gläßle `__
Feel free to contribute patches as pull requests as you see fit. Try to be
consistent with PEP 8 guidelines as far as possible and test everything.
Otherwise, your commit messages should start with a capitalized verb for
consistency. Unless your modification is completely trivial, also add a message
body to your commit.
Credits
-------
Thanks to Jason Orendorff on for the idea on stackoverflow. I also want
to thank @coldfix and @Moredread for contributions and feedback.
.. |Version| image:: https://img.shields.io/pypi/v/obsub.svg
:target: https://pypi.python.org/pypi/obsub/
:alt: Latest Version
.. |License| image:: https://img.shields.io/pypi/l/obsub.svg
:target: https://pypi.python.org/pypi/obsub/
:alt: License
Changelog
---------
v0.2
~~~~
From a user perspective the preservation of function signatures and a couple of
bug fixes are probably most relevant. Python 2.5 is no longer tested by
continuous integration, though we try to avoid unnecessary changes that might
break backwards compatibility.
In addition, there are quite a number of changes that mostly concern
developers.
- Function signatures are now preserved correctly by the event decorator. This
is true only for python3. On python2 there is no support for default
arguments, currently
- Some fixes to memory handling and tests thereof. This includes a more generic
handling of the garbage collection process within the test suite to make it
pass on pypy, too.
- Massive refactoring of test suite from one very long doctest to more focused
unit tests.
- The documentation has been converted from Markdown to reStructuredText, since
it is compatible with both PyPI and GitHub.
- Various improvements and some streamlining of the documentation.
- Fix package name in license.
- Continuous integration now includes coveralls.io support.
- Support for Python 2.5 is no longer tested using Travis CI, since they have
dropped support for this version.
v0.1.1
~~~~~~
- Add __all__ attribute to module
- Fix a couple of documentation issues
v0.1
~~~~
*Initial release*
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686402236.0
obsub-0.2.1/obsub.egg-info/SOURCES.txt 0000644 0001752 0000144 00000000431 14441072274 016540 0 ustar 00emilia users CHANGELOG.rst
LICENSE.rst
MANIFEST.in
README.rst
obsub.py
setup.cfg
setup.py
obsub.egg-info/PKG-INFO
obsub.egg-info/SOURCES.txt
obsub.egg-info/dependency_links.txt
obsub.egg-info/top_level.txt
test/test_core.py
test/test_signature.py
test/test_weakref.py
test/py3/test_signature.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686402236.0
obsub-0.2.1/obsub.egg-info/dependency_links.txt 0000644 0001752 0000144 00000000001 14441072274 020724 0 ustar 00emilia users
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686402236.0
obsub-0.2.1/obsub.egg-info/top_level.txt 0000644 0001752 0000144 00000000006 14441072274 017404 0 ustar 00emilia users obsub
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/obsub.py 0000644 0001752 0000144 00000015654 14441070374 013550 0 ustar 00emilia users '''
This is an implementation of the observer pattern. It uses function
decorators to achieve the desired event registration mechanism.
For further reference, see
http://en.wikipedia.org/wiki/Observer_pattern
The idea is based on this thread:
http://stackoverflow.com/questions/1904351/python-observer-pattern-examples-tips
'''
import functools
import inspect
try:
# use python3 signatures if available
# this takes care of enforcing the correct signature at call time and
# provides the correct default arguments
from inspect import signature
except ImportError: # pragma: no cover
# python2 has no support for signatures
def signature(fn):
return None
__all__ = ['event']
__version__ = '0.2'
class event(object):
'''
This class serves as a utility to decorate a function as an event.
The following example demonstrates its functionality in an abstract way.
A class method can be decorated as follows:
>>> class A(object):
... def __init__(self, name):
... self.name = name
...
... @event
... def progress(self, first, second):
... print("Doing something...")
A.progress is the event. It is triggered after executing the code in the
decorated progress routine.
Now that we have a class with some event, let's create an event handler.
>>> def handler(self, first, second):
... print("%s %s and %s!" % (first, self.name, second))
Note that the handler (and signal calls) must have the signature defined
by the decorated event method.
This handler only greets the object that triggered the event by using its
name attribute. Let's create some instances of A and register our new
event handler to their progress event.
>>> a = A("Foo")
>>> b = A("Bar")
>>> a.progress += handler
>>> b.progress += handler
Now everything has been setup. When we call the method, the event will be
triggered:
>>> a.progress("Hello", "World")
Doing something...
Hello Foo and World!
>>> b.progress(second="Others", first="Hi")
Doing something...
Hi Bar and Others!
What happens if we disobey the call signature?
>>> c = A("World")
>>> c.progress(second="World") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
TypeError: progress() missing 1 required positional argument: 'first'
Class based access is possible as well:
>>> A.progress(a, "Hello", "Y")
Doing something...
Hello Foo and Y!
Bound methods keep the instance alive:
>>> f = a.progress
>>> import weakref, gc
>>> wr = weakref.ref(a)
>>> del a
>>> c=gc.collect()
>>> assert wr() is not None
>>> f("Hi", "Z")
Doing something...
Hi Foo and Z!
If we delete the hard reference to the bound method and run the garbage
collector (to make sure it is run at all), the object will be gone:
>>> del f
>>> c=gc.collect()
>>> assert wr() is None
'''
def __init__(self, function):
'''
Constructor.
* function -- The function to be wrapped by the decorator.
'''
# Copy docstring and other attributes from function
functools.update_wrapper(self, function)
self.__signature__ = signature(function)
# Used to enforce call signature even when no slot is connected.
# Can also execute code (called before handlers)
self.__function = function
def __set__(self, instance, value):
'''
This is a NOP preventing that a boundevent instance is stored.
This prevents operations like `a.progress += handler` to have
side effects that result in a cyclic reference.
http://stackoverflow.com/questions/18287336/memory-leak-when-invoking-iadd-via-get-without-using-temporary
'''
pass
def __get__(self, instance, owner):
'''
Overloaded __get__ method. Defines the object resulting from
a method/function decorated with @event.
See http://docs.python.org/reference/datamodel.html?highlight=__get__#object.__get__
for a detailed explanation of what this special method usually does.
* instance -- The instance of event invoked.
* owner -- The owner class.
'''
# this case corresponds to access via the owner class:
if instance is None:
@functools.wraps(self.__function)
def wrapper(instance, *args, **kwargs):
return self.__get__(instance, owner)(*args, **kwargs)
else:
wrapper = functools.wraps(self.__function)(boundevent(instance, self.__function))
wrapper.__signature__ = self.__signature__
return wrapper
class boundevent(object):
'''Private helper class for event system.'''
def __init__(self, instance, function):
'''
Constructor.
* instance -- the instance whose member the event is
'''
self.instance = instance
self.__function = function
self.__key = ' ' + function.__name__
@property
def __event_handlers(self):
if self.__key not in self.instance.__dict__:
self.instance.__dict__[self.__key] = []
return self.instance.__dict__[self.__key]
def __iadd__(self, function):
'''
Overloaded += operator. It registers event handlers to the event.
* function -- The right-hand-side argument of the operator; this is the
event handling function that registers to the event.
'''
# Add the function as a new event handler
self.__event_handlers.append(function)
# Return the boundevent instance itself for coherent syntax behaviour
return self
def __isub__(self, function):
'''
Overloaded -= operator. It removes registered event handlers from
the event.
* function -- The right-hand-side argument of the operator; this is the
function that needs to be removed from the list of event handlers.
'''
# Remove the function from the list of registered event handlers
self.__event_handlers.remove(function)
# Return the boundevent instance itself for coherent syntax behaviour
return self
def __call__(self, *args, **kwargs):
'''
Overloaded call method; it defines the behaviour of boundevent().
When the event is called, all registered event handlers are called.
* *args -- Arguments given to the event handlers.
* **kwargs -- Keyword arguments given to the event handlers.
'''
# Enforce signature and possibly execute entry code. This makes sure
# any inconsistent call will be caught immediately, independent of
# connected handlers.
result = self.__function(self.instance, *args, **kwargs)
# Call all registered event handlers
for f in self.__event_handlers[:]:
f(self.instance, *args, **kwargs)
return result
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1686402236.6433682
obsub-0.2.1/setup.cfg 0000644 0001752 0000144 00000000156 14441072275 013676 0 ustar 00emilia users [nosetests]
with-doctest = 1
doctest-extension = doctest
exclude = py3
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686402227.0
obsub-0.2.1/setup.py 0000755 0001752 0000144 00000002162 14441072263 013566 0 ustar 00emilia users #!/usr/bin/env python
import logging
from setuptools import setup
# Create a front page for PyPI:
long_description = None
try:
long_description = open('README.rst').read()
long_description += '\n' + open('CHANGELOG.rst').read()
except IOError:
# some file is not available
# just use what we got so far but issue a warning
logging.warn('documentation files missing')
setup(
name='obsub',
version='0.2.1',
description='Implementation of the observer pattern via a decorator',
long_description=long_description,
long_description_content_type='text/x-rst',
author='Emilia Bopp',
author_email='contact@ebopp.de',
url='https://github.com/milibopp/obsub',
py_modules=['obsub',],
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development',
],
license='Public Domain',
tests_require='nose',
test_suite='nose.collector',
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1686402236.6433682
obsub-0.2.1/test/ 0000755 0001752 0000144 00000000000 14441072275 013032 5 ustar 00emilia users ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1686402236.6433682
obsub-0.2.1/test/py3/ 0000755 0001752 0000144 00000000000 14441072275 013545 5 ustar 00emilia users ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/test/py3/test_signature.py 0000644 0001752 0000144 00000001443 14441070374 017157 0 ustar 00emilia users '''
Test that the decorator correctly preserves the signature. Currently, this
is only possible for python3.
'''
from obsub import event
try:
from inspect import signature
except ImportError:
raise NotImplementedError
referenced = {}
def on_blubb(
self:0,
obscure:"NOTE: default argument is a by reference: "=referenced,
*,
with_kwonlyarg:int,
**kwargs):
return obscure
class A(object):
on_blubb = event(on_blubb)
def test_signature():
# Define a test class and an event handler
a = A()
# signature is preserved: (!!)
assert signature(a.on_blubb) == signature(on_blubb)
# NOTE: we even got the exact object as default parameter, not only an
# exact copy:
assert a.on_blubb(with_kwonlyarg="xyz") is referenced
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/test/test_core.py 0000644 0001752 0000144 00000007723 14441070374 015402 0 ustar 00emilia users """
Test the core functionality.
Contains a converted version of all of the doctests.
"""
# test utilities
import unittest
import weakref, gc
# tested module
from obsub import event
class NewStyle(object):
def __init__(self):
self.count = 0
@event
def emit(self, first, second):
self.count += 1
class OldStyle:
def __init__(self):
self.count = 0
@event
def emit(self, first, second):
self.count += 1
class Observer(object):
def __init__(self, call_stack):
self.call_stack = call_stack
def __call__(self, source, first, second):
diff = (sum(1 for call in self.call_stack if call[2] == source)
- source.count) + 1
self.call_stack.append((self, diff, source, first, second))
class TestCore(unittest.TestCase):
"""Test the obsub module for new style classes."""
cls = NewStyle
def setUp(self):
self.call_stack = []
self.maxDiff = None
def observer(self, instance):
observer = Observer(self.call_stack)
instance.emit += observer
return observer
def check_stack(self, expected):
self.assertEqual(expected, self.call_stack)
def test_single_handler(self):
"""A single handler is invoked correctly."""
src = self.cls()
obs = self.observer(src)
src.emit("Hello", "World")
self.check_stack([(obs, 0, src, "Hello", "World")])
def test_remove_handler(self):
"""Removal of event handlers works correctly."""
src = self.cls()
obs = self.observer(src)
src.emit -= obs
src.emit("something", "arbitrary")
self.check_stack([])
def test_multiple_handlers(self):
"""Multiple handlers are invoked in correct order."""
src = self.cls()
obs = [self.observer(src) for i in range(10)]
src.emit("Hello", "World")
self.check_stack([(obs[i], i, src, "Hello", "World")
for i in range(10)])
def test_multiple_sources(self):
"""Multiple instances of the event source class can coexist."""
src = [self.cls() for i in range(10)]
obs = [self.observer(s) for s in src]
for s in src:
s.emit("Hello", "World")
self.check_stack([(obs[i], 0, src[i], "Hello", "World")
for i in range(10)])
def test_keyword_arguments(self):
"""Keyword arguments can be used."""
src = self.cls()
obs = self.observer(src)
src.emit(second="World", first="Hello")
self.check_stack([(obs, 0, src, "Hello", "World")])
def test_wrong_signature(self):
"""Any incorrect call signature raises a TypeError."""
src = self.cls()
self.assertRaises(TypeError, src.emit, "Hello")
self.assertRaises(TypeError, src.emit, first="Hello", third="!")
self.assertRaises(TypeError, src.emit, second="World")
obs = self.observer(src)
self.assertRaises(TypeError, src.emit, forth=":)")
self.check_stack([])
def test_class_based_access(self):
"""The emitter function can be invoked via its class."""
src = self.cls()
obs = self.observer(src)
self.cls.emit(src, "Hello", "World")
self.check_stack([(obs, 0, src, "Hello", "World")])
def test_keep_alive(self):
"""A reference to a bound event method keeps its owner instance alive."""
src = self.cls()
obs = self.observer(src)
emit = src.emit
# remove original reference:
wr = weakref.ref(src)
del src
gc.collect()
# the emitter function is still functioning:
assert wr() is not None
emit("Hello", "World")
self.check_stack([(obs, 0, wr(), "Hello", "World")])
# removing the bound method makes the instance disappear:
del emit
self.call_stack[0] = None
gc.collect()
assert wr() is None
# ERRORS:
class TestCoreOldStyle(TestCore):
cls = OldStyle
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/test/test_signature.py 0000644 0001752 0000144 00000000327 14441070374 016444 0 ustar 00emilia users '''
Test that the decorator correctly preserves the signature. Currently, this
is only possible for python3.
'''
import sys
try:
from test.py3.test_signature import *
except ImportError:
__test__ = False
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1686401276.0
obsub-0.2.1/test/test_weakref.py 0000644 0001752 0000144 00000003765 14441070374 016100 0 ustar 00emilia users '''
These tests that the event handling mechanism does not produce memory leaks.
This could in principle happen, since it introduces a cyclic dependency that
might prevent garbage collection.
'''
import weakref
import gc
from obsub import event
def test_memory_leak():
# Define a test class and an event handler
class A(object):
@event
def on_blubb(self):
pass
def handler(self):
pass
# Instantiate and attach event handler
a = A()
a.on_blubb += handler
# Weak reference for testing
wr = weakref.ref(a)
# At first, weak reference exists
assert wr() is not None
# (implicitly) delete the A-instance by reassigning the only hard-ref.
# This is equivalent to `del a` but serves the purpose to demonstrate
# that there are very subtle ways to delete an instance:
a = None
# Trigger the garbage collection manually
gc.collect()
# after deletion it should be dead
assert wr() is None
def test_object_stays_alive_during_handler_execution():
# Define a test class and event handlers
class A(object):
@event
def on_blubb(self):
pass
deleted = False
class B(object):
def __init__(self, a):
# capture the only hard-ref on the A-instance:
self.a = a
self.a.on_blubb += self.handler
def handler(self, a):
# delete the A-instance
del self.a
a.deleted = True
del a
gc.collect()
def handler(a):
# We want a valid reference to the handled object, ...
assert a is not None
# ..., even if the deletion handler has already been called:
assert a.deleted
# Instantiate and attach event handler
b = B(A())
b.a.on_blubb += handler
wr = weakref.ref(b.a)
b.a.on_blubb()
# Trigger the garbage collection manually
gc.collect()
# make sure, b.a has been deleted after event handling
assert wr() is None