Parameter an einer Klasse übergeben

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
erselbst
User
Beiträge: 7
Registriert: Mittwoch 20. März 2013, 08:05

Hallo,
ich habe volgendes Problem. Ich möchte in der Klasse SyncEventHandler zusätzliche Parameter (Conf Objekt, LOG Objekt) übergeben, was aber nicht funktioniert. Wenn ein Event eintritt, soll eine Funktion ausgeführt werden, welche ein Shell Command ausführt. Dieser Funktion soll das Conf,LOG Objekt übergeben werden. Zuvor muss ja aber die Klasse die Objekte kennen. Wie kann man das machen?

Code: Alles auswählen

#!/usr/bin/env python

import os, sys, re, pyinotify
from synclogger import SyncLogger
from syncexception import SyncException
from configreader import ConfigReader
from syncthread import SyncThread

LOG = None
MASK = pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO

class SyncEventHandler(pyinotify.ProcessEvent):

    # Exclude filter
    Excludes = r'.+(\.swx|\.swp|~)$'

    def process_IN_MOVED_FROM(self, event):
        if not re.compile(SyncEventHandler.Excludes).match(event.pathname):
            # Run the command syncToStorage on this event
            syncToStorage(Conf, LOG, event.pathname)

    # Here are the other events defined


def main(Conf):

    # Create the Logging object if logging enabled
    if Conf.getGlobals('logging') == "1":
        LOG = SyncLogger(Conf)

    # Watch Manager
    wm = pyinotify.WatchManager()

    # Notifier object
    notifier = pyinotify.Notifier(wm, SyncEventHandler())

    # Loop list of all monitored directorys
    for m in Conf.getDirectorys('directorys'):

        # Get the full path to the current directory
        watchpath = ''.join([Conf.getGlobals('base')+os.sep, m])

        # Appent the currend path to the WatchManager
        wm.add_watch(watchpath, MASK, rec=True)

    notifier.loop()

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.stderr.write("An Error occured: You have to define the config file as argument.\n")
        sys.exit(1)
    else:
        try:
            Conf = ConfigReader(sys.argv[1])
            main(Conf)
        except Exception, e:
            raise SyncException("An Exception occured: %s" % (e))
Zuletzt geändert von Anonymous am Mittwoch 20. März 2013, 08:36, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
xeike
User
Beiträge: 83
Registriert: Donnerstag 28. Februar 2013, 09:58

Wie wäre es mit einem Konstruktor?

Code: Alles auswählen

class Foo(...)
  def __init__(self, ConfObj, LogObj):
    # tu was damit

foo = Foo(myconf, mylog)

Xe
BlackJack

@erselbst: Bei `watchpath` willst Du `os.path.join()` verwenden.

`LOG` auf Modulebene wird nicht wirklich verwendet. Auf Modulebene sollten auch nur Konstanten definiert werden, was durch die Namenskonvention komplett Grossbuchstaben zu verwenden, ja auch angedeutet wird. Aber wenn etwas Konstant sein soll, darf man es später natürlich nicht an einen anderen Namen binden. Und `LOG` soll ja nicht für `None` stehen.

Kommentare sollten dem Leser einen Mehrwert über den Quelltext bieten. Im Kommentar noch einmal das Offensichtliche hinzuschreiben, was gleich danach noch einmal genau so deutlich als Code dort steht, macht keinen Sinn, sondern nur unnötig Arbeit. Wenn ein Kommentar nötig ist um einen abgekürzten oder zu allgemeinen Namen zu erklären, dann sollte man den Namen ändern. Also statt an *einer* Stelle im Quelltext zu kommentieren, dass `wm` für „Watch Manager” steht, sollte man das Objekt gleich an den Namen `watch_manager` binden — und weiss dann auch ohne Kommentar *überall* wo der Name verwendet wird, um was es sich handelt.

Im ``if __name__ == '__main__':`` steht zu viel Code. Da sollten keine Namen definiert werden, die auf Modulebene sichtbar sind und auch keine Kommandozeilenargumente ausgewertet werden. Es ist ja gerade der Sinn von dem Idiom, dass man das Modul ohne Seiteneffekte importieren kann.

Falls die ``from``-Importe selbst geschriebene Module betreffen: Python ist nicht Java, es macht keinen Sinn das Modulkonzept auszuhebeln in dem man in jedes Modul nur eine Klasse steckt. Die Importe sehen verdächtig danach aus.

Statt eine Zeichenkette mit einem regulärem Ausdruck an die Klasse zu binden, könnte man den an der Stelle schon kompilieren und die `match()`-Methode an die Klasse binden. Zugriff über `self` statt über die Klasse, das ist flexibler.

Ungetestet:

Code: Alles auswählen

import os
import re
import sys
import pyinotify
from synclogger import SyncLogger
from syncexception import SyncException
from configreader import ConfigReader

MASK = pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO


class SyncEventHandler(pyinotify.ProcessEvent):
    # Exclude filter
    match_exclude = re.compile(r'.+(\.sw[xp]|~)$').match

    def __init__(self, config, logger, pevent=None, **kwargs):
        pyinotify.ProcessEvent.__init__(self, pevent, **kwargs)
        self.config = config
        self.logger = logger

    def process_IN_MOVED_FROM(self, event):
        if not self.match_exclude(event.pathname):
            syncToStorage(self.config, self.logger, event.pathname)

    # Here are the other events defined


def main():
    if len(sys.argv) < 2:
        sys.stderr.write(
            'An Error occured:'
            ' You have to define the config file as argument.\n'
        )
        sys.exit(1)

    try:
        config = ConfigReader(sys.argv[1])
        logger = (
            SyncLogger(config) if config.getGlobals('logging') == '1' else None
        )
        watch_manager = pyinotify.WatchManager()
        notifier = pyinotify.Notifier(
            watch_manager, SyncEventHandler(config, logger)
        )
        for directory in config.getDirectorys('directorys'):
            watch_manager.add_watch(
                os.path.join(config.getGlobals('base'), directory),
                MASK,
                rec=True
            )
        notifier.loop()
    except Exception as error:
        raise SyncException('An Exception occured: {0}'.format(error))


if __name__ == '__main__':
    main()
erselbst
User
Beiträge: 7
Registriert: Mittwoch 20. März 2013, 08:05

Nabend,

Erst einmal vielen Dank für die schnellen Antworten. Besonders an BlackJack für die detaillierte Erklärungen. Ich habe vor vier Jahren mal auf unserer Arbeit einen kleinen Kurs in Python gehabt, aber seit dem auch keine Zeile Code geschrieben. Mit meinem kleine Projekt bin ich nun fast fertig. Ich möchte das ganze noch mit 'appindicator' als System-tray Programm laufen lassen. Aber das wäre der zweite Schritt. Ich möchte gern meine Arbeit nochmal posten, damit ihr vielleicht mal drüber schaut, und wieder den einen oder anderen Kommentar zu den Code abgeben könnt. Ich kann daraus nur lernen ;)

Zu einer Struktur eines Python Projekt habe ich folgende Seite gefunden http://docs.python.org/2/tutorial/modules.html#packages Ist es nicht in meinen Fall nicht zu komplex? Oder sollte man sich an so eine Projekt Struktur in jedem Fall halten, egal wie umfangreich es ist?

Code: Alles auswählen

# File: sync.cfg
# -*- coding: utf-8 -*-

[GLOBALS]
logging = 1
logfile = /home/tux/bin/sync.log
base = /home/tux
sync_binary = /usr/bin/rsync
sync_options = -vrptgoDL --delete
sync_logfile = /home/tux/bin/rsync.log
sync_sshkeys = /home/tux/.ssh/sync-key
sync_sshport = 22
sync_targets = root@earth:/mnt/array1/synctest/

notify_binary = /usr/bin/notify-send
notify_delay = 10
notify_subject = Storage Syncronisation
notify_icon_success = /home/tux/.icons/sync-success.png
notify_icon_failure = /home/tux/.icons/sync-failed.png
notify_sound_success = /home/tux/.sounds/ContactOnline.wav
notify_sound_failure = /home/tux/.sounds/ContactOffline.wav

mplayer_binary = /usr/bin/mplayer

[MESSAGES]
msg_success_sync = Datei-Abgleich wurde abgeschlossen.
msg_failure_sync = Datei-Abgleich wurde abgebrochen.
msg_upload = %s wurde in ihrem persönlichen Onlinespeicher hochgeladen.
msg_delete = %s wurde aus ihrem persönlichen Onlinespeicher entfernt.

[DIRECTORYS]
directorys = bin,tmp

# EOF

Code: Alles auswählen

#!/usr/bin/env python
# File: sync.py
# -*- coding: utf-8 -*-

import os
import re
import sys
import pyinotify
from synclogger import SyncLogger
from syncexception import SyncException
from syncconfig import ConfigReader
from syncexec import CommandExec

MASK = pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO

class SyncEventHandler(pyinotify.ProcessEvent):

    # Exclude filter
    match_exclude = re.compile(r'.+(\.sw[xp]|~)$').match

    def __init__(self, config, logger, pevent=None, **kwargs):
        pyinotify.ProcessEvent.__init__(self, pevent, **kwargs)
        self.config = config
        self.logger = logger

    def process_IN_MOVED_TO(self, event):
        self.proc(event)

    def process_IN_CLOSE_WRITE(self, event):
        self.proc(event)

    def process_IN_DELETE(self, event):
        self.proc(event)

    def proc(self, event):
        if not self.match_exclude(event.pathname):
            if self.logger is not None:
                self.logger.info("The event %s has been applied to %s." %(event.maskname, event.pathname))
            if self.run(event) == 0:
                self.notify('success',event)
            else:
                self.notify('failure',event)

    def notify(self, state, event):

        notify_time = "%.0f" %(int(self.config.getGlobals('notify_delay'))*1000)

        if state == 'success':

            if os.path.isfile(self.config.getGlobals('notify_icon_success')):
                notify_icon = r'-i '+self.config.getGlobals('notify_icon_success')
            else:
                notify_icon = ""

            notify_text = self.config.getMessage('msg_success_sync')+" "
            if event.maskname == 'IN_DELETE':
                notify_text += self.config.getMessage('msg_delete')
            else:
                notify_text += self.config.getMessage('msg_upload')

            cmd1 = r'%s -t %s %s "%s" "%s"' %(
                    self.config.getGlobals('notify_binary'),
                    notify_time,
                    notify_icon,
                    self.config.getGlobals('notify_subject'),
                    notify_text %(event.name)
                )
            CommandExec(cmd1, self.config, self.logger).run()

            if os.path.isfile(self.config.getGlobals('notify_sound_success')) and os.path.isfile(self.config.getGlobals('mplayer_binary')):
                cmd2 = r'%s %s' %(
                        self.config.getGlobals('mplayer_binary'),
                        self.config.getGlobals('notify_sound_success')
                    )
                CommandExec(cmd2, self.config, self.logger).run()

        else:

            if os.path.isfile(self.config.getGlobals('notify_icon_failure')):
                notify_icon = r'-i '+self.config.getGlobals('notify_icon_failure')
            else:
                notify_icon = ""

            notify_text = self.config.getMessage('msg_failure_sync')

            cmd1 = r'%s -t %s %s "%s" "%s"' %(
                    self.config.getGlobals('notify_binary'),
                    notify_time,
                    notify_icon,
                    self.config.getGlobals('notify_subject'),
                    notify_text
                )
            CommandExec(cmd1, self.config, self.logger).run()

            if os.path.isfile(self.config.getGlobals('notify_sound_failure')) and os.path.isfile(self.config.getGlobals('mplayer_binary')):
                cmd2 = r'%s %s' %(
                        self.config.getGlobals('mplayer_binary'),
                        self.config.getGlobals('notify_sound_failure')
                    )
                CommandExec(cmd2, self.config, self.logger).run()

        
    def run(self, event):

        self.watchdir = self._watchdir(event)

        if self.watchdir is not None:
            cmd = r'%s %s -e "ssh -i %s -p %s" %s %s >%s' %(self.config.getGlobals('sync_binary'),
                self.config.getGlobals('sync_options'),
                self.config.getGlobals('sync_sshkeys'),
                self.config.getGlobals('sync_sshport'),
                self.watchdir,
                self.config.getGlobals('sync_targets'),
                self.config.getGlobals('sync_logfile'))

            return CommandExec(cmd, self.config, self.logger).run()
        else:
            return 99

    def _watchdir(self, event):
        for directory in self.config.getDirectorys('directorys'):
            if re.compile(r'^'+os.path.join(self.config.getGlobals('base'), directory)+os.path.sep).match(event.pathname):
                return os.path.join(self.config.getGlobals('base'), directory)

def main():
    if len(sys.argv) < 2:
        sys.stderr.write(
            'An Error occured:'
            ' You have to define the config file as argument.\n'
        )
        sys.exit(1)

    try:
        config = ConfigReader(sys.argv[1])
        logger = (
            SyncLogger(config) if config.getGlobals('logging') == '1' else None
        )
        watch_manager = pyinotify.WatchManager()
        notifier = pyinotify.Notifier(
            watch_manager, SyncEventHandler(config, logger)
        )
        for directory in config.getDirectorys('directorys'):
            watch_manager.add_watch(
                os.path.join(config.getGlobals('base'), directory),
                MASK,
                rec=True
            )
        notifier.loop()
    except Exception as error:
        raise SyncException('An Exception occured: {0}'.format(error))


if __name__ == '__main__':
    main()

Code: Alles auswählen

# File: syncconfig.py
# -*- coding: utf-8 -*-

import ConfigParser
from syncexception import SyncException

class ConfigReader:

    def __init__(self, filename):
        self._cp = ConfigParser.ConfigParser()
        self._globals = {}
        self._directorys = {}
        self._messages = {}

        try:
            self._cp.readfp(open(filename))
        except Exception, ex:
            raise SyncException('Not able to read the configuration file: %s' %(filename))
        self._read_configuration()

    def _read_configuration(self):
        for section in self._cp.sections():
            if section.strip() == 'GLOBALS':
                self._globals = self._get_globals_from_config(section)
            elif section.strip() == 'DIRECTORYS':
                self._directorys = self._get_globals_from_config(section)
            elif section.strip() == 'MESSAGES':
                self._messages = self._get_globals_from_config(section)

    def _get_config_value(self, section, element):
        if section == 'messages':
            if self._messages.get(element):
                return self._messages.get(element)
            else:
                raise SyncException('No value for element %s defined !' %(element))
        elif section == 'globals':
            if self._globals.get(element):
                return self._globals.get(element)
            else:
                raise SyncException('No value for element %s defined !' %(element))
        else:
            raise SyncException('No value for section %s defined !' %(section))

    def _get_globals_from_config(self, section):
        values = {}
        for item in self._cp.items(section):
            values[item[0]] = str(item[1]).strip()
        return values

    def getDirectorys(self, element):
        dirs = self._directorys.get(element)
        dirs_list = [i.strip() for i in dirs.split(",")]
        return dirs_list

    def getGlobals(self, element):
        return self._get_config_value('globals', element)

    def getMessage(self, element):
        return self._get_config_value('messages', element)

Code: Alles auswählen

# File: syncexception.py
# -*- coding: utf-8 -*-

class SyncException(Exception):
    def __init__(self, msg):
        self.msg = msg

    def __str__(self):
        return repr(self.msg)

Code: Alles auswählen

# File: syncexec.py
# -*- coding: utf-8 -*-

import os
from subprocess import Popen, PIPE, STDOUT

# do the sync call to the given repository and log the results
# create a simple thread which extends the Thread class
class CommandExec():
  # constructor, set the sync command and if logging should be enabled
  def __init__(self, command, config, logger):
    self.command = command
    self.config = config
    self.logger = logger

  # running the command, depending if logging is switched on or off
  def run(self):
    proc = Popen(self.command, shell=True, stdout=PIPE, stderr=STDOUT)
    output, errors = proc.communicate()
    if self.logger is not None:
        self.logger.info("The command %s has completed with exit code %s." %(self.command, proc.returncode))
    return proc.returncode

Code: Alles auswählen

# File: synclogger.py
# -*- coding: utf-8 -*-

import logging

class SyncLogger:

    Logformat = r'%(asctime)s %(levelname)s %(message)s'

    def __init__(self, config):
        self.logging = config.getGlobals('logging')
        self.logfile = config.getGlobals('logfile')
        self.logger = logging.getLogger("synclogger")
        
        fh = logging.FileHandler(config.getGlobals('logfile'), 'a')
        formatter = logging.Formatter(self.Logformat)
        fh.setFormatter(formatter)
        self.logger.addHandler(fh)

    def info(self,msg):
        self.logger.setLevel(logging.INFO)
        self.logger.info(msg)

    def error(self,msg):
        self.logger.setLevel(logging.ERROR)
        self.logger.error(msg)

    def warning(self,msg):
        self.logger.setLevel(logging.WARNING)
        self.logger.warning(msg)

    def debug(self,msg):
        self.logger.setLevel(logging.DEBUG)
        self.logger.debug(msg)
Ach, eines noch - mir ist aufgefallen, dass ein rsync bei manchen Datei Operationen (vorhandene Datei mir gedit bearbeiten), mehrmals (3 mal) durchgeführt wird. Die Programme (gedit, vi) legen temporäre Dateien an, bei denen die Events ebenfalls durchgeführt werden. Kann man das irgendwie verhindern? Wenn eine Datei erstellt, bearbeitet, umbenannt oder gelöscht wird, soll nur ein sync durchgeführt werden, und nicht wenn irgendwelche temporäre Dateien erstellt und wieder gelöscht werden.
BlackJack

@erselbst: Die Dokumentation zu Packages sagt ja nicht das man jedes Projekt unbedingt irgendwie auf Packages und Unterpackages verteilen soll, sondern Dokumentiert was Packages und Unterpackages sind. Ich habe es ja schon geschrieben: IMHO ist Dein Programm jetzt schon zu stark aufgeteilt. Eine Klasse pro Modul macht keinen Sinn, weil dann das Modul als Organisationseinheit wertlos wäre. Ein Modul ist dazu da zusammengehörige Konstanten, Funktionen, und Klassen zusammenzufassen. Das könnte man also alles in *einem* Modul speichern.

An dem Quelltext gibt es einige etwas komische Sachen und einiges was zu umständlich gelöst ist.

`SyncException` bräuchte einfach nur aus einem ``pass`` bestehen und man hätte den selben Effekt. Die Ausnahme scheint einfach nur benutzt zu werden um alle anderen Ausnahmen die abgefangen werden durch diese weniger spezifische Ausnahme zu ersetzen. Das ist IMHO keine sinnvolle Ausnahmebehandlung.

`SyncLogger` macht keinen Sinn. Wozu hat man Loglevels wenn man in jeder Methode das Level der jeweiligen Methode einstellt. Da könnte man einfach einmal `DEBUG` setzten und dann die ganz normalen Methoden des Objekts verwenden. Damit werden ausser der `__init__()` alle anderen Methoden überflüssig. Und wenn man nur die `__init__()` hat, macht eine eigene Klasse dafür nicht mehr viel Sinn, da kann man auch einfach eine Funktion schreiben, welche die Logging-Einstellungen vornimmt und einen Logger zurück gibt.

Der `ConfigReader` erscheint mir eine übermässig komplexe Kapselung des `ConfigParser`zu sein. Da wird das `ConfigParser`-Exemplar unnötig an das Objekt gebunden, weil es nach der Abarbeitung von `__init__()` überhaupt nicht mehr gebraucht wird. Und die verschachtelte Struktur einer INI-Datei wird auf Attribute aufgeteilt, nur um beim Zugriff dann Entscheidungen mit ``if``\s zu treffen, die nur deswegen überhaupt nötig sind. Der einzige Mehrwert ist die Möglichkeit eine Liste von Werten abzufragen. Dazu hätte man aber auch vom `ConfigParser` ableiten und eben genau diese eine zusätzliche `get`-Methode implementieren können. Die gesamte Klasse würde damit auf drei einfache Zeilen schrumpfen.

Die Mehrzahl von „directory” heisst übrigens „directories”.

Dateien die man öffnet, sollte man übrigens auch wieder schliessen. Die ``with``-Anweisung kann helfen das zuverlässig zu erledigen.

Mit `CommandExec` haben wir wieder eine sinnlose Klasse, die problemlos durch eine einfachere Funktion ersetzt werden kann. Das ist fast immer bei Klassen der Fall die neben der `__init__()` nur eine Methode haben, die dann auch noch genau einmal direkt nach dem Erstellen des Exemplars aufgerufen wird. Das `config`-Argument wird überhaupt nicht verwendet.

``shell=True`` sollte man bei `Popen` nicht verwenden, schon gar nicht wenn man keine Kontrolle über die Argumente hat die da irgendwie in die Kommandozeichenkette hinein gebastelt wurden. Wenn man nicht ganz so viel Pech hat, dann haut nur das Escapen von Shell-Sonderzeichen nicht richtig hin. Wenn man aber so richtig Pech hat, nutzt das jemand aus um beliebigen Shell-Code mit den Rechten dieses Prozesses ausführen zu lassen. Wenn Du die Ausgabe ignorierst, warum wird sie dann überhaupt vom Programm aufgefangen?

Logging wird falsch verwendet. Das zeichnete sich ja schon bei der eigenartigen Logging-Klasse ab. Beim Logging wird nicht überall im Code entschieden ob geloggt werden soll oder nicht, sondern es wird grundsätzlich die Loggingmethode aufgerufen und das eingestellte Loglevel entscheidet darüber ob und wie das dann ausgegeben wird.

Den `SyncEventHandler` kann man etwas kürzen in dem man die Namen der speziellen Methoden an die `proc()`-Methode bindet statt drei Methoden zu schreiben, die einfach nur `proc()` mit den gleichen Argumenten aufrufen.

Die `notify()`-Methode ist zu lang. Das dürfte zum Teil von Kopieren und Einfügen kommen, statt die Gemeinsamkeiten in eine Funktion auszulagern. Man könnte das auch in eine eigene Klasse kapseln, und bestimmte Prüfungen und das zusammensetzen des Kommandos *einmal* machen, statt bei jedem Aufruf aufs neue.

In der `run()`-Methode wird `watchdir` unnötigerweise an das Objekt gebunden. Die `_watchdir()`-Methode ist überkompliziert und ineffizient. Hier scheint eigentlich nur `str.startswith()` nötig zu sein.

Das ``try``/``except``-Konstrukt in der `main()`-Funktion macht eigentlich keinen Sinn, denn es ersetzt die Ausnahme nur durch eine weniger Informative die noch nicht einmal einen Traceback besitzt, also nicht gerade hilfreich bei der Fehlersuche ist.

Ungetestet und IMHO immer noch verbesserungswürdig:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import re
import sys
from ConfigParser import ConfigParser
from subprocess import Popen, STDOUT
import pyinotify

LOGFORMAT = '%(asctime)s %(levelname)s %(message)s'
MASK = pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO
LOG = logging.getLogger('synclogger')


def configure_logger(filename, level):
    file_handler = logging.FileHandler(filename, 'a')
    file_handler.setFormatter(logging.Formatter(LOGFORMAT))
    LOG.addHandler(file_handler)
    LOG.setLevel(level)


class ExtendetConfigParser(ConfigParser):
    def get_strings(self, section, option):
        return [s.strip() for s in self.get(section, option).split(',')]


def execute_command(command, out_file=None):
    process = Popen(
        command, stderr=STDOUT if out_file else None, stdout=out_file
    )
    process.wait()
    LOG.info(
        'The command %s has completed with exit code %s.',
        command,
        process.returncode
    )
    return process.returncode


