diff --git a/.flake8 b/.flake8 index 7c83cf4..c8d0877 100644 --- a/.flake8 +++ b/.flake8 @@ -1,11 +1,11 @@ # Run flake8 (pycodestyle + pyflakes) check. # https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes # Ignored errors: -# - E501: line too long # - E265: block comment should start with '# ' (makes it easier to enable/disable code) # - W503: line break before binary operator (deprecated rule) # - W505: doc line too long [flake8] -ignore = E501,E265,W503,W505 -exclude = .git/,.virtualenv/,__pycache__/,build/,dist/,docker/ +ignore = E265,W503,W505 +max-line-length = 120 +exclude = .git/,.virtualenv/,.venv/,.local/,.eggs/,__pycache__/,build/,dist/,submodules/ diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000..dbe038a --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,37 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + # It is not possible to use GitHub Python setup because a system dependency is required + + steps: + - name: Checkout project + uses: actions/checkout@v4 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y python3-gi python3-venv make + - name: Install venv + run: | + python3 -m venv /opt/venv --system-site-packages + /opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools wheel + /opt/venv/bin/pip install --no-cache-dir --editable '.[dev]' + - name: Test + run: | + make lint_local + make deadcode_local + make test_local + env: + PATH: /opt/venv/bin:/usr/sbin:/usr/bin:/sbin:/bin diff --git a/.gitignore b/.gitignore index b92d662..7f991d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ -build/ +__pycache__/ *.pyc + +# Unit test / coverage reports +.coverage* +coverage* +htmlcov +report.xml +.mypy_cache +.pytest_cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c700a46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:bookworm + +RUN apt update +RUN apt install -y python3-gi python3-venv make + +RUN python3 -m venv /opt/venv --system-site-packages +ENV VIRTUAL_ENV="/opt/venv" +ENV PATH="/opt/venv/bin:/usr/sbin:/usr/bin:/sbin:/bin" +RUN pip install --no-cache-dir --upgrade pip setuptools wheel + +ARG DOCKER_WORK_DIR +RUN mkdir -p ${DOCKER_WORK_DIR} +WORKDIR ${DOCKER_WORK_DIR} + +COPY pyproject.toml pyproject.toml +COPY easyevent easyevent +RUN pip install --no-cache-dir --editable '.[dev]' diff --git a/Makefile b/Makefile index 59924c7..099fa48 100755 --- a/Makefile +++ b/Makefile @@ -1,21 +1,60 @@ -#!/usr/bin/make -f +DOCKER_IMAGE_NAME ?= easyevent:latest +DOCKER_WORK_DIR ?= /opt/src +DOCKER_RUN ?= docker run --rm -it --user "$(shell id -u):$(shell id -g)" -v ${CURDIR}:${DOCKER_WORK_DIR} -all: build build: - python setup.py build + docker build -t ${DOCKER_IMAGE_NAME} ${BUILD_ARGS} --build-arg DOCKER_WORK_DIR=${DOCKER_WORK_DIR} . -install: build - sudo python setup.py install +rebuild:BUILD_ARGS = --no-cache +rebuild:build -uninstall: - sudo rm -vrf /usr/lib/python2.5/site-packages/easyevent - sudo rm -v /usr/lib/python2.5/site-packages/easyevent*.egg-info +push: + docker push ${DOCKER_IMAGE_NAME} -clean: - rm -rf build - rm -f build-stamp - find . -name "*.pyc" -o -name "*~" | xargs -r rm +pull: + docker pull --quiet ${DOCKER_IMAGE_NAME} + +shell: + ${DOCKER_RUN} ${DOCKER_IMAGE_NAME} /bin/bash + +lint: + ${DOCKER_RUN} ${DOCKER_IMAGE_NAME} make lint_local + +lint_local: + flake8 . + +typing: + ${DOCKER_RUN} ${DOCKER_IMAGE_NAME} make typing_local + +typing_local: + mypy easyevent + +deadcode: + ${DOCKER_RUN} ${DOCKER_IMAGE_NAME} make deadcode_local -builddeb: - dpkg-buildpackage -rfakeroot +deadcode_local: + vulture --min-confidence 90 easyevent tests + +test: + ${DOCKER_RUN} -e "PYTEST_ARGS=${PYTEST_ARGS}" ${DOCKER_IMAGE_NAME} make test_local + +test_local:PYTEST_ARGS := $(or ${PYTEST_ARGS},--cov --no-cov-on-fail --junitxml=report.xml --cov-report xml --cov-report term --cov-report html) +test_local: + pytest ${PYTEST_ARGS} + +list_installed_files: + ${DOCKER_RUN} ${DOCKER_IMAGE_NAME} make list_installed_files_local + +list_installed_files_local: + # List files installed by the Python package + make clean + python3 -m venv /tmp/venv --system-site-packages + cp -a ${DOCKER_WORK_DIR} /tmp/src + cd /tmp/src && /tmp/venv/bin/pip install . + cd /tmp/src && /tmp/venv/bin/pip show -f easyevent + +clean: + rm -rf .coverage .pytest_cache .local .eggs build dist *.egg-info + find . -type f -name *.pyc -delete + find . -type d -name __pycache__ -delete diff --git a/easyevent/__init__.py b/easyevent/__init__.py index 3803f8b..29d93e2 100644 --- a/easyevent/__init__.py +++ b/easyevent/__init__.py @@ -1,4 +1,3 @@ -from .version import VERSION as __version__ from .event import Listener, Launcher, User, forward_event -__all__ = ('__version__', 'Listener', 'Launcher', 'User', 'forward_event') +__all__ = ('Listener', 'Launcher', 'User', 'forward_event') diff --git a/easyevent/event.py b/easyevent/event.py index 4944d8c..efd9820 100644 --- a/easyevent/event.py +++ b/easyevent/event.py @@ -1,10 +1,6 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- """ Module to do event-driven programming very easily. @author: Damien Boucard -@license: Gnu/LGPLv2 -@version: 1.0 Module attributes: @@ -13,12 +9,20 @@ 'callback' (synchronous). """ from collections.abc import Callable -from gi.repository import GLib import logging logger = logging.getLogger('event') dispatcher = 'callback' log_ignores = ['level'] +GLib = None + + +def get_glib(): + # GLib is optional, so import it when needed + global GLib + if GLib is None: + from gi.repository import GLib + return GLib class Manager: @@ -27,7 +31,8 @@ class Manager: so it is not needed to use it directly but via Launch and Listener. @cvar instance: The instance created on importing the module. @type instance: C{L{Manager}} - @ivar listeners: Dictionnary with keys of type C{str} representing a event type and with values of type C{list} representing a collection of C{EventListener}. + @ivar listeners: Dictionnary with keys of type C{str} representing a event type and with values + of type C{list} representing a collection of C{EventListener}. @type listeners: C{dict>} """ def __init__(self): @@ -54,7 +59,11 @@ def add_listener(self, obj, event_type): duplicate_objects.append(listener) i += 1 if i > 0: - logger.warning('Warning, multiple class registration detected (%s times) for class %s for event %s, objects: old %s and new %s', i, class_name, event_type, duplicate_objects, obj) + logger.warning( + 'Warning, multiple class registration detected (%s times) ' + 'for class %s for event %s, objects: old %s and new %s', + i, class_name, event_type, duplicate_objects, obj + ) else: self.listeners[event_type] = [obj] @@ -88,7 +97,8 @@ def dispatch_event(self, event): function = getattr(obj, fctname) if isinstance(function, Callable): if dispatcher == 'gobject': - GLib.idle_add(function, event, priority=GLib.PRIORITY_HIGH) + gl = get_glib() + gl.idle_add(function, event, priority=gl.PRIORITY_HIGH) elif dispatcher == 'callback': function(event) continue @@ -99,7 +109,8 @@ def dispatch_event(self, event): function = getattr(obj, obj.event_default) if isinstance(function, Callable): if dispatcher == 'gobject': - GLib.idle_add(function, event, priority=GLib.PRIORITY_HIGH) + gl = get_glib() + gl.idle_add(function, event, priority=gl.PRIORITY_HIGH) elif dispatcher == 'callback': function(event) continue @@ -120,7 +131,8 @@ class Listener: It is just needed to herite from this class and register to events to listen easily events. It is also needed to write handler methods with event-specific and/or C{L{default}} function. - Event-specific functions have name as the concatenation of the C{prefix} parameter + the listened event type + the C{suffix} parameter. + Event-specific functions have name as the concatenation of: + the C{prefix} parameter + the listened event type + the C{suffix} parameter. If it does not exist, the default function is called as defined by the C{L{default}} parameter/attribute. @@ -131,7 +143,8 @@ class Listener: @type event_pattern: C{str} @ivar event_default: Default handler function name. @type event_default: C{str} - @ivar silent: Silent flag. If C{False}, C{L{UnhandledEventError}} is raised if an event cannot be handled. If C{True}, do nothing, listener does not handle the event. + @ivar silent: Silent flag. If C{False}, C{L{UnhandledEventError}} is raised if an event cannot be handled. + If C{True}, do nothing, listener does not handle the event. @type silent: C{str} """ def __init__(self, prefix='evt_', suffix='', default='eventPerformed', silent=False): @@ -268,7 +281,10 @@ def __str__(self): @return: Object converted string. @rtype: C{str} """ - return '<%s.%s type=%s source=%s content=%s>' % (__name__, self.__class__.__name__, self.type, self.source, self.content) + return ( + '<%s.%s type=%s source=%s content=%s>' + % (__name__, self.__class__.__name__, self.type, self.source, self.content) + ) class UnhandledEventError(AttributeError): diff --git a/easyevent/version.py b/easyevent/version.py deleted file mode 100644 index db7986c..0000000 --- a/easyevent/version.py +++ /dev/null @@ -1 +0,0 @@ -VERSION = '2.0' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..40dd704 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools>=77.0"] + +[project] +name = "easyevent" +description = "Very simple and easy-to-use python module for event-driven programming." +version = "3.0" +authors = [{name = "UbiCast"}] +license = "LGPL-3.0-only" +license-files = ["LICENSE"] +readme = "README.md" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Utilities", +] +requires-python = ">=3.11" + +[project.optional-dependencies] +dev = [ + "flake8", + "mypy", + "pytest", + "pytest-cov", + "vulture", +] + +[project.urls] +Repository = "https://github.com/UbiCastTeam/easyevent" + +[tool.setuptools] +packages = ["easyevent"] +include-package-data = false + +[tool.pytest.ini_options] +addopts = "--verbose --tb=native --color=yes" +log_date_format = "%H:%M:%S" +log_format = "%(asctime)s.%(msecs)03d %(name)s %(levelname)s %(message)s" +log_level = "DEBUG" +testpaths = "tests/" + +[tool.coverage.run] +source_dirs = ["easyevent"] diff --git a/setup.py b/setup.py deleted file mode 100755 index 5fe1ade..0000000 --- a/setup.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from setuptools import setup - -with open('easyevent/version.py') as fo: - content = fo.read().strip() - version = content.split('=')[-1].strip(' \'"') - -setup( - name='easyevent', - version=version, - description='Very simple and easy-to-use python module for event-driven programming.', - author='UbiCast', - author_email='support@ubicast.eu', - url='https://github.com/UbiCastTeam/easyevent', - license='Gnu/LGPLv2', - packages=['easyevent'], -) diff --git a/test/test.py b/test/test.py deleted file mode 100644 index 3f33f00..0000000 --- a/test/test.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2015, Florent Thiery -import easyevent -import sys - - -class Listener(easyevent.User): - def __init__(self): - easyevent.User.__init__(self) - self.register_event('speech') - - def evt_speech(self, event): - print('Got speech event, : %s' % event.content) - print('Test success, quitting') - self.unregister_event('speech') - #self.unregister_all_events() - sys.exit(0) - - -class Shouter(easyevent.User): - def __init__(self): - easyevent.User.__init__(self) - - def shout(self, text): - self.launch_event('speech', text) - - -if __name__ == '__main__': - lst = Listener() - sht = Shouter() - sht.shout('hello world') - # at that point, Listener prints 'Got speech event: hello world' - print('Event not received, test failed') - sys.exit(1) diff --git a/test/test_gobject.py b/test/test_gobject.py deleted file mode 100644 index 8bfb1eb..0000000 --- a/test/test_gobject.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2015, Florent Thiery -from gi.repository import GLib -import easyevent -import sys - -easyevent.event.dispatcher = 'gobject' - - -class Listener(easyevent.User): - - def __init__(self): - easyevent.User.__init__(self) - self.register_event('speech') - - def evt_speech(self, event): - print('Got speech event: %s' % event.content) - print('Test success, quitting') - self.unregister_event('speech') - sys.exit(0) - - -class Shouter(easyevent.User): - - def __init__(self): - easyevent.User.__init__(self) - - def shout(self, text): - self.launch_event('speech', text) - - -if __name__ == '__main__': - lst = Listener() - sht = Shouter() - GLib.idle_add(sht.shout, 'hello world') - - def failure(): - print('Test failed') - import sys - sys.exit(1) - GLib.timeout_add_seconds(1, failure) - - loop = GLib.MainLoop() - loop.run() diff --git a/tests/test_callback.py b/tests/test_callback.py new file mode 100644 index 0000000..7a9af3b --- /dev/null +++ b/tests/test_callback.py @@ -0,0 +1,28 @@ +import easyevent + + +class Listener(easyevent.User): + + def __init__(self): + super().__init__() + self.received_events = [] + self.register_event('speech') + + def evt_speech(self, event): + self.received_events.append(event) + self.unregister_event('speech') + + +class Shouter(easyevent.User): + + def shout(self, text): + self.launch_event('speech', text) + + +def test_basic(): + easyevent.event.dispatcher = 'callback' + lst = Listener() + sht = Shouter() + sht.shout('hello world') + assert len(lst.received_events) == 1 + assert lst.received_events[0].content == 'hello world' diff --git a/tests/test_gobject.py b/tests/test_gobject.py new file mode 100644 index 0000000..517420f --- /dev/null +++ b/tests/test_gobject.py @@ -0,0 +1,36 @@ +from gi.repository import GLib + +import easyevent + + +class Listener(easyevent.User): + + def __init__(self): + super().__init__() + self.received_events = [] + self.register_event('speech') + + def evt_speech(self, event): + self.received_events.append(event) + self.unregister_event('speech') + + +class Shouter(easyevent.User): + + def shout(self, text): + self.launch_event('speech', text) + + +def test_gobject(): + easyevent.event.dispatcher = 'gobject' + loop = GLib.MainLoop() + + lst = Listener() + sht = Shouter() + + GLib.timeout_add(500, sht.shout, 'hello world') + GLib.timeout_add(1000, loop.quit) + loop.run() + + assert len(lst.received_events) == 1 + assert lst.received_events[0].content == 'hello world'