././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1686402236.6433682 obsub-0.2.1/0000755000175200001440000000000014441072275012053 5ustar00emiliausers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/CHANGELOG.rst0000644000175200001440000000247414441070374014101 0ustar00emiliausersChangelog --------- 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* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401503.0 obsub-0.2.1/LICENSE.rst0000644000175200001440000001624114441070737013674 0ustar00emiliausersThis 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/MANIFEST.in0000644000175200001440000000012514441070374013605 0ustar00emiliausersinclude README.rst CHANGELOG.rst include LICENSE.rst recursive-include test/py3 *.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1686402236.6433682 obsub-0.2.1/PKG-INFO0000644000175200001440000001274214441072275013156 0ustar00emiliausersMetadata-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* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686402185.0 obsub-0.2.1/README.rst0000644000175200001440000000712614441072211013536 0ustar00emiliausersobsub ===== |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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1686402236.6423683 obsub-0.2.1/obsub.egg-info/0000755000175200001440000000000014441072275014657 5ustar00emiliausers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686402236.0 obsub-0.2.1/obsub.egg-info/PKG-INFO0000644000175200001440000001274214441072274015761 0ustar00emiliausersMetadata-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* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686402236.0 obsub-0.2.1/obsub.egg-info/SOURCES.txt0000644000175200001440000000043114441072274016540 0ustar00emiliausersCHANGELOG.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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686402236.0 obsub-0.2.1/obsub.egg-info/dependency_links.txt0000644000175200001440000000000114441072274020724 0ustar00emiliausers ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686402236.0 obsub-0.2.1/obsub.egg-info/top_level.txt0000644000175200001440000000000614441072274017404 0ustar00emiliausersobsub ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/obsub.py0000644000175200001440000001565414441070374013550 0ustar00emiliausers''' 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1686402236.6433682 obsub-0.2.1/setup.cfg0000644000175200001440000000015614441072275013676 0ustar00emiliausers[nosetests] with-doctest = 1 doctest-extension = doctest exclude = py3 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686402227.0 obsub-0.2.1/setup.py0000755000175200001440000000216214441072263013566 0ustar00emiliausers#!/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', ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1686402236.6433682 obsub-0.2.1/test/0000755000175200001440000000000014441072275013032 5ustar00emiliausers././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1686402236.6433682 obsub-0.2.1/test/py3/0000755000175200001440000000000014441072275013545 5ustar00emiliausers././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/test/py3/test_signature.py0000644000175200001440000000144314441070374017157 0ustar00emiliausers''' 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/test/test_core.py0000644000175200001440000000772314441070374015402 0ustar00emiliausers""" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/test/test_signature.py0000644000175200001440000000032714441070374016444 0ustar00emiliausers''' 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686401276.0 obsub-0.2.1/test/test_weakref.py0000644000175200001440000000376514441070374016100 0ustar00emiliausers''' 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