mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +00:00
bpo-32363: Disable Task.set_exception() and Task.set_result() (#4923)
This commit is contained in:
parent
3dfbaf51f0
commit
0cf16f9ea0
7 changed files with 158 additions and 44 deletions
|
@ -239,14 +239,15 @@ class Future:
|
||||||
self._schedule_callbacks()
|
self._schedule_callbacks()
|
||||||
self._log_traceback = True
|
self._log_traceback = True
|
||||||
|
|
||||||
def __iter__(self):
|
def __await__(self):
|
||||||
if not self.done():
|
if not self.done():
|
||||||
self._asyncio_future_blocking = True
|
self._asyncio_future_blocking = True
|
||||||
yield self # This tells Task to wait for completion.
|
yield self # This tells Task to wait for completion.
|
||||||
assert self.done(), "await wasn't used with future"
|
if not self.done():
|
||||||
|
raise RuntimeError("await wasn't used with future")
|
||||||
return self.result() # May raise too.
|
return self.result() # May raise too.
|
||||||
|
|
||||||
__await__ = __iter__ # make compatible with 'await' expression
|
__iter__ = __await__ # make compatible with 'yield from'.
|
||||||
|
|
||||||
|
|
||||||
# Needed for testing purposes.
|
# Needed for testing purposes.
|
||||||
|
|
|
@ -37,7 +37,9 @@ def all_tasks(loop=None):
|
||||||
return {t for t in _all_tasks if futures._get_loop(t) is loop}
|
return {t for t in _all_tasks if futures._get_loop(t) is loop}
|
||||||
|
|
||||||
|
|
||||||
class Task(futures.Future):
|
class Task(futures._PyFuture): # Inherit Python Task implementation
|
||||||
|
# from a Python Future implementation.
|
||||||
|
|
||||||
"""A coroutine wrapped in a Future."""
|
"""A coroutine wrapped in a Future."""
|
||||||
|
|
||||||
# An important invariant maintained while a Task not done:
|
# An important invariant maintained while a Task not done:
|
||||||
|
@ -107,11 +109,17 @@ class Task(futures.Future):
|
||||||
if self._source_traceback:
|
if self._source_traceback:
|
||||||
context['source_traceback'] = self._source_traceback
|
context['source_traceback'] = self._source_traceback
|
||||||
self._loop.call_exception_handler(context)
|
self._loop.call_exception_handler(context)
|
||||||
futures.Future.__del__(self)
|
super().__del__()
|
||||||
|
|
||||||
def _repr_info(self):
|
def _repr_info(self):
|
||||||
return base_tasks._task_repr_info(self)
|
return base_tasks._task_repr_info(self)
|
||||||
|
|
||||||
|
def set_result(self, result):
|
||||||
|
raise RuntimeError('Task does not support set_result operation')
|
||||||
|
|
||||||
|
def set_exception(self, exception):
|
||||||
|
raise RuntimeError('Task does not support set_exception operation')
|
||||||
|
|
||||||
def get_stack(self, *, limit=None):
|
def get_stack(self, *, limit=None):
|
||||||
"""Return the list of stack frames for this task's coroutine.
|
"""Return the list of stack frames for this task's coroutine.
|
||||||
|
|
||||||
|
@ -180,7 +188,9 @@ class Task(futures.Future):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _step(self, exc=None):
|
def _step(self, exc=None):
|
||||||
assert not self.done(), f'_step(): already done: {self!r}, {exc!r}'
|
if self.done():
|
||||||
|
raise futures.InvalidStateError(
|
||||||
|
f'_step(): already done: {self!r}, {exc!r}')
|
||||||
if self._must_cancel:
|
if self._must_cancel:
|
||||||
if not isinstance(exc, futures.CancelledError):
|
if not isinstance(exc, futures.CancelledError):
|
||||||
exc = futures.CancelledError()
|
exc = futures.CancelledError()
|
||||||
|
@ -201,15 +211,15 @@ class Task(futures.Future):
|
||||||
if self._must_cancel:
|
if self._must_cancel:
|
||||||
# Task is cancelled right before coro stops.
|
# Task is cancelled right before coro stops.
|
||||||
self._must_cancel = False
|
self._must_cancel = False
|
||||||
self.set_exception(futures.CancelledError())
|
super().set_exception(futures.CancelledError())
|
||||||
else:
|
else:
|
||||||
self.set_result(exc.value)
|
super().set_result(exc.value)
|
||||||
except futures.CancelledError:
|
except futures.CancelledError:
|
||||||
super().cancel() # I.e., Future.cancel(self).
|
super().cancel() # I.e., Future.cancel(self).
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.set_exception(exc)
|
super().set_exception(exc)
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
self.set_exception(exc)
|
super().set_exception(exc)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
blocking = getattr(result, '_asyncio_future_blocking', None)
|
blocking = getattr(result, '_asyncio_future_blocking', None)
|
||||||
|
|
|
@ -370,7 +370,8 @@ class BaseFutureTests:
|
||||||
def test():
|
def test():
|
||||||
arg1, arg2 = coro()
|
arg1, arg2 = coro()
|
||||||
|
|
||||||
self.assertRaises(AssertionError, test)
|
with self.assertRaisesRegex(RuntimeError, "await wasn't used"):
|
||||||
|
test()
|
||||||
fut.cancel()
|
fut.cancel()
|
||||||
|
|
||||||
@mock.patch('asyncio.base_events.logger')
|
@mock.patch('asyncio.base_events.logger')
|
||||||
|
|
|
@ -1332,17 +1332,23 @@ class BaseTaskTests:
|
||||||
self.assertIsNone(task._fut_waiter)
|
self.assertIsNone(task._fut_waiter)
|
||||||
self.assertTrue(fut.cancelled())
|
self.assertTrue(fut.cancelled())
|
||||||
|
|
||||||
def test_step_in_completed_task(self):
|
def test_task_set_methods(self):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def notmuch():
|
def notmuch():
|
||||||
return 'ko'
|
return 'ko'
|
||||||
|
|
||||||
gen = notmuch()
|
gen = notmuch()
|
||||||
task = self.new_task(self.loop, gen)
|
task = self.new_task(self.loop, gen)
|
||||||
task.set_result('ok')
|
|
||||||
|
|
||||||
self.assertRaises(AssertionError, task._step)
|
with self.assertRaisesRegex(RuntimeError, 'not support set_result'):
|
||||||
gen.close()
|
task.set_result('ok')
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, 'not support set_exception'):
|
||||||
|
task.set_exception(ValueError())
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.loop.run_until_complete(task),
|
||||||
|
'ko')
|
||||||
|
|
||||||
def test_step_result(self):
|
def test_step_result(self):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -2231,10 +2237,59 @@ def add_subclass_tests(cls):
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
class SetMethodsTest:
|
||||||
|
|
||||||
|
def test_set_result_causes_invalid_state(self):
|
||||||
|
Future = type(self).Future
|
||||||
|
self.loop.call_exception_handler = exc_handler = mock.Mock()
|
||||||
|
|
||||||
|
async def foo():
|
||||||
|
await asyncio.sleep(0.1, loop=self.loop)
|
||||||
|
return 10
|
||||||
|
|
||||||
|
task = self.new_task(self.loop, foo())
|
||||||
|
Future.set_result(task, 'spam')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.loop.run_until_complete(task),
|
||||||
|
'spam')
|
||||||
|
|
||||||
|
exc_handler.assert_called_once()
|
||||||
|
exc = exc_handler.call_args[0][0]['exception']
|
||||||
|
with self.assertRaisesRegex(asyncio.InvalidStateError,
|
||||||
|
r'step\(\): already done'):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
def test_set_exception_causes_invalid_state(self):
|
||||||
|
class MyExc(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Future = type(self).Future
|
||||||
|
self.loop.call_exception_handler = exc_handler = mock.Mock()
|
||||||
|
|
||||||
|
async def foo():
|
||||||
|
await asyncio.sleep(0.1, loop=self.loop)
|
||||||
|
return 10
|
||||||
|
|
||||||
|
task = self.new_task(self.loop, foo())
|
||||||
|
Future.set_exception(task, MyExc())
|
||||||
|
|
||||||
|
with self.assertRaises(MyExc):
|
||||||
|
self.loop.run_until_complete(task)
|
||||||
|
|
||||||
|
exc_handler.assert_called_once()
|
||||||
|
exc = exc_handler.call_args[0][0]['exception']
|
||||||
|
with self.assertRaisesRegex(asyncio.InvalidStateError,
|
||||||
|
r'step\(\): already done'):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(hasattr(futures, '_CFuture') and
|
@unittest.skipUnless(hasattr(futures, '_CFuture') and
|
||||||
hasattr(tasks, '_CTask'),
|
hasattr(tasks, '_CTask'),
|
||||||
'requires the C _asyncio module')
|
'requires the C _asyncio module')
|
||||||
class CTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
|
||||||
|
test_utils.TestCase):
|
||||||
|
|
||||||
Task = getattr(tasks, '_CTask', None)
|
Task = getattr(tasks, '_CTask', None)
|
||||||
Future = getattr(futures, '_CFuture', None)
|
Future = getattr(futures, '_CFuture', None)
|
||||||
|
|
||||||
|
@ -2245,11 +2300,8 @@ class CTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
@add_subclass_tests
|
@add_subclass_tests
|
||||||
class CTask_CFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
|
class CTask_CFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
|
||||||
|
|
||||||
class Task(tasks._CTask):
|
Task = getattr(tasks, '_CTask', None)
|
||||||
pass
|
Future = getattr(futures, '_CFuture', None)
|
||||||
|
|
||||||
class Future(futures._CFuture):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(hasattr(tasks, '_CTask'),
|
@unittest.skipUnless(hasattr(tasks, '_CTask'),
|
||||||
|
@ -2257,9 +2309,7 @@ class CTask_CFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
|
||||||
@add_subclass_tests
|
@add_subclass_tests
|
||||||
class CTaskSubclass_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
class CTaskSubclass_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
|
|
||||||
class Task(tasks._CTask):
|
Task = getattr(tasks, '_CTask', None)
|
||||||
pass
|
|
||||||
|
|
||||||
Future = futures._PyFuture
|
Future = futures._PyFuture
|
||||||
|
|
||||||
|
|
||||||
|
@ -2268,15 +2318,14 @@ class CTaskSubclass_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
@add_subclass_tests
|
@add_subclass_tests
|
||||||
class PyTask_CFutureSubclass_Tests(BaseTaskTests, test_utils.TestCase):
|
class PyTask_CFutureSubclass_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
|
|
||||||
class Future(futures._CFuture):
|
Future = getattr(futures, '_CFuture', None)
|
||||||
pass
|
|
||||||
|
|
||||||
Task = tasks._PyTask
|
Task = tasks._PyTask
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(hasattr(tasks, '_CTask'),
|
@unittest.skipUnless(hasattr(tasks, '_CTask'),
|
||||||
'requires the C _asyncio module')
|
'requires the C _asyncio module')
|
||||||
class CTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
class CTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
|
|
||||||
Task = getattr(tasks, '_CTask', None)
|
Task = getattr(tasks, '_CTask', None)
|
||||||
Future = futures._PyFuture
|
Future = futures._PyFuture
|
||||||
|
|
||||||
|
@ -2284,22 +2333,22 @@ class CTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
@unittest.skipUnless(hasattr(futures, '_CFuture'),
|
@unittest.skipUnless(hasattr(futures, '_CFuture'),
|
||||||
'requires the C _asyncio module')
|
'requires the C _asyncio module')
|
||||||
class PyTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
class PyTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
||||||
|
|
||||||
Task = tasks._PyTask
|
Task = tasks._PyTask
|
||||||
Future = getattr(futures, '_CFuture', None)
|
Future = getattr(futures, '_CFuture', None)
|
||||||
|
|
||||||
|
|
||||||
class PyTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
|
class PyTask_PyFuture_Tests(BaseTaskTests, SetMethodsTest,
|
||||||
|
test_utils.TestCase):
|
||||||
|
|
||||||
Task = tasks._PyTask
|
Task = tasks._PyTask
|
||||||
Future = futures._PyFuture
|
Future = futures._PyFuture
|
||||||
|
|
||||||
|
|
||||||
@add_subclass_tests
|
@add_subclass_tests
|
||||||
class PyTask_PyFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
|
class PyTask_PyFuture_SubclassTests(BaseTaskTests, test_utils.TestCase):
|
||||||
class Task(tasks._PyTask):
|
Task = tasks._PyTask
|
||||||
pass
|
Future = futures._PyFuture
|
||||||
|
|
||||||
class Future(futures._PyFuture):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(hasattr(tasks, '_CTask'),
|
@unittest.skipUnless(hasattr(tasks, '_CTask'),
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Make asyncio.Task.set_exception() and set_result() raise
|
||||||
|
NotImplementedError. Task._step() and Future.__await__() raise proper
|
||||||
|
exceptions when they are in an invalid state, instead of raising an
|
||||||
|
AssertionError.
|
|
@ -779,7 +779,7 @@ _asyncio_Future_exception_impl(FutureObj *self)
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
_asyncio.Future.set_result
|
_asyncio.Future.set_result
|
||||||
|
|
||||||
res: object
|
result: object
|
||||||
/
|
/
|
||||||
|
|
||||||
Mark the future done and set its result.
|
Mark the future done and set its result.
|
||||||
|
@ -789,11 +789,11 @@ InvalidStateError.
|
||||||
[clinic start generated code]*/
|
[clinic start generated code]*/
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
_asyncio_Future_set_result(FutureObj *self, PyObject *res)
|
_asyncio_Future_set_result(FutureObj *self, PyObject *result)
|
||||||
/*[clinic end generated code: output=a620abfc2796bfb6 input=5b9dc180f1baa56d]*/
|
/*[clinic end generated code: output=1ec2e6bcccd6f2ce input=8b75172c2a7b05f1]*/
|
||||||
{
|
{
|
||||||
ENSURE_FUTURE_ALIVE(self)
|
ENSURE_FUTURE_ALIVE(self)
|
||||||
return future_set_result(self, res);
|
return future_set_result(self, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
|
@ -1468,8 +1468,8 @@ FutureIter_iternext(futureiterobject *it)
|
||||||
Py_INCREF(fut);
|
Py_INCREF(fut);
|
||||||
return (PyObject *)fut;
|
return (PyObject *)fut;
|
||||||
}
|
}
|
||||||
PyErr_SetString(PyExc_AssertionError,
|
PyErr_SetString(PyExc_RuntimeError,
|
||||||
"yield from wasn't used with future");
|
"await wasn't used with future");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2232,6 +2232,39 @@ _asyncio_Task__wakeup_impl(TaskObj *self, PyObject *fut)
|
||||||
return task_wakeup(self, fut);
|
return task_wakeup(self, fut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
_asyncio.Task.set_result
|
||||||
|
|
||||||
|
result: object
|
||||||
|
/
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
_asyncio_Task_set_result(TaskObj *self, PyObject *result)
|
||||||
|
/*[clinic end generated code: output=1dcae308bfcba318 input=9d1a00c07be41bab]*/
|
||||||
|
{
|
||||||
|
PyErr_SetString(PyExc_RuntimeError,
|
||||||
|
"Task does not support set_result operation");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*[clinic input]
|
||||||
|
_asyncio.Task.set_exception
|
||||||
|
|
||||||
|
exception: object
|
||||||
|
/
|
||||||
|
[clinic start generated code]*/
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
_asyncio_Task_set_exception(TaskObj *self, PyObject *exception)
|
||||||
|
/*[clinic end generated code: output=bc377fc28067303d input=9a8f65c83dcf893a]*/
|
||||||
|
{
|
||||||
|
PyErr_SetString(PyExc_RuntimeError,
|
||||||
|
"Task doed not support set_exception operation");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
TaskObj_finalize(TaskObj *task)
|
TaskObj_finalize(TaskObj *task)
|
||||||
{
|
{
|
||||||
|
@ -2304,12 +2337,12 @@ static void TaskObj_dealloc(PyObject *); /* Needs Task_CheckExact */
|
||||||
static PyMethodDef TaskType_methods[] = {
|
static PyMethodDef TaskType_methods[] = {
|
||||||
_ASYNCIO_FUTURE_RESULT_METHODDEF
|
_ASYNCIO_FUTURE_RESULT_METHODDEF
|
||||||
_ASYNCIO_FUTURE_EXCEPTION_METHODDEF
|
_ASYNCIO_FUTURE_EXCEPTION_METHODDEF
|
||||||
_ASYNCIO_FUTURE_SET_RESULT_METHODDEF
|
|
||||||
_ASYNCIO_FUTURE_SET_EXCEPTION_METHODDEF
|
|
||||||
_ASYNCIO_FUTURE_ADD_DONE_CALLBACK_METHODDEF
|
_ASYNCIO_FUTURE_ADD_DONE_CALLBACK_METHODDEF
|
||||||
_ASYNCIO_FUTURE_REMOVE_DONE_CALLBACK_METHODDEF
|
_ASYNCIO_FUTURE_REMOVE_DONE_CALLBACK_METHODDEF
|
||||||
_ASYNCIO_FUTURE_CANCELLED_METHODDEF
|
_ASYNCIO_FUTURE_CANCELLED_METHODDEF
|
||||||
_ASYNCIO_FUTURE_DONE_METHODDEF
|
_ASYNCIO_FUTURE_DONE_METHODDEF
|
||||||
|
_ASYNCIO_TASK_SET_RESULT_METHODDEF
|
||||||
|
_ASYNCIO_TASK_SET_EXCEPTION_METHODDEF
|
||||||
_ASYNCIO_TASK_CURRENT_TASK_METHODDEF
|
_ASYNCIO_TASK_CURRENT_TASK_METHODDEF
|
||||||
_ASYNCIO_TASK_ALL_TASKS_METHODDEF
|
_ASYNCIO_TASK_ALL_TASKS_METHODDEF
|
||||||
_ASYNCIO_TASK_CANCEL_METHODDEF
|
_ASYNCIO_TASK_CANCEL_METHODDEF
|
||||||
|
@ -2461,7 +2494,7 @@ task_step_impl(TaskObj *task, PyObject *exc)
|
||||||
PyObject *o;
|
PyObject *o;
|
||||||
|
|
||||||
if (task->task_state != STATE_PENDING) {
|
if (task->task_state != STATE_PENDING) {
|
||||||
PyErr_Format(PyExc_AssertionError,
|
PyErr_Format(asyncio_InvalidStateError,
|
||||||
"_step(): already done: %R %R",
|
"_step(): already done: %R %R",
|
||||||
task,
|
task,
|
||||||
exc ? exc : Py_None);
|
exc ? exc : Py_None);
|
||||||
|
|
|
@ -86,7 +86,7 @@ _asyncio_Future_exception(FutureObj *self, PyObject *Py_UNUSED(ignored))
|
||||||
}
|
}
|
||||||
|
|
||||||
PyDoc_STRVAR(_asyncio_Future_set_result__doc__,
|
PyDoc_STRVAR(_asyncio_Future_set_result__doc__,
|
||||||
"set_result($self, res, /)\n"
|
"set_result($self, result, /)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Mark the future done and set its result.\n"
|
"Mark the future done and set its result.\n"
|
||||||
|
@ -536,6 +536,22 @@ exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyDoc_STRVAR(_asyncio_Task_set_result__doc__,
|
||||||
|
"set_result($self, result, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n");
|
||||||
|
|
||||||
|
#define _ASYNCIO_TASK_SET_RESULT_METHODDEF \
|
||||||
|
{"set_result", (PyCFunction)_asyncio_Task_set_result, METH_O, _asyncio_Task_set_result__doc__},
|
||||||
|
|
||||||
|
PyDoc_STRVAR(_asyncio_Task_set_exception__doc__,
|
||||||
|
"set_exception($self, exception, /)\n"
|
||||||
|
"--\n"
|
||||||
|
"\n");
|
||||||
|
|
||||||
|
#define _ASYNCIO_TASK_SET_EXCEPTION_METHODDEF \
|
||||||
|
{"set_exception", (PyCFunction)_asyncio_Task_set_exception, METH_O, _asyncio_Task_set_exception__doc__},
|
||||||
|
|
||||||
PyDoc_STRVAR(_asyncio__get_running_loop__doc__,
|
PyDoc_STRVAR(_asyncio__get_running_loop__doc__,
|
||||||
"_get_running_loop($module, /)\n"
|
"_get_running_loop($module, /)\n"
|
||||||
"--\n"
|
"--\n"
|
||||||
|
@ -747,4 +763,4 @@ _asyncio__leave_task(PyObject *module, PyObject *const *args, Py_ssize_t nargs,
|
||||||
exit:
|
exit:
|
||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
/*[clinic end generated code: output=5d100b3d74f2a0f4 input=a9049054013a1b77]*/
|
/*[clinic end generated code: output=616e814431893dcc input=a9049054013a1b77]*/
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue