{"id":32719110,"url":"https://github.com/dr0f0x/pynetevents","last_synced_at":"2025-11-02T18:03:22.490Z","repository":{"id":321864804,"uuid":"1079010961","full_name":"Dr0f0x/pynetevents","owner":"Dr0f0x","description":"A python implementation of c# style events","archived":false,"fork":false,"pushed_at":"2025-11-01T02:21:35.000Z","size":40,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-11-01T02:24:22.360Z","etag":null,"topics":["event-driven","event-driven-architecture","events","python","python-3","python3"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Dr0f0x.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-18T22:31:04.000Z","updated_at":"2025-11-01T01:38:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Dr0f0x/pynetevents","commit_stats":null,"previous_names":["dr0f0x/pynetevents"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Dr0f0x/pynetevents","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dr0f0x%2Fpynetevents","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dr0f0x%2Fpynetevents/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dr0f0x%2Fpynetevents/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dr0f0x%2Fpynetevents/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Dr0f0x","download_url":"https://codeload.github.com/Dr0f0x/pynetevents/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dr0f0x%2Fpynetevents/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":282332561,"owners_count":26652047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-02T02:00:06.609Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event-driven","event-driven-architecture","events","python","python-3","python3"],"created_at":"2025-11-02T18:02:37.584Z","updated_at":"2025-11-02T18:03:22.485Z","avatar_url":"https://github.com/Dr0f0x.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pynetevents\n\n[![CI](https://github.com/Dr0f0x/pynetevents/actions/workflows/python-ci.yml/badge.svg)](https://github.com/Dr0f0x/pynetevents/actions/workflows/python-ci.yml)\n[![Publish](https://github.com/Dr0f0x/pynetevents/actions/workflows/publish.yml/badge.svg)](https://github.com/Dr0f0x/pynetevents/actions/workflows/publish.yml)\n[![codecov](https://codecov.io/gh/Dr0f0x/pynetevents/graph/badge.svg?token=UU36EX1TP2)](https://codecov.io/gh/Dr0f0x/pynetevents)\n[![Codacy Badge](https://app.codacy.com/project/badge/Grade/8aeb45d8e38d4bdbbb1904d14ac104e3)](https://app.codacy.com/gh/Dr0f0x/pynetevents/dashboard?utm_source=gh\u0026utm_medium=referral\u0026utm_content=\u0026utm_campaign=Badge_grade)\n\nA lightweight, flexible implementation of a **C#-like event system** in Python\nthat provides simple, composable event slots — with support for both **strong**\n**and weak references**, **async listeners**, and **exception propagation**.\n\n## Features\n\n- Event slots — attach and fire multiple listeners easily\n- Descriptor-based events that restrict outside access to the event\n- Supports async and sync listeners\n- Possibility to use weak reference listeners to avoid memory leaks\n- Duplicate listener protection (if wanted)\n- Exception propagation for listener failures\n- Clean syntax: `+=` to subscribe, `-=` to unsubscribe\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [Event Slots](#eventslots)\n  - [EventSlot](#eventslot)\n  - [EventSlotWeakRef](#eventslotweakref)\n  - [Common Usage](#common-usage)\n- [Event Descriptor](#event-descriptor)\n- [Exceptions](#exceptions)\n\n\u003e For a full overview of all classes, methods, and options, check out the\n\u003e [API Reference](https://dr0f0x.github.io/pynetevents/).\n\n## Quick Start\n\n#### Installation\n\n```bash\npip install pynetevents\n```\n\n#### Usage\n\n```python\nimport asyncio\nfrom pynetevents import Event\n\nclass ChatServer:\n    on_message = Event()\n\n    def send_message(self, message):\n        self.on_message(message)\n\ndef log_message(msg):\n    print(f\"[LOG] Received: {msg}\")\n\nasync def save_message(msg):\n    await asyncio.sleep(0.1)\n    print(f\"[ASYNC] Saved message: {msg}\")\n\nserver = ChatServer()\nserver.on_message += log_message\nserver.on_message += save_message\n\nserver.send_message(\"Hello, world!\")\n```\n\n## EventSlots\n\nEventSlots are the centerpiece of this implementation. Essentially they are\nContainers that hold all registered listeners and allow to invoke each of them\ncentrally and easily add/remove said listeners.\n\nThere are two EventSlots provided by the package:\n\n### EventSlot\n\nThe **standard event slot** (`EventSlot`) keeps **strong references** to all\nlisteners.  \nThis means that as long as the slot exists, all listeners will remain in memory\n(and possibly the class they are a method of) — even if nothing else references\nthem.\n\nThis is ideal for most scenarios where listeners are meant to persist for the\nlifetime of an object or system (e.g., global events, core application signals).\n\n```python\nfrom pynetevents import EventSlot\n\n# Create a new event slot\nslot = EventSlot(\"on_data\")\n\ndef printer(data):\n    print(f\"Printer received: {data}\")\n\nasync def saver(data):\n    print(f\"[ASYNC] Saved data: {data}\")\n\n# Subscribe listeners\nslot += printer\nslot += saver\n\n# Fire synchronously — runs sync listeners immediately\nslot(\"Hello, strong world!\")\n\n# Fire asynchronously — awaits async listeners\nimport asyncio\nasyncio.run(slot.invoke_async(\"Async invocation example\"))\n\n# Remove a listener\nslot -= printer\n\nslot(\"Goodbye!\")  # Only saver runs (if invoked asynchronously)\n```\n\n### EventSlotWeakRef\n\nThe **weak reference event slot** (`EventSlotWeakRef`) is designed to **avoid\nmemory leaks** by holding listeners via **weak references** whenever possible.\nWhen a listener (typically a bound method) goes out of scope or its owning\nobject is deleted, it is **automatically removed** from the slot — no manual\nunsubscription required.\n\nThis makes it ideal for **instance-based event systems** where many temporary\nobjects may register callbacks.\n\n```python\nfrom pynetevents import EventSlotWeakRef\n\nclass Listener:\n    def __init__(self, name):\n        self.name = name\n\n    def on_event(self, msg):\n        print(f\"{self.name} received: {msg}\")\n\n# Create the weakref slot\nslot = EventSlotWeakRef(\"on_message\")\n\n# Create and register a listener\nlistener = Listener(\"Alpha\")\nslot += listener.on_event\n\n# Fire event\nslot(\"Hello!\")  # -\u003e Alpha received: Hello!\n\n# Delete the listener instance\ndel listener\n\n# The weak reference has been cleared automatically\nslot(\"World!\")  # -\u003e No output (listener no longer exists)\n```\n\n### Common Usage\n\nBoth of these classes have the inherited methods `subscribe`,`subscribe_weak`,\n`unsubscribe`, `unsubscribe_weak` that come from their common base class which\nin theory makes it possible to subscribe using a weakref to the normal\n`EventSlot`, although i would recommend aganst doing so, as it is bound to be\nvery confusing.\n\nInstead the easier way would be to use the overloaded `+=` and `-=` operators,\nlike the examples above do, that are overriden for these classes. For an\n`EventSlot` instance they use the `subscribe` and `unsubscribe` methods under\nthe hood and for the `EventSlotWeakRef` the according alternatives.\n\nInvoking all the listeners of a slot (= Invoking the event can be done) by using\nthe `Invoke` or `InvokeAsync` methods. The difference being that the first calls\nsynchronous listeners normally and only schedules async ones (fire and forget),\nwhile the async version actually awaits them.\n\n```python\nimport asyncio\nfrom pynetevents import EventSlot, EventSlotWeakRef\n\ndef sync_listener(msg):\n    print(f\"[SYNC] Received: {msg}\")\n\nasync def async_listener(msg):\n    await asyncio.sleep(0.1)\n    print(f\"[ASYNC] Processed: {msg}\")\n\nslot = EventSlot(\"on_event\")\n\nslot += sync_listener\nslot += async_listener\n\n# Fire synchronously (does NOT await async listeners)\nslot(\"Fire-and-forget example\")\n\n# Fire asynchronously (awaits async listeners properly)\nasyncio.run(slot.invoke_async(\"Awaited example\"))\n```\n\nAn `EventSlot` can be invoked with any kind of arguments that will be properly\nforwarded to all the listeners (which of course must be able to accept them). As\na short cut for calling the sync invoke method one can also call the EventSlot\nitself, which will do the exact same.\n\n```python\nfrom pynetevents import EventSlot\n\ndef on_data_received(data, status):\n    print(f\"Received '{data}' with status: {status}\")\n\n# Create an EventSlot\nslot = EventSlot(\"on_data\")\nslot += on_data_received\n\n# You can invoke with any arguments the listeners expect\nslot.invoke(\"Sample payload\", status=200)\n\n# Shortcut: calling the slot directly does the same as invoke()\nslot(\"Another payload\", status=404)\n```\n\n`EventSlots` can be configured in their behaviour by using the constructor\narguments. You can customize whether you want exceptions to be propagated or\nonly logged and wether you want to allow duplicate_listeners or throw an\nexception like the default.\n\n```python\nfrom pynetevents import EventSlot\n\n# Default behavior:\n# - Exceptions are caught and logged (not propagated)\n# - Duplicate listeners are not allowed\ndefault_slot = EventSlot(\"default_slot\")\n\n# Exceptions are propagated (raised to the caller)\npropagating_slot = EventSlot(\"propagating_slot\", propagate_exceptions=True)\n\n# Duplicate listeners are allowed\nduplicates_allowed_slot = EventSlot(\"duplicates_allowed_slot\", allow_duplicate_listeners=True)\n\n# Both customized: exceptions propagated AND duplicates allowed\ncustom_slot = EventSlot(\n    \"custom_slot\",\n    propagate_exceptions=True,\n    allow_duplicate_listeners=True\n)\n```\n\n## Event Descriptor\n\nIn addition to the `Slot` classes the package offers a custom descriptor for\ndeclaring `EventSlots` as class attributes, that restricts access to the\nattribute and allows for some other benefits. This decriptor is simply called\n`Event` as I would recommend it as the main way of declaring and using this\nevent implementation.\n\n```python\nfrom pynetevents import Event\n\n# Example class using Event descriptor\nclass ChatServer:\n    # Declare events as class attributes\n    on_message = Event()\n\n# Example listeners\ndef log_message(msg):\n    print(f\"[LOG] Message received: {msg}\")\n\nserver = ChatServer()\n\n# Subscribe listeners normally using '+='\n# the EventSlot instance is automatically created here and uses the name of the attribute `on_message`\nserver.on_message += log_message\n\n# Fire event\nserver.on_message(\"Hello World!\")\n\n# Would throw an error as it assigns a new instance\nserver.on_message = EventSlot()\n```\n\nThe `__get__` method will automatically create an `EventSlot` for each instance\nif one does not already exist. And if one does it checks that the configuration\nof the existing object and the descriptor fit. Internally created slots are\nstored inside the instance dict just like a normal attribute would\n\nThe `__set__` method only allows assigning the same `EventSlot` instance to\nitself (altough the instance itself can be changed), this prohibits assigning\nnew objects to the attribute and is intended to encourage using the `+=` and\n`-=` operators.\n\nThe `__set_name` method gets the name of the `Event` attribute and uses it for\nthe created `EventSlots`.\n\nThe `Event` provides the same configuration options as the `EventSlots`\n(propagate_excpetions, allow_duplicate_listeners) and additionally one to choose\nwether created instances should be `EventSlotWeakRef` or normal ones.\n\n```python\nfrom pynetevents import Event\n\nclass MyApp:\n    # Normal EventSlot, exceptions propagated\n    on_update = Event(propagate_exceptions=True)\n\n    # Weak reference EventSlot, duplicate listeners allowed\n    on_weak_update = Event(\n        use_weakref_slot=True,\n        allow_duplicate_listeners=True\n    )\n\n    # Normal EventSlot with all defaults\n    on_default = Event()\n```\n\nIf you prefer you can still create the `EventSlot` instance in the `__init__`\nmethod of the class and it will be used by the descriptor, however the\nconfigurations must match (or not be passed in the constructor of the slot\nobject). Basically, if the descriptor wants to use a **weak reference slot**\n(`EventSlotWeakRef`) but the found instance is a **normal `EventSlot`**, or if\nany configuration parameter like `propagate_exceptions` or\n`allow_duplicate_listeners` does not match, the descriptor will **raise an\nerror**.\n\n```python\nfrom pynetevents import Event, EventSlot, EventSlotWeakRef\n\nclass MyApp:\n    on_update = Event(use_weakref_slot=True, propagate_exceptions=True)\n    on_call = Event(propagate_exceptions=False)\n\n    def __init__(self):\n        # You can provide your own EventSlot instance\n        # Must match the descriptor configuration (type and params)\n        self.on_update = EventSlotWeakRef(\n            \"on_update\",\n            propagate_exceptions=True\n        )\n\n        # Or do not provide the concering arguments (= leave defaults)\n        self.on_call = EventSlot()\n```\n\n## Exceptions\n\n`pynetevents` provides two very verbose exception types to handle event-related\nerrors:\n\n- **`EventExecutionError`**  \n  Raised when a listener throws an exception during event invocation.  \n  This allows you to catch and inspect errors from individual listeners while\n  optionally continuing to run other listeners.\n\n- **`DuplicateEventListenerError`**  \n  Raised when attempting to add the same listener multiple times to an\n  `EventSlot` if `allow_duplicate_listeners` is set to `False`.  \n  This prevents accidental duplicate registrations and ensures predictable event\n  behavior.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdr0f0x%2Fpynetevents","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdr0f0x%2Fpynetevents","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdr0f0x%2Fpynetevents/lists"}