Logowanie argumentów funkcji **kwargs gdy leci exception

0

Głowny problem

Mam jakiś skrypt python uruchamiany przez Jenkins-a, który robi jakieś cuda: Skanuję dużą ilość plików i generuje uproszony raport.
Jak coś pójdzie nie tak i leci wyjątek, to chce by niektóre funkcje logowały, jakie dostały argumenty, żeby było łatwo ustalić co włąściwie poszło nie tak.
W moim przypadku chodzi o to by było wiadomo, który plik xml jest niepoprawny.

Próba rozwiązania

w związku z tym naskrobałem coś takiego:

def log_argument_on_exception(func):
    """Decorator that logs the argument value if an exception is raised."""

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"{func.__name__}({', '.join(map(repr, args))})")
            raise e

    return wrapper

Funkcja z tym dekoratorem jak rzuca wyjątek to wypisuje np:

parse_xml(WindowsPath('../Rules/Exclusions/Microsoft, Various services - [B1002].xml'))

Problem

Powyższy kod działa bardzo dobrze. W moim przypadku sprawdza się wzorcowo.
Problem polega na tym, że rozwiązanie nie jest dostatecznie uniwersalne, żebym był zadowolony.
Nie wypisuje zawartości **kwargs, wiec jeśli udekorowana funkcja wygląda tak:

@log_argument_on_exception
def magic(path, *, option=None):
    ...

To informacja option nie zostanie wypisana :(.

Taka próba poprawiania (która się wydawała rozsądna), psuje cały kod (normalne argumenty przestają działać):

            print(f"{func.__name__}({', '.join(map(repr, args, kwargs))})")

Próbowałem na inne sposoby z gorszym rezultatem.

Pytanie

Czy da się w jakiś elegancki sposób dodać informację o nazwanych argumentach?
Chcę by kod był pythonic, nie chcę dużego boiler plate code (w ten sposób to potrafię to zrobić sam).

Minimalny Python 3.8.

2

@Dregorio

Wiadomość czy pytałeś o to chatGPT za 3, 2, 1

ChatGpt:

Tak, można dodać informacje o nazwanych argumentach do dekoratora. W Python 3.8 wprowadzono nowy argument pozycyjny * w definicji funkcji, który oznacza koniec argumentów pozycyjnych i początek argumentów nazwanych. Można go wykorzystać do rozdzielenia argumentów pozycyjnych od nazwanych i wypisania ich w odpowiedniej formie.

Oto poprawiona wersja dekoratora, która uwzględnia argumenty nazwane:

def log_argument_on_exception(func):
    """Decorator that logs the argument value if an exception is raised."""

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            args_repr = [repr(arg) for arg in args]
            kwargs_repr = [f"{key}={repr(value)}" for key, value in kwargs.items()]
            signature = ", ".join(args_repr + kwargs_repr)
            print(f"{func.__name__}({signature})")
            raise e

    return wrapper

W tej wersji dekoratora, argumenty pozycyjne i nazwane są reprezentowane przez args_repr i kwargs_repr odpowiednio. Następnie, łącząc te dwie listy, tworzony jest string signature, który zawiera pełną informację o argumentach.

Teraz, jeśli udekorowana funkcja wygląda tak:

@log_argument_on_exception
def magic(path, *, option=None):
    ...

i zostanie rzucany wyjątek, to informacja o path i option będzie wypisana w postaci:

magic('../Rules/Exclusions/Microsoft, Various services - [B1002].xml', option=None)

Dzięki temu kod jest bardziej uniwersalny i pokrywa oba rodzaje argumentów.

Nie podoba mi się to rozwiązanie, ale działa.
Ciekawy jestem, czy jest rozwiązanie, które jest bardziej Pythonic.

0

@MarekR22: Mnie zdziwiło, że to co Ty zaproponowałeś nie zadziałało z tym **kwargs

0

Najlepsze co udało mi się samemu napisać:

import functools
import itertools
 
def repr_kwargs(kwargs):
	return (f"{key}={repr(value)}" for key, value in kwargs.items())
 
def log_argument_on_exception(func):
    """Decorator that logs the argument value if an exception is raised."""
 
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            all_args = itertools.chain(map(repr, args), repr_kwargs(kwargs))
            print(f"{func.__name__}({', '.join(all_args)})")
            raise e
 
    return wrapper

https://ideone.com/lial2T

Róznica z wersją ChatGpt jest niewielka :( jest troszkę lepiej. Przynajmniej repr_kwargs może się przydać w innym miejscu.

0

Tak nawiasem pisząc, nie lepiej użyć logging i wysyłać logi do kibany, na przykład?

0

Nie wiem czy bardziej pythonicznie, ale z użyciem pprint, które ma różne opcje do formatowania wyniku.

import pprint

def foobar(*args,**kwargs):
    argz = pprint.pformat(f'{args=} and {kwargs=}')
    print(f'Function called with {argz}')


foobar(3,5,8,path='/a/b/c',blah='3435',option=None)

--
p.s. testowałeś tego wrappera na metodzie klasy?

0

Pomyślałbym nad użyciem inspect.getcallargs, które ładnie ci dopasuje argumenty do parametrów pozycyjnych, nazwanych i schowanych w argsach/kwargsach

import functools
import inspect


def log_argument_on_exception(func):
    """Decorator that logs the argument value if an exception is raised."""

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"{func.__name__}: {inspect.getcallargs(func, *args, **kwargs)}")

    return wrapper
>>> @log_argument_on_exception
... def foo(i, x, *args, **kwargs):
...     raise Exception()
... 
>>> foo(3, 5, 8, path='/a/b/c', blah='3435', option=None)
foo: {'i': 3, 'x': 5, 'args': (8,), 'kwargs': {'path': '/a/b/c', 'blah': '3435', 'option': None}}

1 użytkowników online, w tym zalogowanych: 0, gości: 1