Commit 39ffc09b authored by Jason Madden's avatar Jason Madden

Add a 1.1 what's new page. Some doc love for the intro. [skip ci]

parent f72d7546
......@@ -48,6 +48,9 @@ extlinks = {'issue': ('https://github.com/gevent/gevent/issues/%s',
'pr': ('https://github.com/gevent/gevent/pull/%s',
'pull request #')}
autodoc_default_flags = ['members', 'show-inheritance']
autoclass_content = 'both'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
......@@ -99,14 +102,14 @@ add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = 'perldoc'
# A list of ignored prefixes for module index sorting.
modindex_common_prefix = ['gevent.']
......@@ -252,4 +255,4 @@ class MyClassDocumenter(ClassDocumenter):
members.sort(key=key)
return members_check_module, members
autodoc.ClassDocumenter = MyClassDocumenter
#autodoc.ClassDocumenter = MyClassDocumenter
......@@ -4,8 +4,9 @@ Table Of Contents
.. toctree::
intro
whatsnew_1_0
whatsnew_1_1
reference
whatsnew_1_0
changelog
* :ref:`genindex`
......
......@@ -5,9 +5,10 @@ gevent is a coroutine-based Python networking library.
Features include:
* Fast event loop based on libev (epoll on Linux, kqueue on FreeBSD).
* Fast event loop based on libev (epoll on Linux, kqueue on FreeBSD,
select on Mac OS X).
* Lightweight execution units based on greenlet.
* API that re-uses concepts from the Python standard library (e.g. :class:`Event`, :class:`Queue`).
* API that re-uses concepts from the Python standard library (e.g. :class:`gevent.event.Event`, :class:`gevent.queue.Queue`).
* Cooperative :mod:`socket` and :mod:`ssl` modules.
* Ability to use standard library and 3rd party modules written for standard blocking sockets (:mod:`gevent.monkey`).
* DNS queries performed through threadpool (default) or through c-ares (enabled via GEVENT_RESOLVER=ares env var).
......@@ -20,14 +21,14 @@ Features include:
Installation
------------
gevent runs on Python 2.6 and newer and requires
gevent runs on Python 2.6, 2.7, 3.3 and 3.4 and requires
* greenlet__ which can be installed with ``pip install greenlet``.
For ssl to work on Python older than 2.6, ssl_ package is required.
gevent also runs on PyPy 2.5.0 and 2.6.0, although 2.7.0 is
recommended. On PyPy, there are no external dependencies.
__ http://pypi.python.org/pypi/greenlet
.. _ssl: http://pypi.python.org/pypi/ssl
Example
......@@ -43,11 +44,13 @@ The following example shows how to run tasks concurrently.
>>> [job.value for job in jobs]
['74.125.79.106', '208.77.188.166', '82.94.164.162']
After the jobs have been spawned, :func:`gevent.joinall` waits for them to complete,
no longer than 2 seconds though. The results are then collected by checking
:attr:`gevent.Greenlet.value` property. The :func:`gevent.socket.gethostbyname` function
has the same interface as the standard :func:`socket.gethostbyname` but it does not block
the whole interpreter and thus lets the other greenlets proceed with their requests unhindered.
After the jobs have been spawned, :func:`gevent.joinall` waits for
them to complete, allowing up to 2 seconds. The results are
then collected by checking the :attr:`gevent.Greenlet.value` property.
The :func:`gevent.socket.gethostbyname` function has the same
interface as the standard :func:`socket.gethostbyname` but it does not
block the whole interpreter and thus lets the other greenlets proceed
with their requests unhindered.
.. _monkey-patching:
......@@ -57,8 +60,8 @@ Monkey patching
The example above used :mod:`gevent.socket` for socket operations. If the standard :mod:`socket`
module was used the example would have taken 3 times longer to complete because the DNS requests would
be sequential. Using the standard socket module inside greenlets makes gevent rather
pointless, so what about module and packages that are built on top of :mod:`socket`?
be sequential (serialized). Using the standard socket module inside greenlets makes gevent rather
pointless, so what about modules and packages that are built on top of :mod:`socket`?
That's what monkey patching is for. The functions in :mod:`gevent.monkey` carefully
replace functions and classes in the standard :mod:`socket` module with their cooperative
......@@ -70,6 +73,10 @@ in a multi-greenlet environment.
See `examples/concurrent_download.py`__
.. note:: When monkey patching, it is recommended to do so as early as
possible in the lifetime of the process. If possible,
monkey patching should be the first lines executed.
__ https://github.com/gevent/gevent/blob/master/examples/concurrent_download.py#L1
Event loop
......@@ -78,42 +85,59 @@ Event loop
Unlike other network libraries, in similar fashion to eventlet, gevent starts
the event loop implicitly in a dedicated greenlet. There's no ``reactor`` that
you must call a ``run()`` or ``dispatch()`` function on. When a function from
gevent's API wants to block, it obtains the :class:`Hub` instance - a greenlet
that runs the event loop - and switches to it. If there's no :class:`Hub`
gevent's API wants to block, it obtains the :class:`gevent.hub.Hub` instance --- a greenlet
that runs the event loop --- and switches to it. If there's no :class:`gevent.hub.Hub`
instance yet, one is created on the fly.
The event loop provided by libev uses the fastest polling mechanism
available on the system by default. It is possible to command libev to
use a particular polling mechanism by setting the ``LIBEV_FLAGS``
environment variable. Possible values include ``LIBEV_FLAGS=1`` for
the select backend, ``LIBEV_FLAGS=2`` for the poll backend,
``LIBEV_FLAGS=4`` for the epoll backend and ``LIBEV_FLAGS=8`` for the
kqueue backend. Please read the `libev documentation`_ for more
available on the system by default. Please read the `libev documentation`_ for more
information.
.. As of 1.1 or before, we set the EVFLAG_NOENV so this isn't possible any more.
It is possible to command libev to
use a particular polling mechanism by setting the ``LIBEV_FLAGS``
environment variable. Possible values include ``LIBEV_FLAGS=1`` for
the select backend, ``LIBEV_FLAGS=2`` for the poll backend,
``LIBEV_FLAGS=4`` for the epoll backend and ``LIBEV_FLAGS=8`` for the
kqueue backend.
.. _`libev documentation`: http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#FUNCTIONS_CONTROLLING_EVENT_LOOPS
The Libev API is available under :mod:`gevent.core` module. Note, that
the callbacks supplied to the libev API are run in the :class:`Hub`
the callbacks supplied to the libev API are run in the :class:`gevent.hub.Hub`
greenlet and thus cannot use the synchronous gevent API. It is possible to
use the asynchronous API there, like :func:`spawn` and :meth:`Event.set`.
use the asynchronous API there, like :func:`gevent.spawn` and
:meth:`gevent.event.Event.set`.
Cooperative multitasking
------------------------
The greenlets all run in the same OS thread and are scheduled cooperatively. This means that until
a particular greenlet gives up control, (by calling a blocking function that will switch to the :class:`Hub`), other greenlets
won't get a chance to run. It is typically not an issue for an I/O bound app, but one should be aware
of this when doing something CPU intensive, or when calling blocking I/O functions that bypass the libev event loop.
.. currentmodule:: gevent
The greenlets all run in the same OS thread and are scheduled
cooperatively. This means that until a particular greenlet gives up
control, (by calling a blocking function that will switch to the
:class:`gevent.hub.Hub`), other greenlets won't get a chance to run. This is
typically not an issue for an I/O bound app, but one should be aware
of this when doing something CPU intensive, or when calling blocking
I/O functions that bypass the libev event loop.
.. tip:: Even some apparently cooperative functions, like
:func:`gevent.sleep`, can temporarily take priority over
waiting I/O operations in some circumstances.
Synchronizing access to objects shared across the greenlets is unnecessary in most cases, thus
:class:`Lock` and :class:`Semaphore` classes, although present, aren't used very often. Other abstractions
from threading and multiprocessing remain useful in the cooperative world:
Synchronizing access to objects shared across the greenlets is
unnecessary in most cases (because yielding control is usually
explict), thus :class:`lock.BoundedSemaphore`, :class:`lock.RLock` and
:class:`lock.Semaphore` classes, although present, aren't used very
often. Other abstractions from threading and multiprocessing remain
useful in the cooperative world:
- :class:`Event` allows one to wake up a number of greenlets that are calling :meth:`Event.wait` method.
- :class:`AsyncResult` is similar to :class:`Event` but allows passing a value or an exception to the waiters.
- :class:`Queue` and :class:`JoinableQueue`.
- :class:`event.Event` allows one to wake up a number of greenlets that are calling :meth:`event.Event.wait` method.
- :class:`event.AsyncResult` is similar to :class:`event.Event` but allows passing a value or an exception to the waiters.
- :class:`queue.Queue` and :class:`queue.JoinableQueue`.
Lightweight pseudothreads
......@@ -121,13 +145,13 @@ Lightweight pseudothreads
.. currentmodule:: gevent.greenlet
The greenlets are spawned by creating a :class:`Greenlet` instance and calling its :meth:`start <Greenlet.start>`
method. (The :func:`spawn` function is a shortcut that does exactly that). The :meth:`start <Greenlet.start>`
The greenlets are spawned by creating a :class:`gevent.Greenlet` instance and calling its :meth:`start <gevent.Greenlet.start>`
method. (The :func:`gevent.spawn` function is a shortcut that does exactly that). The :meth:`start <gevent.Greenlet.start>`
method schedules a switch to the greenlet that will happen as soon as the current greenlet gives up control.
If there is more than one active event, they will be executed one by one, in an undefined order.
If there is more than one active greenlet, they will be executed one by one, in an undefined order.
If there is an error during execution it won't escape greenlet's boundaries. An unhandled error results
in a stacktrace being printed, complemented by the failed function's signature and arguments:
If there is an error during execution it won't escape the greenlet's boundaries. An unhandled error results
in a stacktrace being printed, annotated by the failed function's signature and arguments:
>>> gevent.spawn(lambda : 1/0)
>>> gevent.sleep(1)
......@@ -140,14 +164,16 @@ The traceback is asynchronously printed to ``sys.stderr`` when the greenlet dies
:class:`Greenlet` instances have a number of useful methods:
- :meth:`join <Greenlet.join>` -- waits until the greenlet exits;
- :meth:`kill <Greenlet.kill>` -- interrupts greenlet's execution;
- :meth:`get <Greenlet.get>` -- returns the value returned by greenlet or re-raised the exception that killed it.
- :meth:`join <gevent.Greenlet.join>` -- waits until the greenlet exits;
- :meth:`kill <gevent.Greenlet.kill>` -- interrupts greenlet's execution;
- :meth:`get <gevent.Greenlet.get>` -- returns the value returned by greenlet or re-raised the exception that killed it.
It is possible to customize the string printed after the traceback by subclassing the :class:`Greenlet` class
It is possible to customize the string printed after the traceback by subclassing the :class:`gevent.Greenlet` class
and redefining its ``__str__`` method.
To subclass a :class:`Greenlet`, override its :meth:`_run` method and call ``Greenlet.__init__(self)`` in ``__init__``::
To subclass a :class:`gevent.Greenlet`, override its
:meth:`gevent.Greenlet._run` method and call
``Greenlet.__init__(self)`` in ``__init__``::
class MyNoopGreenlet(Greenlet):
......@@ -162,7 +188,7 @@ To subclass a :class:`Greenlet`, override its :meth:`_run` method and call ``Gre
return 'MyNoopGreenlet(%s)' % self.seconds
Greenlets can be killed asynchronously. Killing will resume the sleeping greenlet, but instead
of continuing execution, a :exc:`GreenletExit` will be raised.
of continuing execution, a :exc:`gevent.greenlet.GreenletExit` will be raised.
>>> g = MyNoopGreenlet(4)
>>> g.start()
......@@ -170,11 +196,11 @@ of continuing execution, a :exc:`GreenletExit` will be raised.
>>> g.dead
True
The :exc:`GreenletExit` exception and its subclasses are handled differently than other exceptions.
Raising :exc:`GreenletExit` is not considered an exceptional situation, so the traceback is not printed.
The :exc:`GreenletExit` is returned by :meth:`get <Greenlet.get>` as if it were returned by the greenlet, not raised.
The :exc:`gevent.greenlet.GreenletExit` exception and its subclasses are handled differently than other exceptions.
Raising :exc:`gevent.greenlet.GreenletExit` is not considered an exceptional situation, so the traceback is not printed.
The :exc:`gevent.greenlet.GreenletExit` is returned by :meth:`get <gevent.Greenlet.get>` as if it were returned by the greenlet, not raised.
The :meth:`kill <Greenlet.kill>` method can accept a custom exception to be raised:
The :meth:`kill <gevent.Greenlet.kill>` method can accept a custom exception to be raised:
>>> g = MyNoopGreenlet.spawn(5) # spawn() creates a Greenlet and starts it
>>> g.kill(Exception("A time to kill"))
......@@ -183,30 +209,46 @@ The :meth:`kill <Greenlet.kill>` method can accept a custom exception to be rais
Exception: A time to kill
MyNoopGreenlet(5) failed with Exception
The :meth:`kill <Greenlet.kill>` can also accept a *timeout* argument specifying the number of seconds to wait for the greenlet to exit.
Note, that :meth:`kill <Greenlet.kill>` cannot guarantee that the target greenlet will not ignore the exception, thus it's a good idea always to pass a timeout to :meth:`kill <Greenlet.kill>`.
The :meth:`kill <gevent.Greenlet.kill>` can also accept a *timeout*
argument specifying the number of seconds to wait for the greenlet to
exit. Note, that :meth:`kill <gevent.Greenlet.kill>` cannot guarantee
that the target greenlet will not ignore the exception (i.e., it might
catch it), thus it's a good idea always to pass a timeout to
:meth:`kill <gevent.Greenlet.kill>`.
.. tip:: The exact timing at which an exception is raised within a
target greenlet as the result of :meth:`kill
<gevent.Greenlet.kill>` is not defined. See that function's
documentation for more details.
Timeouts
--------
Many functions in the gevent API are synchronous, blocking the current greenlet until the operation is done. For example,
:meth:`kill <Greenlet.kill>` waits until the target greenlet is :attr:`dead` before returning [#f1]_. Many of those
functions can be made asynchronous by passing the argument ``block=False``.
Many functions in the gevent API are synchronous, blocking the current
greenlet until the operation is done. For example, :meth:`kill
<gevent.Greenlet.kill>` waits until the target greenlet is
:attr:`gevent.greenlet.Greenlet.dead` before returning [#f1]_. Many of
those functions can be made asynchronous by passing the argument
``block=False``.
Furthermore, many of the synchronous functions accept a *timeout* argument, which specifies a limit on how long the function
can block (examples: :meth:`Event.wait`, :meth:`Greenlet.join`, :meth:`Greenlet.kill`, :meth:`AsyncResult.get`, and many more).
Furthermore, many of the synchronous functions accept a *timeout*
argument, which specifies a limit on how long the function can block
(examples: :meth:`gevent.event.Event.wait`,
:meth:`gevent.Greenlet.join`, :meth:`gevent.Greenlet.kill`,
:meth:`gevent.event.AsyncResult.get`, and many more).
The :class:`socket <gevent.socket.socket>` and :class:`SSLObject <gevent.ssl.SSLObject>` instances can also have a timeout,
set by the :meth:`settimeout <gevent.socket.socket.settimeout>` method.
The :class:`socket <gevent.socket.socket>` and :class:`SSLObject
<gevent.ssl.SSLObject>` instances can also have a timeout, set by the
:meth:`settimeout <gevent.socket.socket.settimeout>` method.
When these are not enough, the :class:`Timeout` class can be used to add timeouts to arbitrary sections of (yielding) code.
When these are not enough, the :class:`gevent.timeout.Timeout` class can be used to
add timeouts to arbitrary sections of (cooperative, yielding) code.
Futher reading
--------------
To limit concurrency, use the :class:`Pool` class (see `example: dns_mass_resolve.py`_).
To limit concurrency, use the :class:`gevent.pool.Pool` class (see `example: dns_mass_resolve.py`_).
Gevent comes with TCP/SSL/HTTP/WSGI servers. See :doc:`servers`.
......
What's new gevent 1.0
---------------------
==========================
What's new in gevent 1.0
==========================
The detailed information is available in changelog. Below is the summary of all changes since 0.13.8.
Gevent 1.0 supports Python 2.5 - 2.7. The version of greenlet required is 0.3.2. The source distribution
now includes the dependencies (libev and c-ares) and has not dependencies other than greenlet.
now includes the dependencies (libev and c-ares) and has no dependencies other than greenlet.
New core
~~~~~~~~
========
New event loop is used libev instead of libevent (see http://blog.gevent.org/2011/04/28/libev-and-libevent/ for motivation).
Now the event loop is using libev instead of libevent (see http://blog.gevent.org/2011/04/28/libev-and-libevent/ for motivation).
The new :mod:`gevent.core` has been rewritten to wrap libev's API. (On Windows, the :mod:`gevent.core` accepts Windows handles
rather than stdio file descriptors.).
......@@ -26,7 +27,7 @@ Thus ``sys.exit()`` when run inside a greenlet is no longer trapped and kills th
New dns resolver
~~~~~~~~~~~~~~~~
================
Two new DNS resolvers: threadpool-based one (enabled by default) and c-ares based one. That threadpool-based resolver was added mostly for Windows and Mac OS X platforms where c-ares might behave differently w.r.t system configuration. On Linux, however, the c-ares based resolver is probably a better choice. To enable c-ares resolver set GEVENT_RESOLVER=ares environment variable.
......@@ -46,7 +47,7 @@ It is possible to implement your own DNS resolver and make gevent use it. The GE
New API
~~~~~~~
=======
- :func:`gevent.wait` and :func:`gevent.iwait`
- UDP server: gevent.server.DatagramServer
......@@ -66,10 +67,10 @@ New API
Breaking changes
~~~~~~~~~~~~~~~~
================
Removed features
^^^^^^^^^^^^^^^^
----------------
- gevent.dns module (wrapper around libevent-dns)
- gevent.http module (wrapper around libevent-http)
......@@ -84,20 +85,20 @@ Renamed gevent.coros to gevent.lock. The gevent.coros is still available but dep
API changes
^^^^^^^^^^^
-----------
In all servers, method "kill" was renamed to "close". The old name is available as deprecated alias.
- ``Queue(0)`` is now equivalent to an unbound queue and raises :exc:`DeprecationError`. Use :class:`gevent.queue.Channel` if you need a channel.
The :class:`Greenlet` objects:
The :class:`gevent.Greenlet` objects:
- Added ``__nonzero__`` implementation that returns `True` after greenlet was started until it's dead. This overrides
greenlet's __nonzero__ which returned `False` after `start()` until it was first switched to.
Bugfixes
~~~~~~~~
========
- Issue #302: "python -m gevent.monkey" now sets __file__ properly.
- Issue #143: greenlet links are now executed in the order they were added
......
==========================
What's new in gevent 1.1
==========================
Detailed information an what has changed is avaialble in the
:doc:`changelog`. This document summarizes the most important changes
since gevent 1.0.3.
Platform Support
================
gevent 1.1 support Python 2.6, 2.7, 3.3, and 3.4 on the CPython (python.org)
interpreter. It also supports PyPy 2.5.0 and above (with best results
being obtained on PyPy 2.7.0 and above); PyPy3 is not supported.
Support for Python 2.5 was removed when support for Python 3 was
added. Any further releases in the 1.0.x line will maintain support
for Python 2.5.
Improved subprocess support
===========================
In gevent 1.0, support and monkey patching for the ``subprocess``
module was added. Monkey patching was off by default.
In 1.1, monkey patching subprocess is on by default due to
improvements in handling child processes and requirements by
downstream libraries, notably `gunicorn`_.
- :func:`gevent.os.fork`, which is monkey patched by default (and
should be used to fork a gevent-aware process that expects to use
gevent in the child process) has been improved and cooperates with
:func:`gevent.os.waitpid` (again monkey patched by default).
- fork-watchers will be called, even in multi-threaded programs.
- The default threadpool and threaded resolver work in child
processes.
- File descriptors are no longer leaked if
:class:`gevent.subprocess.Popen` fails to start the child.
In addition, simple use of :class:`multiprocessing.Process` is now
possible in a monkey patched system, at least on POSIX platforms.
.. note:: All of the above entail forking a child process. Forking
a child process that uses gevent, greenlets, and libev
can have some unexpected consequences if the child
doesn't immediately ``exec`` a new binary. Be sure you
understand these consequences before using this
functionality, especially late in a program's lifecycle.
For a more robust solution to certain uses of child
process, consider `gipc`_.
.. _gunicorn: http://gunicorn.org
.. _gipc: https://gehrcke.de/gipc/
Monkey patching
===============
Monkey patching is more robust, especially if the standard library
:mod:`threading` or :mod:`logging` modules had been imported before
applying the patch. In addition, there are now supported ways to
determine if something has been monkey patched.
API Additions
=============
Numerous APIs offer slightly expanded functionality in this version. Highlights
include:
- A gevent-friendly version of :obj:`select.poll` (on platforms that
implement it).
- :class:`gevent.fileobject.FileObjectPosix` uses the :mod:`io`
package on both Python 2 and Python 3, increasing its functionality
correctness, and performance. (Previously, the Python 2 implementation used the
undocumented :class:`socket._fileobject`.)
- Locks raise the same error as standard library locks if they are
over-released.
- :meth:`ThreadPool.apply <gevent.threadpool.ThreadPool.apply>` can
now be used recursively.
- The various pool objects (:class:`gevent.pool.Group`,
:class:`gevent.pool.Pool`, :class:`gevent.threadpool.ThreadPool`)
support the same improved APIs: ``imap`` and ``imap_unordered``
accept multiple iterables, ``apply`` raises any exception raised by
the target callable.
- Killing a greenlet (with :func:`gevent.kill` or
:meth:`Greenlet.kill <gevent.Greenlet.kill>`) before it is actually started and
switched to now prevents the greenlet from ever running, instead of
raising an exception when it is later switched to.
- Almost anywhere that gevent raises an exception from one greenlet to
another (e.g., :meth:`Greenlet.get <gevent.Greenlet.get>`),
the original traceback is preserved and raised.
Compatibility
=============
This release is intended to be compatible with 1.0.x with minimal or
no changes to client source code. However, there are a few changes to
be aware of that might affect some applications. Most of these changes
are due to the increased platform support of Python 3 and PyPy and
reduce the cases of undocumented or non-standard behaviour.
- :class:`gevent.baseserver.BaseServer` deterministically
`closes its sockets <https://github.com/gevent/gevent/issues/248#issuecomment-82467350>`_.
As soon as a request completes (the request handler returns),
the ``BaseServer`` and its subclasses including
:class:`gevent.server.StreamServer` and
:class:`gevent.pywsgi.WSGIServer` close the client socket.
In gevent 1.0, the client socket was left to the mercies of the
garbage collector. In the typical case, the socket would still
be closed as soon as the request handler returned due to
CPython's reference-counting garbage collector. But this meant
that a reference cycle could leave a socket dangling open for
an indeterminate amount of time, and a reference leak would
result in it never being closed. It also meant that Python 3
would produce ResourceWarnings, and PyPy (which, unlike
CPython, does not use a reference-counted GC) would only close
(and flush) the socket at an arbitrary time in the future.
If your application relied on the socket not being closed when
the request handler returned (e.g., you spawned a greenlet that
continued to use the socket) you will need to keep the request
handler from returning (e.g., ``join`` the greenlet) or
subclass the server to prevent it from closing the socket; the
former approach is strongly preferred.
- :class:`gevent.pywsgi.WSGIServer` ensures that headers set by the
application can be encoded in the ISO-8859-1 charset.
Under gevent 1.0, non-``bytes`` headers (that is, ``unicode`` since
gevent 1.0 only ran on Python 2) were encoded according to the
current default Python encoding. In some cases, this could allow
non-Latin-1 characters to be sent in the headers, but this violated
the HTTP specification, and their interpretation by the recipient is
unknown. Now, a UnicodeError will be raised.
Most applications that adhered to the WSGI PEP, `PEP 3333`_ will not
need to make any changes. See :issue:`614` for more discussion.
.. _`PEP 3333`: https://www.python.org/dev/peps/pep-3333/
- Under Python 2, the previously undocumented ``timeout`` parameter to
:meth:`Popen.wait <gevent.subprocess.Popen.wait>` (a gevent extension
) now throws an exception, just like the documented parameter to the
same stdlib method in Python 3.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment