Декораторы для сопрограмм#

Декораторы для сопрограмм в целом создаются так же, как и декораторы для обычных функций. Основное отличие в том, что если декоратор должен подменять функцию (это делается в большинстве декораторов функций), надо чтобы декоратор подменял сопрограмму сопрограммой.

Пример декоратора timecode, который замеряет время выполнения сопрограммы:

import asyncio
from datetime import datetime
from functools import wraps
import yaml
from scrapli import AsyncScrapli
from scrapli.exceptions import ScrapliException


def timecode(function):
    @wraps(function)
    async def wrapper(*args, **kwargs):
        start_time = datetime.now()
        result = await function(*args, **kwargs)
        print(">>> Функция выполнялась:", datetime.now() - start_time)
        return result
    return wrapper


@timecode
async def send_show(device, command):
    try:
        async with AsyncScrapli(**device) as conn:
            result = await conn.send_command(command)
            await asyncio.sleep(2)
            return result.result
    except ScrapliException as error:
        print(error, device["host"])


if __name__ == "__main__":
    with open("devices_async.yaml") as f:
        devices = yaml.safe_load(f)
        r1 = devices[0]
    result = asyncio.run(send_show(r1, "sh clock"))
    print(result)

Для того чтобы замерить время выполнения send_show, надо дождаться выполнения сопрограммы - сделать await, а await можно писать только внутри сопрограммы, соответственно функция wrapper должна быть сопрограммой.

Аналогичный обычный декоратор:

def timecode(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        start_time = datetime.now()
        result = function(*args, **kwargs)
        print('>>> Функция выполнялась:', datetime.now() - start_time)
        return result
    return wrapper

Примечание

Измерение времени выполнения сопрограммы неоднозначное занятие, так как оно зависит не только от самой сопрограммы, но и от того что еще работает в цикле событий.

Встроенные декораторы#

Многие встроенные декораторы работают с функциями/методами, которые являются сопрограммами: functools.wraps, classmethod, staticmethod.

Примечание

Сопрограммы можно декорировать и property, но по смыслу property обычно применяется к чему-то, что выполняется быстро, поэтому стоит, как минимум, подумать о том надо ли делать property метод, который выполняет операции ввода-вывода.

Пример использования classmethod:

from pprint import pprint
import asyncio
import asyncssh


class ConnectAsyncSSH:

    @classmethod
    async def connect(cls, host, username, password, enable_password, connection_timeout=5):
        self = cls()

        self.host = host
        self.username = username
        self.password = password
        self.enable_password = enable_password
        self.connection_timeout = connection_timeout
        self._ssh = await asyncio.wait_for(
            asyncssh.connect(
                host=self.host,
                username=self.username,
                password=self.password,
                encryption_algs="+aes128-cbc,aes256-cbc",
            ),
            timeout=self.connection_timeout,
        )
        self.writer, self.reader, _ = await self._ssh.open_session(
            term_type="Dumb", term_size=(200, 24)
        )
        await self.reader.readuntil(">")
        self.writer.write("enable\n")
        await self.reader.readuntil("Password")
        self.writer.write(f"{self.enable_password}\n")
        await self.reader.readuntil("#")
        self.writer.write("terminal length 0\n")
        await self.reader.readuntil("#")
        return self

    async def get_prompt(self):
        self.writer.write("\n")
        output = await self.reader.readuntil("#")
        return output

Пример использования staticmethod:

import asyncio

class PingIP:
    def __init__(self, ip_list):
        self.ip_list = ip_list

    @staticmethod
    async def _ping(ip):
        reply = await asyncio.create_subprocess_shell(
            f"ping -c 3 -n {ip}",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await reply.communicate()
        ip_is_reachable = reply.returncode == 0
        return ip, ip_is_reachable

    async def scan(self):
        ping_ok = []
        ping_not_ok = []
        coroutines = [self._ping(ip) for ip in self.ip_list]
        result = await asyncio.gather(*coroutines)
        for ip, status in result:
            if status:
                ping_ok.append(ip)
            else:
                ping_not_ok.append(ip)
        return ping_ok, ping_not_ok


if __name__ == "__main__":
    ip_list = ["192.168.100.1", "192.168.100.2", "192.168.100.3", "192.168.100.11"]
    scanner = PingIP(ip_list)
    results = asyncio.run(scanner.scan())
    print(results)