Commit 8d727b67 authored by scoder's avatar scoder Committed by GitHub

Merge branch 'master' into feature/pythran

parents 8705f0fa 4f857124
......@@ -390,14 +390,30 @@ def normalize_existing(base_path, rel_paths):
@cached_function
def normalize_existing0(base_dir, rel_paths):
"""
Given some base directory ``base_dir`` and a list of path names
``rel_paths``, normalize each relative path name ``rel`` by
replacing it by ``os.path.join(base, rel)`` if that file exists.
Return a couple ``(normalized, needed_base)`` where ``normalized``
if the list of normalized file names and ``needed_base`` is
``base_dir`` if we actually needed ``base_dir``. If no paths were
changed (for example, if all paths were already absolute), then
``needed_base`` is ``None``.
"""
normalized = []
needed_base = None
for rel in rel_paths:
if os.path.isabs(rel):
normalized.append(rel)
continue
path = join_path(base_dir, rel)
if path_exists(path):
normalized.append(os.path.normpath(path))
needed_base = base_dir
else:
normalized.append(rel)
return normalized
return (normalized, needed_base)
def resolve_depends(depends, include_dirs):
......@@ -498,20 +514,25 @@ class DependencyTree(object):
return all
@cached_method
def cimports_and_externs(self, filename):
def cimports_externs_incdirs(self, filename):
# This is really ugly. Nested cimports are resolved with respect to the
# includer, but includes are resolved with respect to the includee.
cimports, includes, externs = self.parse_dependencies(filename)[:3]
cimports = set(cimports)
externs = set(externs)
incdirs = set()
for include in self.included_files(filename):
included_cimports, included_externs = self.cimports_and_externs(include)
included_cimports, included_externs, included_incdirs = self.cimports_externs_incdirs(include)
cimports.update(included_cimports)
externs.update(included_externs)
return tuple(cimports), normalize_existing(filename, externs)
incdirs.update(included_incdirs)
externs, incdir = normalize_existing(filename, externs)
if incdir:
incdirs.add(incdir)
return tuple(cimports), externs, incdirs
def cimports(self, filename):
return self.cimports_and_externs(filename)[0]
return self.cimports_externs_incdirs(filename)[0]
def package(self, filename):
return package(filename)
......@@ -594,12 +615,22 @@ class DependencyTree(object):
def distutils_info0(self, filename):
info = self.parse_dependencies(filename)[3]
externs = self.cimports_and_externs(filename)[1]
kwds = info.values
cimports, externs, incdirs = self.cimports_externs_incdirs(filename)
# Add dependencies on "cdef extern from ..." files
if externs:
if 'depends' in info.values:
info.values['depends'] = list(set(info.values['depends']).union(externs))
if 'depends' in kwds:
kwds['depends'] = list(set(kwds['depends']).union(externs))
else:
info.values['depends'] = list(externs)
kwds['depends'] = list(externs)
# Add include_dirs to ensure that the C compiler will find the
# "cdef extern from ..." files
if incdirs:
include_dirs = list(kwds.get('include_dirs', []))
for inc in incdirs:
if inc not in include_dirs:
include_dirs.append(inc)
kwds['include_dirs'] = include_dirs
return info
def distutils_info(self, filename, aliases=None, base=None):
......
......@@ -477,11 +477,7 @@ class FusedCFuncDefNode(StatListNode):
pyx_code.imports.put_chunk(
u"""
cdef type ndarray
try:
import numpy
ndarray = numpy.ndarray
except (ImportError, AttributeError, TypeError):
ndarray = None
ndarray = __Pyx_ImportNumPyArrayTypeIfAvailable()
""")
seen_int_dtypes = set()
......@@ -540,13 +536,18 @@ class FusedCFuncDefNode(StatListNode):
# PROCESSING ARGUMENT {{arg_tuple_idx}}
if {{arg_tuple_idx}} < len(<tuple>args):
arg = (<tuple>args)[{{arg_tuple_idx}}]
elif '{{arg.name}}' in <dict>kwargs:
elif kwargs is not None and '{{arg.name}}' in <dict>kwargs:
arg = (<dict>kwargs)['{{arg.name}}']
else:
{{if arg.default}}
arg = (<tuple>defaults)[{{default_idx}}]
{{else}}
raise TypeError("Expected at least %d arguments" % len(<tuple>args))
{{if arg_tuple_idx < min_positional_args}}
raise TypeError("Expected at least %d argument%s, got %d" % (
{{min_positional_args}}, {{'"s"' if min_positional_args != 1 else '""'}}, len(<tuple>args)))
{{else}}
raise TypeError("Missing keyword-only argument: '%s'" % "{{arg.default}}")
{{endif}}
{{endif}}
""")
......@@ -568,6 +569,10 @@ class FusedCFuncDefNode(StatListNode):
'memviewslice_cname': MemoryView.memviewslice_cname,
'func_args': self.node.args,
'n_fused': len(fused_types),
'min_positional_args':
self.node.num_required_args - self.node.num_required_kw_args
if is_def else
sum(1 for arg in self.node.args if arg.default is None),
'name': orig_py_func.entry.name,
}
......@@ -577,14 +582,11 @@ class FusedCFuncDefNode(StatListNode):
u"""
cdef extern from *:
void __pyx_PyErr_Clear "PyErr_Clear" ()
type __Pyx_ImportNumPyArrayTypeIfAvailable()
int __Pyx_Is_Little_Endian()
""")
decl_code.indent()
pyx_code.put_chunk(
u"""
from __future__ import absolute_import # for later numpy import
""")
pyx_code.put_chunk(
u"""
def __pyx_fused_cpdef(signatures, args, kwargs, defaults):
......@@ -593,8 +595,8 @@ class FusedCFuncDefNode(StatListNode):
dest_sig = [None] * {{n_fused}}
if kwargs is None:
kwargs = {}
if kwargs is not None and not kwargs:
kwargs = None
cdef Py_ssize_t i
......@@ -655,15 +657,18 @@ class FusedCFuncDefNode(StatListNode):
if all_buffer_types:
self._buffer_declarations(pyx_code, decl_code, all_buffer_types)
env.use_utility_code(Code.UtilityCode.load_cached("Import", "ImportExport.c"))
env.use_utility_code(Code.UtilityCode.load_cached("ImportNumPyArray", "ImportExport.c"))
pyx_code.put_chunk(
u"""
candidates = []
for sig in <dict>signatures:
match_found = False
for src_type, dst_type in zip(sig.strip('()').split('|'), dest_sig):
src_sig = sig.strip('()').split('|')
for i in range(len(dest_sig)):
dst_type = dest_sig[i]
if dst_type is not None:
if src_type == dst_type:
if src_sig[i] == dst_type:
match_found = True
else:
match_found = False
......
......@@ -2649,6 +2649,9 @@ class DefNode(FuncDefNode):
child_attrs = ["args", "star_arg", "starstar_arg", "body", "decorators", "return_type_annotation"]
is_staticmethod = False
is_classmethod = False
lambda_name = None
reqd_kw_flags_cname = "0"
is_wrapper = 0
......@@ -2761,7 +2764,6 @@ class DefNode(FuncDefNode):
return True
def analyse_declarations(self, env):
self.is_classmethod = self.is_staticmethod = False
if self.decorators:
for decorator in self.decorators:
func = decorator.decorator
......
......@@ -1355,8 +1355,18 @@ class DecoratorTransform(ScopeTrackingTransform, SkipDeclarations):
return self._reject_decorated_property(node, decorator_node)
return self._add_to_property(properties, node, handler_name, decorator_node)
# we clear node.decorators, so we need to set the
# is_staticmethod/is_classmethod attributes now
for decorator in node.decorators:
func = decorator.decorator
if func.is_name:
node.is_classmethod |= func.name == 'classmethod'
node.is_staticmethod |= func.name == 'staticmethod'
# transform normal decorators
return self.chain_decorators(node, node.decorators, node.name)
decs = node.decorators
node.decorators = None
return self.chain_decorators(node, decs, node.name)
@staticmethod
def _reject_decorated_property(node, decorator_node):
......
......@@ -2208,15 +2208,17 @@ class CPointerBaseType(CType):
if base_type.signed == 2:
self.to_py_function = "__Pyx_PyObject_FromCString"
if self.is_ptr:
self.from_py_function = "__Pyx_PyObject_AsSString"
self.from_py_function = "__Pyx_PyObject_As%sSString"
elif base_type.signed:
self.to_py_function = "__Pyx_PyObject_FromString"
if self.is_ptr:
self.from_py_function = "__Pyx_PyObject_AsString"
self.from_py_function = "__Pyx_PyObject_As%sString"
else:
self.to_py_function = "__Pyx_PyObject_FromCString"
if self.is_ptr:
self.from_py_function = "__Pyx_PyObject_AsUString"
self.from_py_function = "__Pyx_PyObject_As%sUString"
if self.is_ptr:
self.from_py_function %= '' if self.base_type.is_const else 'Writable'
self.exception_value = "NULL"
elif self.is_pyunicode_ptr and not base_type.is_error:
self.to_py_function = "__Pyx_PyUnicode_FromUnicode"
......@@ -2596,7 +2598,7 @@ class CFuncType(CType):
return self.same_c_signature_as_resolved_type(
other_type.resolve(), as_cmethod)
def same_c_signature_as_resolved_type(self, other_type, as_cmethod = 0):
def same_c_signature_as_resolved_type(self, other_type, as_cmethod = 0, as_pxd_definition = 0):
#print "CFuncType.same_c_signature_as_resolved_type:", \
# self, other_type, "as_cmethod =", as_cmethod ###
if other_type is error_type:
......@@ -2618,6 +2620,11 @@ class CFuncType(CType):
return 0
if self.optional_arg_count != other_type.optional_arg_count:
return 0
if as_pxd_definition:
# A narrowing of the return type declared in the pxd is allowed.
if not self.return_type.subtype_of_resolved_type(other_type.return_type):
return 0
else:
if not self.return_type.same_as(other_type.return_type):
return 0
if not self.same_calling_convention_as(other_type):
......@@ -3238,7 +3245,7 @@ class CStructOrUnionType(CType):
return False
context = dict(
struct_name=self.name,
struct_type=self,
var_entries=self.scope.var_entries,
funcname=self.from_py_function,
)
......
......@@ -2098,7 +2098,8 @@ class CClassScope(ClassScope):
# Fix with_gil vs nogil.
entry.type = entry.type.with_with_gil(type.with_gil)
elif type.compatible_signature_with(entry.type, as_cmethod = 1) and type.nogil == entry.type.nogil:
if self.defined and not in_pxd:
if (self.defined and not in_pxd
and not type.same_c_signature_as_resolved_type(entry.type, as_cmethod = 1, as_pxd_definition = 1)):
error(pos,
"Compatible but non-identical C method '%s' not redeclared "
"in definition part of extension type '%s'" % (name, self.class_name))
......
# cython.* namespace for pure mode.
from __future__ import absolute_import
__version__ = "0.25.2"
__version__ = "0.26.alpha0"
try:
from __builtin__ import basestring
......
......@@ -8,8 +8,8 @@ cdef extern from *:
object PyErr_Format(exc, const char *format, ...)
@cname("{{funcname}}")
cdef {{struct_name}} {{funcname}}(obj) except *:
cdef {{struct_name}} result
cdef {{struct_type}} {{funcname}}(obj) except *:
cdef {{struct_type}} result
if not PyMapping_Check(obj):
PyErr_Format(TypeError, b"Expected %.16s, got %.200s", b"a mapping", Py_TYPE(obj).tp_name)
......@@ -33,8 +33,8 @@ cdef extern from *:
object PyErr_Format(exc, const char *format, ...)
@cname("{{funcname}}")
cdef {{struct_name}} {{funcname}}(obj) except *:
cdef {{struct_name}} result
cdef {{struct_type}} {{funcname}}(obj) except *:
cdef {{struct_type}} result
cdef Py_ssize_t length
if not PyMapping_Check(obj):
PyErr_Format(TypeError, b"Expected %.16s, got %.200s", b"a mapping", Py_TYPE(obj).tp_name)
......
......@@ -7,12 +7,12 @@ cdef extern from *:
cdef cppclass string "{{type}}":
string()
string(char* c_str, size_t size)
cdef char* __Pyx_PyObject_AsStringAndSize(object, Py_ssize_t*) except NULL
cdef const char* __Pyx_PyObject_AsStringAndSize(object, Py_ssize_t*) except NULL
@cname("{{cname}}")
cdef string {{cname}}(object o) except *:
cdef Py_ssize_t length
cdef char* data = __Pyx_PyObject_AsStringAndSize(o, &length)
cdef const char* data = __Pyx_PyObject_AsStringAndSize(o, &length)
return string(data, length)
......@@ -27,7 +27,7 @@ cdef extern from *:
{{for py_type in ['PyObject', 'PyUnicode', 'PyStr', 'PyBytes', 'PyByteArray']}}
cdef extern from *:
cdef object __Pyx_{{py_type}}_FromStringAndSize(char*, size_t)
cdef object __Pyx_{{py_type}}_FromStringAndSize(const char*, size_t)
@cname("{{cname.replace("PyObject", py_type, 1)}}")
cdef inline object {{cname.replace("PyObject", py_type, 1)}}(const string& s):
......
......@@ -661,3 +661,41 @@ bad:
Py_XDECREF(ob);
return NULL;
}
/////////////// ImportNumPyArray.proto ///////////////
static PyObject *__pyx_numpy_ndarray = NULL;
static PyObject* __Pyx_ImportNumPyArrayTypeIfAvailable(void); /*proto*/
/////////////// ImportNumPyArray.cleanup ///////////////
Py_CLEAR(__pyx_numpy_ndarray);
/////////////// ImportNumPyArray ///////////////
//@requires: ImportExport.c::Import
static PyObject* __Pyx__ImportNumPyArray(void) {
PyObject *numpy_module, *ndarray_object = NULL;
numpy_module = __Pyx_Import(PYIDENT("numpy"), NULL, 0);
if (likely(numpy_module)) {
ndarray_object = PyObject_GetAttrString(numpy_module, "ndarray");
}
if (unlikely(!ndarray_object)) {
// ImportError, AttributeError, ...
PyErr_Clear();
}
if (unlikely(!ndarray_object || !PyObject_TypeCheck(ndarray_object, &PyType_Type))) {
Py_XDECREF(ndarray_object);
Py_INCREF(Py_None);
ndarray_object = Py_None;
}
return ndarray_object;
}
static CYTHON_INLINE PyObject* __Pyx_ImportNumPyArrayTypeIfAvailable(void) {
if (unlikely(!__pyx_numpy_ndarray)) {
__pyx_numpy_ndarray = __Pyx__ImportNumPyArray();
}
return __pyx_numpy_ndarray;
}
......@@ -37,8 +37,8 @@
#define __Pyx_sst_abs(value) ((value<0) ? -value : value)
#endif
static CYTHON_INLINE char* __Pyx_PyObject_AsString(PyObject*);
static CYTHON_INLINE char* __Pyx_PyObject_AsStringAndSize(PyObject*, Py_ssize_t* length);
static CYTHON_INLINE const char* __Pyx_PyObject_AsString(PyObject*);
static CYTHON_INLINE const char* __Pyx_PyObject_AsStringAndSize(PyObject*, Py_ssize_t* length);
#define __Pyx_PyByteArray_FromString(s) PyByteArray_FromStringAndSize((const char*)s, strlen((const char*)s))
#define __Pyx_PyByteArray_FromStringAndSize(s, l) PyByteArray_FromStringAndSize((const char*)s, l)
......@@ -54,8 +54,11 @@ static CYTHON_INLINE PyObject* __Pyx_PyUnicode_FromString(const char*);
#define __Pyx_PyStr_FromStringAndSize __Pyx_PyUnicode_FromStringAndSize
#endif
#define __Pyx_PyObject_AsSString(s) ((signed char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_AsUString(s) ((unsigned char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_AsWritableString(s) ((char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_AsWritableSString(s) ((signed char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_AsWritableUString(s) ((unsigned char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_AsSString(s) ((const signed char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_AsUString(s) ((const unsigned char*) __Pyx_PyObject_AsString(s))
#define __Pyx_PyObject_FromCString(s) __Pyx_PyObject_FromString((const char*)s)
#define __Pyx_PyBytes_FromCString(s) __Pyx_PyBytes_FromString((const char*)s)
#define __Pyx_PyByteArray_FromCString(s) __Pyx_PyByteArray_FromString((const char*)s)
......@@ -189,12 +192,14 @@ static CYTHON_INLINE PyObject* __Pyx_PyUnicode_FromString(const char* c_str) {
return __Pyx_PyUnicode_FromStringAndSize(c_str, (Py_ssize_t)strlen(c_str));
}
static CYTHON_INLINE char* __Pyx_PyObject_AsString(PyObject* o) {
// Py3.7 returns a "const char*" for unicode strings
static CYTHON_INLINE const char* __Pyx_PyObject_AsString(PyObject* o) {
Py_ssize_t ignore;
return __Pyx_PyObject_AsStringAndSize(o, &ignore);
}
static CYTHON_INLINE char* __Pyx_PyObject_AsStringAndSize(PyObject* o, Py_ssize_t *length) {
// Py3.7 returns a "const char*" for unicode strings
static CYTHON_INLINE const char* __Pyx_PyObject_AsStringAndSize(PyObject* o, Py_ssize_t *length) {
#if CYTHON_COMPILING_IN_CPYTHON && (__PYX_DEFAULT_STRING_ENCODING_IS_ASCII || __PYX_DEFAULT_STRING_ENCODING_IS_DEFAULT)
if (
#if PY_MAJOR_VERSION < 3 && __PYX_DEFAULT_STRING_ENCODING_IS_ASCII
......
......@@ -339,42 +339,57 @@ Public Declarations
---------------------
You can make C types, variables and functions defined in a Cython module
accessible to C code that is linked with the module, by declaring them with
the public keyword::
accessible to C code that is linked together with the Cython-generated C file,
by declaring them with the public keyword::
cdef public struct Bunny: # public type declaration
int vorpalness
cdef public int spam # public variable declaration
cdef public void grail(Bunny *): # public function declaration
print "Ready the holy hand grenade"
cdef public void grail(Bunny *) # public function declaration
If there are any public declarations in a Cython module, a header file called
:file:`modulename.h` file is generated containing equivalent C declarations for
inclusion in other C code.
Users who are embedding Python in C with Cython need to make sure to call Py_Initialize()
and Py_Finalize(). For example, in the following snippet that includes :file:`modulename.h`::
A typical use case for this is building an extension module from multiple
C sources, one of them being Cython generated (i.e. with something like
``Extension("grail", sources=["grail.pyx", "grail_helper.c"])`` in ``setup.py``.
In this case, the file ``grail_helper.c`` just needs to add
``#include "grail.h"`` in order to access the public Cython variables.
A more advanced use case is embedding Python in C using Cython.
In this case, make sure to call Py_Initialize() and Py_Finalize().
For example, in the following snippet that includes :file:`grail.h`:
.. code-block:: c
#include <Python.h>
#include "modulename.h"
#include "grail.h"
void grail() {
int main() {
Py_Initialize();
initmodulename();
initgrail();
Bunny b;
grail(b);
Py_Finalize();
}
Any C code wanting to make use of these declarations will need to be linked,
either statically or dynamically, with the extension module.
This C code can then be built together with the Cython-generated C code
in a single program (or library).
If the Cython module resides within a package, then the name of the ``.h``
file consists of the full dotted name of the module, e.g. a module called
:mod:`foo.spam` would have a header file called :file:`foo.spam.h`.
.. NOTE::
On some operating systems like Linux, it is also possible to first
build the Cython extension in the usual way and then link against
the resulting ``.so`` file like a dynamic library.
Beware that this is not portable, so it should be avoided.
.. _api:
C API Declarations
......@@ -418,10 +433,12 @@ made available when you include :file:`modulename_api.h`.::
Vehicle car;
int main(int argc, char *argv[]) {
Py_Initialize();
import_delorean();
car.speed = atoi(argv[1]);
car.power = atof(argv[2]);
activate(&car);
Py_Finalize();
}
.. note::
......@@ -434,7 +451,10 @@ made available when you include :file:`modulename_api.h`.::
Using the :keyword:`api` method does not require the C code using the
declarations to be linked with the extension module in any way, as the Python
import machinery is used to make the connection dynamically. However, only
functions can be accessed this way, not variables.
functions can be accessed this way, not variables. Note also that for the
module import mechanism to be set up correctly, the user must call
Py_Initialize() and Py_Finalize(); if you experience a segmentation fault in
the call to :func:`import_modulename`, it is likely that this wasn't done.
You can use both :keyword:`public` and :keyword:`api` on the same function to
make it available by both methods, e.g.::
......
......@@ -6,3 +6,6 @@ cdef class MissingRedeclaration(Base):
cdef class BadRedeclaration(Base):
cdef f(self)
cdef class NarrowerReturn(Base):
pass
......@@ -28,6 +28,11 @@ cdef class UnneededRedeclaration(Base):
cpdef f(self):
pass
cdef class NarrowerReturn(Base):
# This does not require a new vtable entry.
cdef Base f(self):
pass
_ERRORS = u"""
8: 9: Signature not compatible with previous declaration
......
......@@ -19,8 +19,15 @@ setup(
######## site-packages/b/other.pxd ########
cdef inline foo(int a):
return a**2
cdef extern from "foo.c":
int foo(int)
######## site-packages/b/foo.c ########
static int foo(int a)
{
return a * a;
}
######## a.pyx ########
......
......@@ -304,3 +304,47 @@ def test_annotations(a: "test", b: "other" = 2, c: 123 = 4) -> "ret":
def inner(x: "banana", y: b()) -> c():
return x,y
return inner
def add_one(func):
"Decorator to add 1 to the last argument of the function call"
def inner(*args):
args = args[:-1] + (args[-1] + 1,)
return func(*args)
return inner
@add_one
def test_decorated(x):
"""
>>> test_decorated(0)
1
"""
return x
@add_one
@add_one
def test_decorated2(x):
"""
>>> test_decorated2(0)
2
"""
return x
cdef class TestDecoratedMethods:
@add_one
def test(self, x):
"""
>>> TestDecoratedMethods().test(0)
1
"""
return x
@add_one
@add_one
def test2(self, x):
"""
>>> TestDecoratedMethods().test2(0)
2
"""
return x
......@@ -94,6 +94,12 @@ def test_multiarg():
x is an int, y is a float: 1 2.0
x is an int, y is a float: 1 2.0
x is a long, y is a double: 4 5.0
>>> multiarg()
Traceback (most recent call last):
TypeError: Expected at least 2 arguments, got 0
>>> multiarg(1, 2.0, 3) # doctest: +ELLIPSIS
Traceback (most recent call last):
TypeError: ...2...arg...3...
"""
multiarg[int, float](1, 2.0)
multiarg[cy.int, cy.float](1, 2.0)
......
......@@ -105,11 +105,15 @@ def opt_func(fused_t obj, cython.floating myf = 1.2, cython.integral myi = 7):
>>> opt_func(object(), f)
Traceback (most recent call last):
...
TypeError: Function call with ambiguous argument types
>>> opt_func()
Traceback (most recent call last):
TypeError: Expected at least 1 argument, got 0
>>> opt_func("abc", f, i, 5) # doctest: +ELLIPSIS
Traceback (most recent call last):
TypeError: ...at most 3...
>>> opt_func[ExtClassA, cy.float, cy.long](object(), f)
Traceback (most recent call last):
...
TypeError: Argument 'obj' has incorrect type (expected fused_def.ExtClassA, got object)
"""
print cython.typeof(obj), cython.typeof(myf), cython.typeof(myi)
......
......@@ -181,7 +181,7 @@ try:
>>> print(test_nested_dtypes(np.zeros((3,), dtype=np.dtype([\
('a', np.dtype('i,i')),\
('b', np.dtype('i,i'))\
]))))
])))) # doctest: +NORMALIZE_WHITESPACE
array([((0, 0), (0, 0)), ((1, 2), (1, 4)), ((1, 2), (1, 4))],
dtype=[('a', [('f0', '!i4'), ('f1', '!i4')]), ('b', [('f0', '!i4'), ('f1', '!i4')])])
......@@ -234,7 +234,7 @@ try:
1,1
8,16
>>> test_point_record()
>>> test_point_record() # doctest: +NORMALIZE_WHITESPACE
array([(0., 0.), (1., -1.), (2., -2.)],
dtype=[('x', '!f8'), ('y', '!f8')])
......
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