class SyncEventHandler(pyinotify.ProcessEvent):
    # Exclude filter
    match_exclude = re.compile(r'.+(\.sw[xp]|~)$').match

    def __init__(self, config, pevent=None, **kwargs):
        pyinotify.ProcessEvent.__init__(self, pevent, **kwargs)
        self.config = config

    def process(self, event):
        if not self.match_exclude(event.pathname):
            LOG.info(
                'The event %s has been applied to %s.',
                event.maskname,
                event.pathname
            )
            if self.run(event) == 0:
                self.notify('success', event)
            else:
                self.notify('failure', event)

    process_IN_DELETE = process_IN_CLOSE_WRITE = process_IN_MOVED_TO = process

    def notify(self, state, event):
        notify_time = '%.0f' % (
            self.config.getint('globals', 'notify_delay') * 1000
        )
        if state == 'success':
            command = [
                self.config.get('globals', 'notify_binary'),
                '-t', notify_time,
            ]
            if os.path.isfile(
                self.config.get('globals', 'notify_icon_success')
            ):
                command.extend(
                    ['-i', self.config.get('globals', 'notify_icon_success')]
                )
            notify_text = self.config.get('messages', 'msg_success_sync') + ' '
            if event.maskname == 'IN_DELETE':
                notify_text += self.config.get('messages', 'msg_delete')
            else:
                notify_text += self.config.get('messages', 'msg_upload')

            command.extend(
                [
                    self.config.get('globals', 'notify_subject'),
                    notify_text % event.name
                ]
            )
            execute_command(command)
            if (
                os.path.isfile(
                    self.config.get('globals', 'notify_sound_success')
                )
                and os.path.isfile(self.config.get('globals', 'mplayer_binary'))
            ):
                execute_command(
                    [
                        self.config.get('globals', 'mplayer_binary'),
                        self.config.get('globals', 'notify_sound_success')
                    ]
                )
        elif state == 'failure':
            command = [
                self.config.get('globals', 'notify_binary'),
                '-t', notify_time,
            ]
            if os.path.isfile(
                self.config.get('globals', 'notify_icon_failure')
            ):
                command.extend(
                    ['-i', self.config.get('globals', 'notify_icon_failure')]
                )
            command.extend(
                [
                    self.config.get('globals', 'notify_subject'),
                    self.config.get('messages', 'msg_failure_sync')   
                ]
            )
            execute_command(command)

            if (
                os.path.isfile(
                    self.config.get('globals', 'notify_sound_failure')
                )
                and os.path.isfile(self.config.get('globals', 'mplayer_binary'))
            ):
                execute_command(
                    [
                        self.config.get('globals', 'mplayer_binary'),
                        self.config.get('globals', 'notify_sound_failure')
                    ]
                )
        else:
            LOG.error('Unknown state %r', state)
    
    def run(self, event):
        watchdir = self._watchdir(event)
        if watchdir is not None:
            with open(
                self.config.get('globals', 'sync_logfile'), 'w'
            ) as log_file:
                return execute_command(
                    [
                        self.config.get('globals', 'sync_binary'),
                        self.config.get('globals', 'sync_options'),
                        '-e', 'ssh -i {0} -p {1}'.format(
                            self.config.get('globals', 'sync_sshkeys'),
                            self.config.get('globals', 'sync_sshport')
                        ),
                        watchdir,
                        self.config.get('globals', 'sync_targets'),
                        self.config.get('globals', 'sync_logfile'),
                    ],
                    log_file
                )
        else:
            return 99  # TODO: Too magic.

    def _watchdir(self, event):
        for directory in self.config.get_strings('directories', 'directories'):
            if re.compile(r'^'+os.path.join(self.config.get('globals', 'base'), directory)+os.path.sep).match(event.pathname):
                return os.path.join(self.config.get('globals', 'base'), directory)
        return None


def main():
    if len(sys.argv) < 2:
        sys.stderr.write(
            'An Error occured:'
            ' You have to give a config file as argument.\n'
        )
        sys.exit(1)
    config = ExtendetConfigParser()
    config.read(sys.argv[1])

    configure_logger(
        config.get('globals', 'logfile'),
        logging.DEBUG
            if config.getboolean('globals', 'logging')
            else logging.ERROR
    )
    watch_manager = pyinotify.WatchManager()
    notifier = pyinotify.Notifier(watch_manager, SyncEventHandler(config))
    for directory in config.get_strings('directories', 'directories'):
        watch_manager.add_watch(
            os.path.join(config.get('globals', 'base'), directory),
            MASK,
            rec=True
        )
    notifier.loop()


if __name__ == '__main__':
    main()
Ich würde das herumreichen vom Logger und von der Konfiguration einschränken.

Edit: Logger nicht mehr herum gereicht.
erselbst
User
Beiträge: 7
Registriert: Mittwoch 20. März 2013, 08:05

