mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
Merge remote-tracking branch 'remotes/jsnow-gitlab/tags/python-pull-request' into staging
Python Pull request # gpg: Signature made Mon 27 Sep 2021 20:24:39 BST # gpg: using RSA key F9B7ABDBBCACDF95BE76CBD07DEF8106AAFC390E # gpg: Good signature from "John Snow (John Huston) <jsnow@redhat.com>" [full] # Primary key fingerprint: FAEB 9711 A12C F475 812F 18F2 88A9 064D 1835 61EB # Subkey fingerprint: F9B7 ABDB BCAC DF95 BE76 CBD0 7DEF 8106 AAFC 390E * remotes/jsnow-gitlab/tags/python-pull-request: (32 commits) python/aqmp-tui: Add syntax highlighting python: add optional pygments dependency python: Add entry point for aqmp-tui python/aqmp-tui: Add AQMP TUI python: Add dependencies for AQMP TUI python/aqmp: Add Coverage.py support python/aqmp: add LineProtocol tests python/aqmp: add AsyncProtocol unit tests python: bump avocado to v90.0 python/aqmp: add scary message python/aqmp: add asyncio_run compatibility wrapper python/aqmp: add _raw() execution interface python/aqmp: add execute() interfaces python/aqmp: Add message routing to QMP protocol python/pylint: disable no-member check python/aqmp: add QMP protocol support python/pylint: disable too-many-function-args python/aqmp: add QMP event support python/aqmp: add well-known QMP object models python/aqmp: add QMP Message format ... Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
@@ -15,3 +15,8 @@ qemu.egg-info/
|
||||
.venv/
|
||||
.tox/
|
||||
.dev-venv/
|
||||
|
||||
# Coverage.py reports
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
|
||||
@@ -92,6 +92,13 @@ check:
|
||||
check-tox:
|
||||
@tox $(QEMU_TOX_EXTRA_ARGS)
|
||||
|
||||
.PHONY: check-coverage
|
||||
check-coverage:
|
||||
@coverage run -m avocado --config avocado.cfg run tests/*.py
|
||||
@coverage combine
|
||||
@coverage html
|
||||
@coverage report
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
python3 setup.py clean --all
|
||||
@@ -100,3 +107,5 @@ clean:
|
||||
.PHONY: distclean
|
||||
distclean: clean
|
||||
rm -rf qemu.egg-info/ .venv/ .tox/ $(QEMU_VENV_DIR) dist/
|
||||
rm -f .coverage .coverage.*
|
||||
rm -rf htmlcov/
|
||||
|
||||
Generated
+24
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "eff562a688ebc6f3ffe67494dbb804b883e2159ad81c4d55d96da9f7aec13e91"
|
||||
"sha256": "784b327272db32403d5a488507853b5afba850ba26a5948e5b6a90c1baef2d9c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -39,11 +39,11 @@
|
||||
},
|
||||
"avocado-framework": {
|
||||
"hashes": [
|
||||
"sha256:3fca7226d7d164f124af8a741e7fa658ff4345a0738ddc32907631fd688b38ed",
|
||||
"sha256:48ac254c0ae2ef0c0ceeb38e3d3df0388718eda8f48b3ab55b30b252839f42b1"
|
||||
"sha256:244cb569f8eb4e50a22ac82e1a2b2bba2458999f4281efbe2651bd415d59c65b",
|
||||
"sha256:6f15998b67ecd0e7dde790c4de4dd249d6df52dfe6d5cc4e2dd6596df51c3583"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==87.0"
|
||||
"version": "==90.0"
|
||||
},
|
||||
"distlib": {
|
||||
"hashes": [
|
||||
@@ -200,6 +200,14 @@
|
||||
],
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
|
||||
"sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.9.0"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:082a6d461b54f90eea49ca90fff4ee8b6e45e8029e5dbd72f6107ef84f3779c0",
|
||||
@@ -289,6 +297,18 @@
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.10.0.0"
|
||||
},
|
||||
"urwid": {
|
||||
"hashes": [
|
||||
"sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae"
|
||||
],
|
||||
"version": "==2.1.2"
|
||||
},
|
||||
"urwid-readline": {
|
||||
"hashes": [
|
||||
"sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4"
|
||||
],
|
||||
"version": "==0.13"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[run]
|
||||
test_runner = runner
|
||||
|
||||
[simpletests]
|
||||
# Don't show stdout/stderr in the test *summary*
|
||||
status.failure_fields = ['status']
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
QEMU Monitor Protocol (QMP) development library & tooling.
|
||||
|
||||
This package provides a fairly low-level class for communicating
|
||||
asynchronously with QMP protocol servers, as implemented by QEMU, the
|
||||
QEMU Guest Agent, and the QEMU Storage Daemon.
|
||||
|
||||
`QMPClient` provides the main functionality of this package. All errors
|
||||
raised by this library dervive from `AQMPError`, see `aqmp.error` for
|
||||
additional detail. See `aqmp.events` for an in-depth tutorial on
|
||||
managing QMP events.
|
||||
"""
|
||||
|
||||
# Copyright (C) 2020, 2021 John Snow for Red Hat, Inc.
|
||||
#
|
||||
# Authors:
|
||||
# John Snow <jsnow@redhat.com>
|
||||
#
|
||||
# Based on earlier work by Luiz Capitulino <lcapitulino@redhat.com>.
|
||||
#
|
||||
# This work is licensed under the terms of the GNU GPL, version 2. See
|
||||
# the COPYING file in the top-level directory.
|
||||
|
||||
import warnings
|
||||
|
||||
from .error import AQMPError
|
||||
from .events import EventListener
|
||||
from .message import Message
|
||||
from .protocol import ConnectError, Runstate, StateError
|
||||
from .qmp_client import ExecInterruptedError, ExecuteError, QMPClient
|
||||
|
||||
|
||||
_WMSG = """
|
||||
|
||||
The Asynchronous QMP library is currently in development and its API
|
||||
should be considered highly fluid and subject to change. It should
|
||||
not be used by any other scripts checked into the QEMU tree.
|
||||
|
||||
Proceed with caution!
|
||||
"""
|
||||
|
||||
warnings.warn(_WMSG, FutureWarning)
|
||||
|
||||
|
||||
# The order of these fields impact the Sphinx documentation order.
|
||||
__all__ = (
|
||||
# Classes, most to least important
|
||||
'QMPClient',
|
||||
'Message',
|
||||
'EventListener',
|
||||
'Runstate',
|
||||
|
||||
# Exceptions, most generic to most explicit
|
||||
'AQMPError',
|
||||
'StateError',
|
||||
'ConnectError',
|
||||
'ExecuteError',
|
||||
'ExecInterruptedError',
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
AQMP Error Classes
|
||||
|
||||
This package seeks to provide semantic error classes that are intended
|
||||
to be used directly by clients when they would like to handle particular
|
||||
semantic failures (e.g. "failed to connect") without needing to know the
|
||||
enumeration of possible reasons for that failure.
|
||||
|
||||
AQMPError serves as the ancestor for all exceptions raised by this
|
||||
package, and is suitable for use in handling semantic errors from this
|
||||
library. In most cases, individual public methods will attempt to catch
|
||||
and re-encapsulate various exceptions to provide a semantic
|
||||
error-handling interface.
|
||||
|
||||
.. admonition:: AQMP Exception Hierarchy Reference
|
||||
|
||||
| `Exception`
|
||||
| +-- `AQMPError`
|
||||
| +-- `ConnectError`
|
||||
| +-- `StateError`
|
||||
| +-- `ExecInterruptedError`
|
||||
| +-- `ExecuteError`
|
||||
| +-- `ListenerError`
|
||||
| +-- `ProtocolError`
|
||||
| +-- `DeserializationError`
|
||||
| +-- `UnexpectedTypeError`
|
||||
| +-- `ServerParseError`
|
||||
| +-- `BadReplyError`
|
||||
| +-- `GreetingError`
|
||||
| +-- `NegotiationError`
|
||||
"""
|
||||
|
||||
|
||||
class AQMPError(Exception):
|
||||
"""Abstract error class for all errors originating from this package."""
|
||||
|
||||
|
||||
class ProtocolError(AQMPError):
|
||||
"""
|
||||
Abstract error class for protocol failures.
|
||||
|
||||
Semantically, these errors are generally the fault of either the
|
||||
protocol server or as a result of a bug in this library.
|
||||
|
||||
:param error_message: Human-readable string describing the error.
|
||||
"""
|
||||
def __init__(self, error_message: str):
|
||||
super().__init__(error_message)
|
||||
#: Human-readable error message, without any prefix.
|
||||
self.error_message: str = error_message
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
QMP Message Format
|
||||
|
||||
This module provides the `Message` class, which represents a single QMP
|
||||
message sent to or from the server.
|
||||
"""
|
||||
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
from typing import (
|
||||
Dict,
|
||||
Iterator,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
|
||||
from .error import ProtocolError
|
||||
|
||||
|
||||
class Message(MutableMapping[str, object]):
|
||||
"""
|
||||
Represents a single QMP protocol message.
|
||||
|
||||
QMP uses JSON objects as its basic communicative unit; so this
|
||||
Python object is a :py:obj:`~collections.abc.MutableMapping`. It may
|
||||
be instantiated from either another mapping (like a `dict`), or from
|
||||
raw `bytes` that still need to be deserialized.
|
||||
|
||||
Once instantiated, it may be treated like any other MutableMapping::
|
||||
|
||||
>>> msg = Message(b'{"hello": "world"}')
|
||||
>>> assert msg['hello'] == 'world'
|
||||
>>> msg['id'] = 'foobar'
|
||||
>>> print(msg)
|
||||
{
|
||||
"hello": "world",
|
||||
"id": "foobar"
|
||||
}
|
||||
|
||||
It can be converted to `bytes`::
|
||||
|
||||
>>> msg = Message({"hello": "world"})
|
||||
>>> print(bytes(msg))
|
||||
b'{"hello":"world","id":"foobar"}'
|
||||
|
||||
Or back into a garden-variety `dict`::
|
||||
|
||||
>>> dict(msg)
|
||||
{'hello': 'world'}
|
||||
|
||||
|
||||
:param value: Initial value, if any.
|
||||
:param eager:
|
||||
When `True`, attempt to serialize or deserialize the initial value
|
||||
immediately, so that conversion exceptions are raised during
|
||||
the call to ``__init__()``.
|
||||
"""
|
||||
# pylint: disable=too-many-ancestors
|
||||
|
||||
def __init__(self,
|
||||
value: Union[bytes, Mapping[str, object]] = b'{}', *,
|
||||
eager: bool = True):
|
||||
self._data: Optional[bytes] = None
|
||||
self._obj: Optional[Dict[str, object]] = None
|
||||
|
||||
if isinstance(value, bytes):
|
||||
self._data = value
|
||||
if eager:
|
||||
self._obj = self._deserialize(self._data)
|
||||
else:
|
||||
self._obj = dict(value)
|
||||
if eager:
|
||||
self._data = self._serialize(self._obj)
|
||||
|
||||
# Methods necessary to implement the MutableMapping interface, see:
|
||||
# https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping
|
||||
|
||||
# We get pop, popitem, clear, update, setdefault, __contains__,
|
||||
# keys, items, values, get, __eq__ and __ne__ for free.
|
||||
|
||||
def __getitem__(self, key: str) -> object:
|
||||
return self._object[key]
|
||||
|
||||
def __setitem__(self, key: str, value: object) -> None:
|
||||
self._object[key] = value
|
||||
self._data = None
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self._object[key]
|
||||
self._data = None
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._object)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._object)
|
||||
|
||||
# Dunder methods not related to MutableMapping:
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self._obj is not None:
|
||||
return f"Message({self._object!r})"
|
||||
return f"Message({bytes(self)!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Pretty-printed representation of this QMP message."""
|
||||
return json.dumps(self._object, indent=2)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
"""bytes representing this QMP message."""
|
||||
if self._data is None:
|
||||
self._data = self._serialize(self._obj or {})
|
||||
return self._data
|
||||
|
||||
# Conversion Methods
|
||||
|
||||
@property
|
||||
def _object(self) -> Dict[str, object]:
|
||||
"""
|
||||
A `dict` representing this QMP message.
|
||||
|
||||
Generated on-demand, if required. This property is private
|
||||
because it returns an object that could be used to invalidate
|
||||
the internal state of the `Message` object.
|
||||
"""
|
||||
if self._obj is None:
|
||||
self._obj = self._deserialize(self._data or b'{}')
|
||||
return self._obj
|
||||
|
||||
@classmethod
|
||||
def _serialize(cls, value: object) -> bytes:
|
||||
"""
|
||||
Serialize a JSON object as `bytes`.
|
||||
|
||||
:raise ValueError: When the object cannot be serialized.
|
||||
:raise TypeError: When the object cannot be serialized.
|
||||
|
||||
:return: `bytes` ready to be sent over the wire.
|
||||
"""
|
||||
return json.dumps(value, separators=(',', ':')).encode('utf-8')
|
||||
|
||||
@classmethod
|
||||
def _deserialize(cls, data: bytes) -> Dict[str, object]:
|
||||
"""
|
||||
Deserialize JSON `bytes` into a native Python `dict`.
|
||||
|
||||
:raise DeserializationError:
|
||||
If JSON deserialization fails for any reason.
|
||||
:raise UnexpectedTypeError:
|
||||
If the data does not represent a JSON object.
|
||||
|
||||
:return: A `dict` representing this QMP message.
|
||||
"""
|
||||
try:
|
||||
obj = json.loads(data)
|
||||
except JSONDecodeError as err:
|
||||
emsg = "Failed to deserialize QMP message."
|
||||
raise DeserializationError(emsg, data) from err
|
||||
if not isinstance(obj, dict):
|
||||
raise UnexpectedTypeError(
|
||||
"QMP message is not a JSON object.",
|
||||
obj
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
class DeserializationError(ProtocolError):
|
||||
"""
|
||||
A QMP message was not understood as JSON.
|
||||
|
||||
When this Exception is raised, ``__cause__`` will be set to the
|
||||
`json.JSONDecodeError` Exception, which can be interrogated for
|
||||
further details.
|
||||
|
||||
:param error_message: Human-readable string describing the error.
|
||||
:param raw: The raw `bytes` that prompted the failure.
|
||||
"""
|
||||
def __init__(self, error_message: str, raw: bytes):
|
||||
super().__init__(error_message)
|
||||
#: The raw `bytes` that were not understood as JSON.
|
||||
self.raw: bytes = raw
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "\n".join([
|
||||
super().__str__(),
|
||||
f" raw bytes were: {str(self.raw)}",
|
||||
])
|
||||
|
||||
|
||||
class UnexpectedTypeError(ProtocolError):
|
||||
"""
|
||||
A QMP message was JSON, but not a JSON object.
|
||||
|
||||
:param error_message: Human-readable string describing the error.
|
||||
:param value: The deserialized JSON value that wasn't an object.
|
||||
"""
|
||||
def __init__(self, error_message: str, value: object):
|
||||
super().__init__(error_message)
|
||||
#: The JSON value that was expected to be an object.
|
||||
self.value: object = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
strval = json.dumps(self.value, indent=2)
|
||||
return "\n".join([
|
||||
super().__str__(),
|
||||
f" json value was: {strval}",
|
||||
])
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
QMP Data Models
|
||||
|
||||
This module provides simplistic data classes that represent the few
|
||||
structures that the QMP spec mandates; they are used to verify incoming
|
||||
data to make sure it conforms to spec.
|
||||
"""
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
from collections import abc
|
||||
from typing import (
|
||||
Any,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
|
||||
class Model:
|
||||
"""
|
||||
Abstract data model, representing some QMP object of some kind.
|
||||
|
||||
:param raw: The raw object to be validated.
|
||||
:raise KeyError: If any required fields are absent.
|
||||
:raise TypeError: If any required fields have the wrong type.
|
||||
"""
|
||||
def __init__(self, raw: Mapping[str, Any]):
|
||||
self._raw = raw
|
||||
|
||||
def _check_key(self, key: str) -> None:
|
||||
if key not in self._raw:
|
||||
raise KeyError(f"'{self._name}' object requires '{key}' member")
|
||||
|
||||
def _check_value(self, key: str, type_: type, typestr: str) -> None:
|
||||
assert key in self._raw
|
||||
if not isinstance(self._raw[key], type_):
|
||||
raise TypeError(
|
||||
f"'{self._name}' member '{key}' must be a {typestr}"
|
||||
)
|
||||
|
||||
def _check_member(self, key: str, type_: type, typestr: str) -> None:
|
||||
self._check_key(key)
|
||||
self._check_value(key, type_, typestr)
|
||||
|
||||
@property
|
||||
def _name(self) -> str:
|
||||
return type(self).__name__
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self._name}({self._raw!r})"
|
||||
|
||||
|
||||
class Greeting(Model):
|
||||
"""
|
||||
Defined in qmp-spec.txt, section 2.2, "Server Greeting".
|
||||
|
||||
:param raw: The raw Greeting object.
|
||||
:raise KeyError: If any required fields are absent.
|
||||
:raise TypeError: If any required fields have the wrong type.
|
||||
"""
|
||||
def __init__(self, raw: Mapping[str, Any]):
|
||||
super().__init__(raw)
|
||||
#: 'QMP' member
|
||||
self.QMP: QMPGreeting # pylint: disable=invalid-name
|
||||
|
||||
self._check_member('QMP', abc.Mapping, "JSON object")
|
||||
self.QMP = QMPGreeting(self._raw['QMP'])
|
||||
|
||||
|
||||
class QMPGreeting(Model):
|
||||
"""
|
||||
Defined in qmp-spec.txt, section 2.2, "Server Greeting".
|
||||
|
||||
:param raw: The raw QMPGreeting object.
|
||||
:raise KeyError: If any required fields are absent.
|
||||
:raise TypeError: If any required fields have the wrong type.
|
||||
"""
|
||||
def __init__(self, raw: Mapping[str, Any]):
|
||||
super().__init__(raw)
|
||||
#: 'version' member
|
||||
self.version: Mapping[str, object]
|
||||
#: 'capabilities' member
|
||||
self.capabilities: Sequence[object]
|
||||
|
||||
self._check_member('version', abc.Mapping, "JSON object")
|
||||
self.version = self._raw['version']
|
||||
|
||||
self._check_member('capabilities', abc.Sequence, "JSON array")
|
||||
self.capabilities = self._raw['capabilities']
|
||||
|
||||
|
||||
class ErrorResponse(Model):
|
||||
"""
|
||||
Defined in qmp-spec.txt, section 2.4.2, "error".
|
||||
|
||||
:param raw: The raw ErrorResponse object.
|
||||
:raise KeyError: If any required fields are absent.
|
||||
:raise TypeError: If any required fields have the wrong type.
|
||||
"""
|
||||
def __init__(self, raw: Mapping[str, Any]):
|
||||
super().__init__(raw)
|
||||
#: 'error' member
|
||||
self.error: ErrorInfo
|
||||
#: 'id' member
|
||||
self.id: Optional[object] = None # pylint: disable=invalid-name
|
||||
|
||||
self._check_member('error', abc.Mapping, "JSON object")
|
||||
self.error = ErrorInfo(self._raw['error'])
|
||||
|
||||
if 'id' in raw:
|
||||
self.id = raw['id']
|
||||
|
||||
|
||||
class ErrorInfo(Model):
|
||||
"""
|
||||
Defined in qmp-spec.txt, section 2.4.2, "error".
|
||||
|
||||
:param raw: The raw ErrorInfo object.
|
||||
:raise KeyError: If any required fields are absent.
|
||||
:raise TypeError: If any required fields have the wrong type.
|
||||
"""
|
||||
def __init__(self, raw: Mapping[str, Any]):
|
||||
super().__init__(raw)
|
||||
#: 'class' member, with an underscore to avoid conflicts in Python.
|
||||
self.class_: str
|
||||
#: 'desc' member
|
||||
self.desc: str
|
||||
|
||||
self._check_member('class', str, "string")
|
||||
self.class_ = self._raw['class']
|
||||
|
||||
self._check_member('desc', str, "string")
|
||||
self.desc = self._raw['desc']
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Miscellaneous Utilities
|
||||
|
||||
This module provides asyncio utilities and compatibility wrappers for
|
||||
Python 3.6 to provide some features that otherwise become available in
|
||||
Python 3.7+.
|
||||
|
||||
Various logging and debugging utilities are also provided, such as
|
||||
`exception_summary()` and `pretty_traceback()`, used primarily for
|
||||
adding information into the logging stream.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import traceback
|
||||
from typing import (
|
||||
Any,
|
||||
Coroutine,
|
||||
Optional,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# --------------------------
|
||||
# Section: Utility Functions
|
||||
# --------------------------
|
||||
|
||||
|
||||
async def flush(writer: asyncio.StreamWriter) -> None:
|
||||
"""
|
||||
Utility function to ensure a StreamWriter is *fully* drained.
|
||||
|
||||
`asyncio.StreamWriter.drain` only promises we will return to below
|
||||
the "high-water mark". This function ensures we flush the entire
|
||||
buffer -- by setting the high water mark to 0 and then calling
|
||||
drain. The flow control limits are restored after the call is
|
||||
completed.
|
||||
"""
|
||||
transport = cast(asyncio.WriteTransport, writer.transport)
|
||||
|
||||
# https://github.com/python/typeshed/issues/5779
|
||||
low, high = transport.get_write_buffer_limits() # type: ignore
|
||||
transport.set_write_buffer_limits(0, 0)
|
||||
try:
|
||||
await writer.drain()
|
||||
finally:
|
||||
transport.set_write_buffer_limits(high, low)
|
||||
|
||||
|
||||
def upper_half(func: T) -> T:
|
||||
"""
|
||||
Do-nothing decorator that annotates a method as an "upper-half" method.
|
||||
|
||||
These methods must not call bottom-half functions directly, but can
|
||||
schedule them to run.
|
||||
"""
|
||||
return func
|
||||
|
||||
|
||||
def bottom_half(func: T) -> T:
|
||||
"""
|
||||
Do-nothing decorator that annotates a method as a "bottom-half" method.
|
||||
|
||||
These methods must take great care to handle their own exceptions whenever
|
||||
possible. If they go unhandled, they will cause termination of the loop.
|
||||
|
||||
These methods do not, in general, have the ability to directly
|
||||
report information to a caller’s context and will usually be
|
||||
collected as a Task result instead.
|
||||
|
||||
They must not call upper-half functions directly.
|
||||
"""
|
||||
return func
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# Section: Compatibility Wrappers
|
||||
# -------------------------------
|
||||
|
||||
|
||||
def create_task(coro: Coroutine[Any, Any, T],
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> 'asyncio.Future[T]':
|
||||
"""
|
||||
Python 3.6-compatible `asyncio.create_task` wrapper.
|
||||
|
||||
:param coro: The coroutine to execute in a task.
|
||||
:param loop: Optionally, the loop to create the task in.
|
||||
|
||||
:return: An `asyncio.Future` object.
|
||||
"""
|
||||
if sys.version_info >= (3, 7):
|
||||
if loop is not None:
|
||||
return loop.create_task(coro)
|
||||
return asyncio.create_task(coro) # pylint: disable=no-member
|
||||
|
||||
# Python 3.6:
|
||||
return asyncio.ensure_future(coro, loop=loop)
|
||||
|
||||
|
||||
def is_closing(writer: asyncio.StreamWriter) -> bool:
|
||||
"""
|
||||
Python 3.6-compatible `asyncio.StreamWriter.is_closing` wrapper.
|
||||
|
||||
:param writer: The `asyncio.StreamWriter` object.
|
||||
:return: `True` if the writer is closing, or closed.
|
||||
"""
|
||||
if sys.version_info >= (3, 7):
|
||||
return writer.is_closing()
|
||||
|
||||
# Python 3.6:
|
||||
transport = writer.transport
|
||||
assert isinstance(transport, asyncio.WriteTransport)
|
||||
return transport.is_closing()
|
||||
|
||||
|
||||
async def wait_closed(writer: asyncio.StreamWriter) -> None:
|
||||
"""
|
||||
Python 3.6-compatible `asyncio.StreamWriter.wait_closed` wrapper.
|
||||
|
||||
:param writer: The `asyncio.StreamWriter` to wait on.
|
||||
"""
|
||||
if sys.version_info >= (3, 7):
|
||||
await writer.wait_closed()
|
||||
return
|
||||
|
||||
# Python 3.6
|
||||
transport = writer.transport
|
||||
assert isinstance(transport, asyncio.WriteTransport)
|
||||
|
||||
while not transport.is_closing():
|
||||
await asyncio.sleep(0)
|
||||
|
||||
# This is an ugly workaround, but it's the best I can come up with.
|
||||
sock = transport.get_extra_info('socket')
|
||||
|
||||
if sock is None:
|
||||
# Our transport doesn't have a socket? ...
|
||||
# Nothing we can reasonably do.
|
||||
return
|
||||
|
||||
while sock.fileno() != -1:
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
def asyncio_run(coro: Coroutine[Any, Any, T], *, debug: bool = False) -> T:
|
||||
"""
|
||||
Python 3.6-compatible `asyncio.run` wrapper.
|
||||
|
||||
:param coro: A coroutine to execute now.
|
||||
:return: The return value from the coroutine.
|
||||
"""
|
||||
if sys.version_info >= (3, 7):
|
||||
return asyncio.run(coro, debug=debug)
|
||||
|
||||
# Python 3.6
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_debug(debug)
|
||||
ret = loop.run_until_complete(coro)
|
||||
loop.close()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Section: Logging & Debugging
|
||||
# ----------------------------
|
||||
|
||||
|
||||
def exception_summary(exc: BaseException) -> str:
|
||||
"""
|
||||
Return a summary string of an arbitrary exception.
|
||||
|
||||
It will be of the form "ExceptionType: Error Message", if the error
|
||||
string is non-empty, and just "ExceptionType" otherwise.
|
||||
"""
|
||||
name = type(exc).__qualname__
|
||||
smod = type(exc).__module__
|
||||
if smod not in ("__main__", "builtins"):
|
||||
name = smod + '.' + name
|
||||
|
||||
error = str(exc)
|
||||
if error:
|
||||
return f"{name}: {error}"
|
||||
return name
|
||||
|
||||
|
||||
def pretty_traceback(prefix: str = " | ") -> str:
|
||||
"""
|
||||
Formats the current traceback, indented to provide visual distinction.
|
||||
|
||||
This is useful for printing a traceback within a traceback for
|
||||
debugging purposes when encapsulating errors to deliver them up the
|
||||
stack; when those errors are printed, this helps provide a nice
|
||||
visual grouping to quickly identify the parts of the error that
|
||||
belong to the inner exception.
|
||||
|
||||
:param prefix: The prefix to append to each line of the traceback.
|
||||
:return: A string, formatted something like the following::
|
||||
|
||||
| Traceback (most recent call last):
|
||||
| File "foobar.py", line 42, in arbitrary_example
|
||||
| foo.baz()
|
||||
| ArbitraryError: [Errno 42] Something bad happened!
|
||||
"""
|
||||
output = "".join(traceback.format_exception(*sys.exc_info()))
|
||||
|
||||
exc_lines = []
|
||||
for line in output.split('\n'):
|
||||
exc_lines.append(prefix + line)
|
||||
|
||||
# The last line is always empty, omit it
|
||||
return "\n".join(exc_lines[:-1])
|
||||
+41
-2
@@ -27,6 +27,7 @@ packages =
|
||||
qemu.qmp
|
||||
qemu.machine
|
||||
qemu.utils
|
||||
qemu.aqmp
|
||||
|
||||
[options.package_data]
|
||||
* = py.typed
|
||||
@@ -36,18 +37,27 @@ packages =
|
||||
# version, use e.g. "pipenv install --dev pylint==3.0.0".
|
||||
# Subsequently, edit 'Pipfile' to remove e.g. 'pylint = "==3.0.0'.
|
||||
devel =
|
||||
avocado-framework >= 87.0
|
||||
avocado-framework >= 90.0
|
||||
flake8 >= 3.6.0
|
||||
fusepy >= 2.0.4
|
||||
isort >= 5.1.2
|
||||
mypy >= 0.770
|
||||
pylint >= 2.8.0
|
||||
tox >= 3.18.0
|
||||
urwid >= 2.1.2
|
||||
urwid-readline >= 0.13
|
||||
Pygments >= 2.9.0
|
||||
|
||||
# Provides qom-fuse functionality
|
||||
fuse =
|
||||
fusepy >= 2.0.4
|
||||
|
||||
# AQMP TUI dependencies
|
||||
tui =
|
||||
urwid >= 2.1.2
|
||||
urwid-readline >= 0.13
|
||||
Pygments >= 2.9.0
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
qom = qemu.qmp.qom:main
|
||||
@@ -58,6 +68,7 @@ console_scripts =
|
||||
qom-fuse = qemu.qmp.qom_fuse:QOMFuse.entry_point [fuse]
|
||||
qemu-ga-client = qemu.qmp.qemu_ga_client:main
|
||||
qmp-shell = qemu.qmp.qmp_shell:main
|
||||
aqmp-tui = qemu.aqmp.aqmp_tui:main [tui]
|
||||
|
||||
[flake8]
|
||||
extend-ignore = E722 # Prefer pylint's bare-except checks to flake8's
|
||||
@@ -73,8 +84,22 @@ namespace_packages = True
|
||||
# fusepy has no type stubs:
|
||||
allow_subclassing_any = True
|
||||
|
||||
[mypy-qemu.aqmp.aqmp_tui]
|
||||
# urwid and urwid_readline have no type stubs:
|
||||
allow_subclassing_any = True
|
||||
|
||||
# The following missing import directives are because these libraries do not
|
||||
# provide type stubs. Allow them on an as-needed basis for mypy.
|
||||
[mypy-fuse]
|
||||
# fusepy has no type stubs:
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-urwid]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-urwid_readline]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pygments]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[pylint.messages control]
|
||||
@@ -88,6 +113,8 @@ ignore_missing_imports = True
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=consider-using-f-string,
|
||||
too-many-function-args, # mypy handles this with less false positives.
|
||||
no-member, # mypy also handles this better.
|
||||
|
||||
[pylint.basic]
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
@@ -100,6 +127,7 @@ good-names=i,
|
||||
fh, # fh = open(...)
|
||||
fd, # fd = os.open(...)
|
||||
c, # for c in string: ...
|
||||
T, # for TypeVars. See pylint#3401
|
||||
|
||||
[pylint.similarities]
|
||||
# Ignore imports when computing similarities.
|
||||
@@ -134,5 +162,16 @@ allowlist_externals = make
|
||||
deps =
|
||||
.[devel]
|
||||
.[fuse] # Workaround to trigger tox venv rebuild
|
||||
.[tui] # Workaround to trigger tox venv rebuild
|
||||
commands =
|
||||
make check
|
||||
|
||||
# Coverage.py [https://coverage.readthedocs.io/en/latest/] is a tool for
|
||||
# measuring code coverage of Python programs. It monitors your program,
|
||||
# noting which parts of the code have been executed, then analyzes the
|
||||
# source to identify code that could have been executed but was not.
|
||||
|
||||
[coverage:run]
|
||||
concurrency = multiprocessing
|
||||
source = qemu/
|
||||
parallel = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user