Hallo,
sorry, das ich mich jetzt erst melde. Wochenende lag da zwischen, krank bin ich auch noch :-(

Also der Code funktioniert wunderbar. Ich musste die Keywords der Sektionen noch groß schreiben, dann ging es bis auf das rsync Kommando. Der Parameter sync_option führte immer zum Fehler. Ich habe es nun so gelöst, das man zwei Parameter angeben kann. Ich habe es versucht, mit join() an die Stelle des Parameters zu bringen, hat aber nicht funktioniert. Nun speichere ich mir die Werte im Array sync_opts und setze sync_opts[0] und sync_opts[1] an die entsprechende Stelle. Ist nicht schön, aber wie kann man es besser machen?

Dann habe ich blöder weise das Logfile in ein zu überwachendes Verzeichnis gelegt und mir somit eine Endlosschleife eingehandelt. Dann habe ich ein Parameter excludes_pattern unter DIRECTORIES eingefügt, wo man mit Regulären Ausdrücken, so was ausschließen kann. Funktioniert ganz gut.

Auf die Parameter sync_sshkeys und sync_sshport habe ich verzichtet. Wenn man als sync_targets z.B. ein anderes Verzeichnis auf einer anderen Partition synchronisieren möchte, benötigt man kein sync_sshkeys + sync_sshport. Daher habe ich es nun sync_sshopts genannt und wenn dieser vorhanden ist, wird dieser auch genutzt.

Hier nochmal die Änderungen der sync.cfg

Code: Alles auswählen

....
sync_options = --delete -vrptgoDL
sync_sshopts = -e ssh -i /home/tux/.ssh/sync-key -p 22
....
mplayer_binary = /usr/bin/mplayer
mplayer_option = -quiet
....

[DIRECTORIES]
directories = bin,movies,music,pics
excludes_pattern = ^.*/(rsync\.log|pysync\.log)$|^.+(\.sw[xp]|~)$
....
Und hier nochmal der vollständige Code der sync.py

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import re
import sys
from ConfigParser import ConfigParser
from subprocess import Popen, STDOUT
import pyinotify

LOGFORMAT = '%(asctime)s %(levelname)s %(message)s'
MASK = pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO
LOG = logging.getLogger('synclogger')


def configure_logger(filename, level):
    file_handler = logging.FileHandler(filename, 'a')
    file_handler.setFormatter(logging.Formatter(LOGFORMAT))
    LOG.addHandler(file_handler)
    LOG.setLevel(level)


class ExtendetConfigParser(ConfigParser):
    def get_strings(self, section, option):
        return [s.strip() for s in self.get(section, option).split(',')]


def execute_command(command, out_file=None):
    process = Popen(
        command, stderr=STDOUT if out_file else None, stdout=out_file
    )
    process.wait()
    LOG.info(
        'The command %s has completed with exit code %s.',
        command,
        process.returncode
    )
    return process.returncode


class SyncEventHandler(pyinotify.ProcessEvent):

    def __init__(self, config, pevent=None, **kwargs):
        pyinotify.ProcessEvent.__init__(self, pevent, **kwargs)
        self.config = config
        self.match_exclude = re.compile(
            self.config.get('DIRECTORIES', 'excludes_pattern')
        ).match

    def process(self, event):
        if not self.match_exclude(event.pathname):
            LOG.info(
                'The event %s has been applied to %s.',
                event.maskname,
                event.pathname
            )
            if self.run(event) == 0:
                self.notify('success', event)
            else:
                self.notify('failure', event)

    process_IN_DELETE = process_IN_CLOSE_WRITE = process_IN_MOVED_TO = process

    def notify(self, state, event):
        notify_time = '%.0f' % (
            self.config.getint('GLOBALS', 'notify_delay') * 1000
        )
        if state == 'success':
            command = [
                self.config.get('GLOBALS', 'notify_binary'),
                '-t', notify_time,
            ]
            if os.path.isfile(
                self.config.get('GLOBALS', 'notify_icon_success')
            ):
                command.extend(
                    ['-i', self.config.get('GLOBALS', 'notify_icon_success')]
                )
            notify_text = self.config.get('MESSAGES', 'msg_success_sync') + ' '
            if event.maskname == 'IN_DELETE':
                notify_text += self.config.get('MESSAGES', 'msg_delete')
            else:
                notify_text += self.config.get('MESSAGES', 'msg_upload')

            command.extend(
                [
                    self.config.get('GLOBALS', 'notify_subject'),
                    notify_text % event.name
                ]
            )
            execute_command(command)
            if (
                os.path.isfile(
                    self.config.get('GLOBALS', 'notify_sound_success')
                )
                and os.path.isfile(self.config.get('GLOBALS', 'mplayer_binary'))
            ):
                execute_command(
                    [
                        self.config.get('GLOBALS', 'mplayer_binary'),
                        self.config.get('GLOBALS', 'mplayer_option'),
                        self.config.get('GLOBALS', 'notify_sound_success')
                    ]
                )
        elif state == 'failure':
            command = [
                self.config.get('GLOBALS', 'notify_binary'),
                '-t', notify_time,
            ]
            if os.path.isfile(
                self.config.get('GLOBALS', 'notify_icon_failure')
            ):
                command.extend(
                    ['-i', self.config.get('GLOBALS', 'notify_icon_failure')]
                )
            command.extend(
                [
                    self.config.get('GLOBALS', 'notify_subject'),
                    self.config.get('MESSAGES', 'msg_failure_sync')  
                ]
            )
            execute_command(command)

            if (
                os.path.isfile(
                    self.config.get('GLOBALS', 'notify_sound_failure')
                )
                and os.path.isfile(self.config.get('GLOBALS', 'mplayer_binary'))
            ):
                execute_command(
                    [
                        self.config.get('GLOBALS', 'mplayer_binary'),
                        self.config.get('GLOBALS', 'mplayer_option'),
                        self.config.get('GLOBALS', 'notify_sound_failure')
                    ]
                )
        else:
            LOG.error('Unknown state %r', state)
   
    def run(self, event):
        watchdir = self._watchdir(event)
        if watchdir is not None:

            ssh_opts = ""
            logfile = ""
            sync_opts = [s.strip() for s in self.config.get('GLOBALS', 'sync_options').split()]

            if self.config.get('GLOBALS', 'sync_sshopts') is not None:
                ssh_opts = self.config.get('GLOBALS', 'sync_sshopts')

            with open(
                self.config.get('GLOBALS', 'sync_logfile'), 'w'
            ) as log_file:
                return execute_command(
                    [
                        self.config.get('GLOBALS', 'sync_binary'),
                        sync_opts[0],
                        sync_opts[1],
                        ssh_opts,
                        watchdir,
                        self.config.get('GLOBALS', 'sync_targets')
                    ],
                    log_file
                )
        else:
            return 99  # TODO: Too magic.

    def _watchdir(self, event):
        for directory in self.config.get_strings('DIRECTORIES', 'directories'):
            if re.compile(r'^'+os.path.join(self.config.get('GLOBALS', 'base'), directory)+os.path.sep).match(event.pathname):
                return os.path.join(self.config.get('GLOBALS', 'base'), directory)
        return None


def main():
    if len(sys.argv) < 2:
        sys.stderr.write(
            'An Error occured:'
            ' You have to give a config file as argument.\n'
        )
        sys.exit(1)
    config = ExtendetConfigParser()
    config.read(sys.argv[1])

    configure_logger(
        config.get('GLOBALS', 'logfile'),
        logging.DEBUG
            if config.getboolean('GLOBALS', 'logging')
            else logging.ERROR
    )
    watch_manager = pyinotify.WatchManager()
    notifier = pyinotify.Notifier(watch_manager, SyncEventHandler(config))
    for directory in config.get_strings('DIRECTORIES', 'directories'):
        watch_manager.add_watch(
            os.path.join(config.get('GLOBALS', 'base'), directory),
            MASK,
            rec=True
        )
    notifier.loop()


if __name__ == '__main__':
    main()
Habe ich vergessen zu posten. Hier sind die Logauszüge vom rsync Kommando mit dem Exit Code. Erst wenn sync_option seperat gesetzt werden, wird der Befehl korrekt ausgeführt (letzte Zeile).

Code: Alles auswählen

command ['/usr/bin/rsync', '-vvvrptgoDL', '-e "ssh -i /home/mschulz/.ssh/unison -p 22"', '/home/mschulz/bin', 'root@storage01:/mnt/array1/mschulz/test/'] has completed with exit code 14
command ['/usr/bin/rsync', '-vvvrptgoDL', '-e "ssh -i /home/mschulz/.ssh/unison"', '/home/mschulz/bin', 'root@storage01:/mnt/array1/mschulz/test/'] has completed with exit code 14.
command ['/usr/bin/rsync', '-vvvrptgoDL --delete', '-e "ssh -i /home/mschulz/.ssh/unison"', '/home/mschulz/bin', 'root@storage01:/mnt/array1/mschulz/test/'] has completed with exit code 1.
command ['/usr/bin/rsync', '-vvvrptgoDL', '-e ssh -i /home/mschulz/.ssh/unison -p 22', '/home/mschulz/bin', 'root@storage01:/mnt/array1/mschulz/test/'] has completed with exit code 0.
command ['/usr/bin/rsync', '--delete', '-vrptgoDL', '-e ssh -i /home/mschulz/.ssh/unison -p 22', '/home/mschulz/bin', 'root@storage01:/mnt/array1/mschulz/test/'] has completed with exit code 0.
BlackJack

@erselbst: `logfile` in der `run()`-Methode wird überhaupt nicht verwendet.

Die Optionen aus der Konfigurationsdatei kann man mit `shlex.split()` aufteilen. Und dann muss man die Liste für den `execute_command()`-Aufruf ja nicht in einem Stück hinschreiben. Man kann die ja auch nach und nach aufbauen und mit `append()` und/oder `extend()` um weitere Elemente erweitern. Die `run()`-Methode könnte dann so aussehen (ungetestet):

Code: Alles auswählen

    def run(self, event):
        watchdir = self._watchdir(event)
        if watchdir is not None:
            command = [self.config.get('GLOBALS', 'sync_binary')]
            command.extend(
                shlex.split(self.config.get('GLOBALS', 'sync_options'))
            )
            ssh_opts = self.config.get('GLOBALS', 'sync_sshopts')
            if ssh_opts is not None:
                command.append(ssh_opts)
            command.extend(
                [watchdir, self.config.get('GLOBALS', 'sync_targets')]
            )
            with open(
                self.config.get('GLOBALS', 'sync_logfile'), 'w'
            ) as log_file:
                return execute_command(command, log_file)
        else:
            return 99  # TODO: Too magic.
`_watchdir()` ist wie schon gesagt viel zu kompliziert. `re.compile()` macht nur Sinn wenn man das Ergebnis auch irgend wo speichert. Sonst kann man hier auch gleich `re.match()` verwenden. `re.match()` schaut immer am Anfang der Zeichenkette, also ist das '^' redundant. In Pfadnamen können Zeichen vorkommen die in regulären Ausdrücken eine besondere Bedeutung haben, also muss man die hier „escapen” um auf der sicheren Seite zu sein. *Aber*: reguläre Ausdrücke sind hier schon vollkommen überflüssig, denn es handelt sich ja nicht um ein Muster sondern eine statische Zeichenkette — Du willst wissen ob der Pfadname von dem Ereignis mit einem der überwachten Verzeichnisse beginnt. Das geht viel einfacher mit der `startswith()`-Methode auf Zeichenketten. Wenn man dann noch die mehrfachen Berechnungen mit gleichem Ergebnis jeweils nur einmal durchführt und sich das Ergebnis merkt, kommt man zu (ungetestet):

Code: Alles auswählen

    def _watchdir(self, event):
        base = self.config.get('GLOBALS', 'base')
        for directory in self.config.get_strings('DIRECTORIES', 'directories'):
            full_path = os.path.join(base, directory)
            if event.pathname.startswith(full_path + os.path.sep):
                return full_path
        return None
Was ich noch ändern würde ist das herum reichen des `config`-Objekts. Das würde ich auf die `main()`-Funktion beschränken und dem `SyncEventHandler` stattdessen ein Objekt zum Synchronisieren und eines zum Benachrichtigen des Benutzers übergen, und eventuell noch die Zeichenketten/Vorlagen für die verschiedenen Nachrichten, die in der Konfigurationsdatei stehen. Die beiden Objekte könnten in der `main()`-Funktion erstellt werden und da mit den nötigen Daten aus der Konfigurationsdatei versorgt werden.

Auf jeden Fall ist die `notify()`-Methode zu lang. Da ist zu viel Code doppelt vorhanden, der sich nur geringfügig durch die Daten unterscheidet.
erselbst
User
Beiträge: 7
Registriert: Mittwoch 20. März 2013, 08:05

Hallo BlackJack,
die run + _watchdir Methode konnte ich so übernehmen. An die extend() Funktion habe ich nicht gedacht. Die haben wir ja schon bei der notify Methode verwendet. Ich habe mir nun eine Funktion desktop_notify() geschrieben, welche das pynotify Modul nutzt. Somit wird das Benachrichtigungsfeld von Python erzeugt, nicht von notify-send. Ob es von Vorteil ist, wenn noch zusätzlich ein Modul geladen werden muss.

Hmm???

Was ich nicht verstehe, ist das aufteilen des config Objekts. Es werden doch alle Werte aus der Konfiguration in der SyncEventHandler Klasse benötigt. Was für ein Vorteil habe ich, wenn ich z.B. ein message, sync und globals Objekt habe? Alle drei Objekte müssen doch der Klasse dann übergeben werden, anstatt nur ein config Objekt.

Hier ist nun das fertige Programm, was schon seit ein paar Tagen seinen Dienst tut.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import re
import sys
import shlex
from ConfigParser import ConfigParser
from subprocess import Popen, STDOUT
import pyinotify
import pynotify

LOGFORMAT = '%(asctime)s %(levelname)s %(message)s'
MASK = pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO
LOG = logging.getLogger('synclogger')
NOTIFYDELAY = 10

def configure_logger(filename, level):
    file_handler = logging.FileHandler(filename, 'a')
    file_handler.setFormatter(logging.Formatter(LOGFORMAT))
    LOG.addHandler(file_handler)
    LOG.setLevel(level)


class ExtendetConfigParser(ConfigParser):
    def get_strings(self, section, option):
        return [s.strip() for s in self.get(section, option).split(',')]


def execute_command(command, out_file=None):
    process = Popen(
        command, stderr=STDOUT if out_file else None, stdout=out_file
    )
    process.wait()
    LOG.info(
        'The command %s has completed with exit code %s.',
        command,
        process.returncode
    )
    return process.returncode

def desktop_notify(subject, body, **args):
    if args.has_key("timeout") and args['timeout'] is not None:
        timeout = int('%.0f' % (args['timeout']))
    else:
        timeout = NOTIFYDELAY

    note = pynotify.init(subject)

    if args.has_key("icon") and args['icon'] is not None and os.path.isfile(args['icon']):
        note = pynotify.Notification(subject, body, args['icon'])
    else:
        note = pynotify.Notification(subject, body)

    note.set_timeout(timeout)
    note.show()

class SyncEventHandler(pyinotify.ProcessEvent):

    def __init__(self, config, pevent=None, **kwargs):
        pyinotify.ProcessEvent.__init__(self, pevent, **kwargs)
        self.config = config
        self.match_exclude = re.compile(
            self.config.get('DIRECTORIES', 'excludes_pattern')
        ).match

    def process(self, event):
        if not self.match_exclude(event.pathname):
            LOG.info(
                'The event %s has been applied to %s.',
                event.maskname,
                event.pathname
            )
            if self.run(event) == 0:
                self.notify('success', event)
            else:
                self.notify('failure', event)

    process_IN_DELETE = process_IN_CLOSE_WRITE = process_IN_MOVED_TO = process

    def notify(self, state, event):

        notify_time = int('%.0f' % (
            self.config.getint('GLOBALS', 'notify_delay') * 1000
        ))

        if state == 'success':

            notify_text = self.config.get('MESSAGES', 'msg_success_sync') + ' '
            if event.maskname == 'IN_DELETE':
                notify_text += self.config.get('MESSAGES', 'msg_delete')
            else:
                notify_text += self.config.get('MESSAGES', 'msg_upload')

            if os.path.isfile(
                self.config.get('GLOBALS', 'notify_icon_success')
            ):
                notify_icon = self.config.get('GLOBALS', 'notify_icon_success')

            if os.path.isfile(
                    self.config.get('GLOBALS', 'notify_sound_success')
            ):
                notify_sound = self.config.get('GLOBALS', 'notify_sound_success')

        elif state == 'failure':

            notify_text = self.config.get('MESSAGES', 'msg_failure_sync')

            if os.path.isfile(
                self.config.get('GLOBALS', 'notify_icon_failure')
            ):
                notify_icon = self.config.get('GLOBALS', 'notify_icon_failure')

            if os.path.isfile(
                    self.config.get('GLOBALS', 'notify_sound_failure')
            ):
                notify_sound = self.config.get('GLOBALS', 'notify_sound_failure')

        else:
            LOG.error('Unknown state %r', state)

        desktop_notify(self.config.get('GLOBALS', 'notify_subject'),
                       notify_text % event.name,
                       timeout = notify_time,
                       icon = notify_icon
        )

        if (
            notify_sound is not None
            and os.path.isfile(self.config.get('GLOBALS', 'mplayer_binary'))
        ):
            execute_command(
                [
                    self.config.get('GLOBALS', 'mplayer_binary'),
                    self.config.get('GLOBALS', 'mplayer_option'),
                    notify_sound
                ]
            )

   
    def run(self, event):
        watchdir = self._watchdir(event)
        if watchdir is not None:
            command = [self.config.get('GLOBALS', 'sync_binary')]
            command.extend(
                shlex.split(self.config.get('GLOBALS', 'sync_options'))
            )
            ssh_opts = self.config.get('GLOBALS', 'sync_sshopts')
            if ssh_opts is not None:
                command.append(ssh_opts)
            command.extend(
                [watchdir, self.config.get('GLOBALS', 'sync_targets')]
            )
            with open(
                self.config.get('GLOBALS', 'sync_logfile'), 'w'
            ) as log_file:
                return execute_command(command, log_file)
        else:
            LOG.error('No watch directory defined!')
            return 99  # TODO: It's magic.

    def _watchdir(self, event):
        for directory in self.config.get_strings('DIRECTORIES', 'directories'):
            full_path = os.path.join(self.config.get('GLOBALS', 'base'), directory)
            if event.pathname.startswith(full_path + os.path.sep):
                return full_path
        return None


def main():
    if len(sys.argv) < 2:
        sys.stderr.write(
            'An Error occured:'
            ' You have to give a config file as argument.\n'
        )
        sys.exit(1)
    config = ExtendetConfigParser()
    config.read(sys.argv[1])

    configure_logger(
        config.get('GLOBALS', 'logfile'),
        logging.DEBUG
            if config.getboolean('GLOBALS', 'logging')
            else logging.ERROR
    )
    watch_manager = pyinotify.WatchManager()
    notifier = pyinotify.Notifier(watch_manager, SyncEventHandler(config))
    for directory in config.get_strings('DIRECTORIES', 'directories'):
        watch_manager.add_watch(
            os.path.join(config.get('GLOBALS', 'base'), directory),
            MASK,
            rec=True
        )
    notifier.loop()


if __name__ == '__main__':
    main()
BlackJack

@erselbst: Mit einem `config`-Objekt was sich durch den gesamten Code zieht hat man letztendlich ja wieder so etwas wie ``global`` nur dass man die „Variablen” immer *alle* herum reicht. Code lässt sich so schwerer Testen oder Wiederverwenden, weil man immer ein `config`-Objekt erstellen muss, wobei auch nicht so einfach ersichtlich ist welche Werte dort für welche Funktionalität minimal benötigt werden, ohne dass man den gesamten Code nachvollzieht der direkt oder indirekt Zugriff auf `config` hat.

In der `notify()`-Methode ist ein Fehler: Es kann passieren, dass `notify_icon` und `notify_sound` verwendet werden, ohne dass sie definiert wurden.
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Vielleicht noch eine Stil-Anmerkung: Bevor man ständig Funktions- bzw Methodenaufrufe hat, die über mindestens 3 Zeilen gehen, würde ich persönlich eher diejenigen Argumente, die in relativ langem Quelltext ausgedrückt werden müssen, vorher an einen eigenen Namen binden und diesen Namen dann für den Aufruf verwenden. Beim Loggen könnte dies z.B. `msg = 'blablabla'` sein, für den Aufruf auf `command` in `SyncEventHandler.run()` könnte ich mir vorab ein `options = self.config.get('GLOBALS', 'sync_options')` und ein anschließendes `command.extend(shlex.split(options))` vorstellen.

Nach wie vor erscheint mir die `.notify()`-Methode zu lang: Dieses ganze `isfile()`-`config.get()`-Zeugs ließe sich auch in einer separaten Methode auslagern, welche man durchaus im `ExtendedConfigParser` einbauen könne (so zumindest mein erster "Gedankenblitz").
Antworten