Commit 5684a9ac authored by Georg Brandl's avatar Georg Brandl

Clean up tools: remove "world" and "framer", move single SSL script to scripts/.

parent 4cf83f4d
framer is a tool to generate boilerplate code for C extension types.
The boilerplate is generated from a specification object written in
Python. The specification uses the class statement to describe the
extension module and any extension types it contains. From the
specification, framer can generate all the boilerplate C code,
including function definitions, argument handling code, and type
objects.
Add spec for getsets.
Generate a distutils setup script.
Handle operator overloading.
Generate traverse and clear methods for GC.
Handle mapping, sequence, buffer protocols.
Finish the todo list.
"""Generate the skeleton for cStringIO as an example of framer."""
from framer.bases import Module, Type
from framer.member import member
class cStringIO(Module):
"""A simple fast partial StringIO replacement.
This module provides a simple useful replacement for the StringIO
module that is written in C. It does not provide the full
generality of StringIO, but it provides enough for most
applications and is especially useful in conjunction with the
pickle module.
Usage:
from io import StringIO
an_output_stream = StringIO()
an_output_stream.write(some_stuff)
...
value = an_output_stream.getvalue()
an_input_stream = StringIO(a_string)
spam = an_input_stream.readline()
spam = an_input_stream.read(5)
an_input_stream.seek(0) # OK, start over
spam = an_input_stream.read() # and read it all
"""
__file__ = "cStringIO.c"
def StringIO(o):
"""Return a StringIO-like stream for reading or writing"""
StringIO.pyarg = "|O"
class InputType(Type):
"Simple type for treating strings as input file streams"
abbrev = "input"
struct = """\
typedef struct {
PyObject_HEAD
char *buf;
int pos;
int size;
PyObject *pbuf;
} InputObject;
"""
def flush(self):
"""Does nothing"""
def getvalue(self):
"""Get the string value.
If use_pos is specified and is a true value, then the
string returned will include only the text up to the
current file position.
"""
def isatty(self):
"""Always returns False"""
def read(self, s):
"""Return s characters or the rest of the string."""
read.pyarg = "|i"
def readline(self):
"""Read one line."""
def readlines(self, hint):
"""Read all lines."""
readlines.pyarg = "|i"
def reset(self):
"""Reset the file position to the beginning."""
def tell(self):
"""Get the current position."""
def truncate(self, pos):
"""Truncate the file at the current position."""
truncate.pyarg = "|i"
def seek(self, position, mode=0):
"""Set the current position.
The optional mode argument can be 0 for absolute, 1 for relative,
and 2 for relative to EOF. The default is absolute.
"""
seek.pyarg = "i|i"
def close(self):
pass
class OutputType(InputType):
"Simple type for output strings."
abbrev = "output"
struct = """\
typedef struct {
PyObject_HEAD
char *buf;
int pos;
int size;
int softspace;
} OutputObject;
"""
softspace = member()
def close(self):
"""Explicitly release resources."""
def write(self, s):
"""Write a string to the file."""
# XXX Hack: writing None resets the buffer
def writelines(self, lines):
"""Write each string in lines."""
cStringIO.gen()
"""A tool to generate basic framework for C extension types.
The basic ideas is the same as modulator, but the code generates code
using many of the new features introduced in Python 2.2. It also
takes a more declarative approach to generating code.
"""
"""Provides the Module and Type base classes that user code inherits from."""
__all__ = ["Module", "Type", "member"]
from framer import struct, template
from framer.function import Function, Method
from framer.member import member
from framer.slots import *
from framer.util import cstring, unindent
from types import FunctionType
# The Module and Type classes are implemented using metaclasses,
# because most of the methods are class methods. It is easier to use
# metaclasses than the cumbersome classmethod() builtin. They have
# class methods because they are exposed to user code as base classes.
class BaseMetaclass(type):
"""Shared infrastructure for generating modules and types."""
# just methoddef so far
def dump_methoddef(self, f, functions, vars):
def p(templ, vars=vars): # helper function to generate output
print(templ % vars, file=f)
if not functions:
return
p(template.methoddef_start)
for name, func in sorted(functions.items()):
if func.__doc__:
p(template.methoddef_def_doc, func.vars)
else:
p(template.methoddef_def, func.vars)
p(template.methoddef_end)
class ModuleMetaclass(BaseMetaclass):
"""Provides methods for Module class."""
def gen(self):
self.analyze()
self.initvars()
f = open(self.__filename, "w")
self.dump(f)
f.close()
def analyze(self):
self.name = getattr(self, "abbrev", self.__name__)
self.__functions = {}
self.__types = {}
self.__members = False
for name, obj in self.__dict__.items():
if isinstance(obj, FunctionType):
self.__functions[name] = Function(obj, self)
elif isinstance(obj, TypeMetaclass):
obj._TypeMetaclass__module = self.name
obj.analyze()
self.__types[name] = obj
if obj.has_members():
self.__members = True
def initvars(self):
v = self.__vars = {}
filename = getattr(self, "__file__", None)
if filename is None:
filename = self.__name__ + "module.c"
self.__filename = v["FileName"] = filename
name = v["ModuleName"] = self.__name__
v["MethodDefName"] = "%s_methods" % name
v["ModuleDocstring"] = cstring(unindent(self.__doc__))
def dump(self, f):
def p(templ, vars=self.__vars): # helper function to generate output
print(templ % vars, file=f)
p(template.module_start)
if self.__members:
p(template.member_include)
print(file=f)
if self.__doc__:
p(template.module_doc)
for name, type in sorted(self.__types.items()):
type.dump(f)
for name, func in sorted(self.__functions.items()):
func.dump(f)
self.dump_methoddef(f, self.__functions, self.__vars)
p(template.module_init_start)
for name, type in sorted(self.__types.items()):
type.dump_init(f)
p("}")
class Module(metaclass=ModuleMetaclass):
pass
class TypeMetaclass(BaseMetaclass):
def dump(self, f):
self.initvars()
# defined after initvars() so that __vars is defined
def p(templ, vars=self.__vars):
print(templ % vars, file=f)
if self.struct is not None:
print(unindent(self.struct, False), file=f)
if self.__doc__:
p(template.docstring)
for name, func in sorted(self.__methods.items()):
func.dump(f)
self.dump_methoddef(f, self.__methods, self.__vars)
self.dump_memberdef(f)
self.dump_slots(f)
def has_members(self):
if self.__members:
return True
else:
return False
def analyze(self):
# called by ModuleMetaclass analyze()
self.name = getattr(self, "abbrev", self.__name__)
src = getattr(self, "struct", None)
if src is not None:
self.__struct = struct.parse(src)
else:
self.__struct = None
self.__methods = {}
self.__members = {}
for cls in self.__mro__:
for k, v in cls.__dict__.items():
if isinstance(v, FunctionType):
self.__methods[k] = Method(v, self)
if isinstance(v, member):
self.__members[k] = v
assert self.__struct is not None
v.register(k, self.__struct)
self.analyze_slots()
def analyze_slots(self):
self.__slots = {}
for s in Slots:
if s.special is not None:
meth = self.__methods.get(s.special)
if meth is not None:
self.__slots[s] = meth
self.__slots[TP_NAME] = '"%s.%s"' % (self.__module, self.__name__)
if self.__doc__:
self.__slots[TP_DOC] = "%s_doc" % self.name
if self.__struct is not None:
self.__slots[TP_BASICSIZE] = "sizeof(%s)" % self.__struct.name
self.__slots[TP_DEALLOC] = "%s_dealloc" % self.name
if self.__methods:
self.__slots[TP_METHODS] = "%s_methods" % self.name
if self.__members:
self.__slots[TP_MEMBERS] = "%s_members" % self.name
def initvars(self):
v = self.__vars = {}
v["TypeName"] = self.__name__
v["CTypeName"] = "Py%s_Type" % self.__name__
v["MethodDefName"] = self.__slots[TP_METHODS]
if self.__doc__:
v["DocstringVar"] = self.__slots[TP_DOC]
v["Docstring"] = cstring(unindent(self.__doc__))
if self.__struct is not None:
v["StructName"] = self.__struct.name
if self.__members:
v["MemberDefName"] = self.__slots[TP_MEMBERS]
def dump_memberdef(self, f):
def p(templ, vars=self.__vars):
print(templ % vars, file=f)
if not self.__members:
return
p(template.memberdef_start)
for name, slot in sorted(self.__members.items()):
slot.dump(f)
p(template.memberdef_end)
def dump_slots(self, f):
def p(templ, vars=self.__vars):
print(templ % vars, file=f)
if self.struct:
p(template.dealloc_func, {"name" : self.__slots[TP_DEALLOC]})
p(template.type_struct_start)
for s in Slots[:-5]: # XXX
val = self.__slots.get(s, s.default)
ntabs = 4 - (4 + len(val)) // 8
line = " %s,%s/* %s */" % (val, "\t" * ntabs, s.name)
print(line, file=f)
p(template.type_struct_end)
def dump_init(self, f):
def p(templ):
print(templ % self.__vars, file=f)
p(template.type_init_type)
p(template.module_add_type)
class Type(metaclass=TypeMetaclass):
pass
"""Functions."""
from framer import template
from framer.util import cstring, unindent
METH_O = "METH_O"
METH_NOARGS = "METH_NOARGS"
METH_VARARGS = "METH_VARARGS"
def parsefmt(fmt):
for c in fmt:
if c == '|':
continue
yield c
class Argument:
def __init__(self, name):
self.name = name
self.ctype = "PyObject *"
self.default = None
def __str__(self):
return "%s%s" % (self.ctype, self.name)
def setfmt(self, code):
self.ctype = self._codes[code]
if self.ctype[-1] != "*":
self.ctype += " "
_codes = {"O": "PyObject *",
"i": "int",
}
def decl(self):
if self.default is None:
return str(self) + ";"
else:
return "%s = %s;" % (self, self.default)
class _ArgumentList(object):
# these instance variables should be initialized by subclasses
ml_meth = None
fmt = None
def __init__(self, args):
self.args = list(map(Argument, args))
def __len__(self):
return len(self.args)
def __getitem__(self, i):
return self.args[i]
def dump_decls(self, f):
pass
class NoArgs(_ArgumentList):
def __init__(self, args):
assert len(args) == 0
super(NoArgs, self).__init__(args)
self.ml_meth = METH_NOARGS
def c_args(self):
return "PyObject *self"
class OneArg(_ArgumentList):
def __init__(self, args):
assert len(args) == 1
super(OneArg, self).__init__(args)
self.ml_meth = METH_O
def c_args(self):
return "PyObject *self, %s" % self.args[0]
class VarArgs(_ArgumentList):
def __init__(self, args, fmt=None):
super(VarArgs, self).__init__(args)
self.ml_meth = METH_VARARGS
if fmt is not None:
self.fmt = fmt
i = 0
for code in parsefmt(fmt):
self.args[i].setfmt(code)
i += 1
def c_args(self):
return "PyObject *self, PyObject *args"
def targets(self):
return ", ".join(["&%s" % a.name for a in self.args])
def dump_decls(self, f):
for a in self.args:
print(" %s" % a.decl(), file=f)
def ArgumentList(func, method):
code = func.__code__
args = code.co_varnames[:code.co_argcount]
if method:
args = args[1:]
pyarg = getattr(func, "pyarg", None)
if pyarg is not None:
args = VarArgs(args, pyarg)
if func.__defaults__:
L = list(func.__defaults__)
ndefault = len(L)
i = len(args) - ndefault
while L:
args[i].default = L.pop(0)
return args
else:
if len(args) == 0:
return NoArgs(args)
elif len(args) == 1:
return OneArg(args)
else:
return VarArgs(args)
class Function:
method = False
def __init__(self, func, parent):
self._func = func
self._parent = parent
self.analyze()
self.initvars()
def dump(self, f):
def p(templ, vars=None): # helper function to generate output
if vars is None:
vars = self.vars
print(templ % vars, file=f)
if self.__doc__:
p(template.docstring)
d = {"name" : self.vars["CName"],
"args" : self.args.c_args(),
}
p(template.funcdef_start, d)
self.args.dump_decls(f)
if self.args.ml_meth == METH_VARARGS:
p(template.varargs)
p(template.funcdef_end)
def analyze(self):
self.__doc__ = self._func.__doc__
self.args = ArgumentList(self._func, self.method)
def initvars(self):
v = self.vars = {}
v["PythonName"] = self._func.__name__
s = v["CName"] = "%s_%s" % (self._parent.name, self._func.__name__)
v["DocstringVar"] = s + "_doc"
v["MethType"] = self.args.ml_meth
if self.__doc__:
v["Docstring"] = cstring(unindent(self.__doc__))
if self.args.fmt is not None:
v["ArgParse"] = self.args.fmt
v["ArgTargets"] = self.args.targets()
class Method(Function):
method = True
from framer import template
from framer.util import cstring, unindent
T_SHORT = "T_SHORT"
T_INT = "T_INT"
T_LONG = "T_LONG"
T_FLOAT = "T_FLOAT"
T_DOUBLE = "T_DOUBLE"
T_STRING = "T_STRING"
T_OBJECT = "T_OBJECT"
T_CHAR = "T_CHAR"
T_BYTE = "T_BYTE"
T_UBYTE = "T_UBYTE"
T_UINT = "T_UINT"
T_ULONG = "T_ULONG"
T_STRING_INPLACE = "T_STRING_INPLACE"
T_OBJECT_EX = "T_OBJECT_EX"
RO = READONLY = "READONLY"
READ_RESTRICTED = "READ_RESTRICTED"
WRITE_RESTRICTED = "WRITE_RESTRICTED"
RESTRICT = "RESTRICTED"
c2t = {"int" : T_INT,
"unsigned int" : T_UINT,
"long" : T_LONG,
"unsigned long" : T_LONG,
"float" : T_FLOAT,
"double" : T_DOUBLE,
"char *" : T_CHAR,
"PyObject *" : T_OBJECT,
}
class member(object):
def __init__(self, cname=None, type=None, flags=None, doc=None):
self.type = type
self.flags = flags
self.cname = cname
self.doc = doc
self.name = None
self.struct = None
def register(self, name, struct):
self.name = name
self.struct = struct
self.initvars()
def initvars(self):
v = self.vars = {}
v["PythonName"] = self.name
if self.cname is not None:
v["CName"] = self.cname
else:
v["CName"] = self.name
v["Flags"] = self.flags or "0"
v["Type"] = self.get_type()
if self.doc is not None:
v["Docstring"] = cstring(unindent(self.doc))
v["StructName"] = self.struct.name
def get_type(self):
"""Deduce type code from struct specification if not defined"""
if self.type is not None:
return self.type
ctype = self.struct.get_type(self.name)
return c2t[ctype]
def dump(self, f):
if self.doc is None:
print(template.memberdef_def % self.vars, file=f)
else:
print(template.memberdef_def_doc % self.vars, file=f)
"""Descriptions of all the slots in Python's type objects."""
class Slot(object):
def __init__(self, name, cast=None, special=None, default="0"):
self.name = name
self.cast = cast
self.special = special
self.default = default
Slots = (Slot("ob_size"),
Slot("tp_name"),
Slot("tp_basicsize"),
Slot("tp_itemsize"),
Slot("tp_dealloc", "destructor"),
Slot("tp_print", "printfunc"),
Slot("tp_getattr", "getattrfunc"),
Slot("tp_setattr", "setattrfunc"),
Slot("tp_reserved", "void*"),
Slot("tp_repr", "reprfunc", "__repr__"),
Slot("tp_as_number"),
Slot("tp_as_sequence"),
Slot("tp_as_mapping"),
Slot("tp_hash", "hashfunc", "__hash__"),
Slot("tp_call", "ternaryfunc", "__call__"),
Slot("tp_str", "reprfunc", "__str__"),
Slot("tp_getattro", "getattrofunc", "__getattr__", # XXX
"PyObject_GenericGetAttr"),
Slot("tp_setattro", "setattrofunc", "__setattr__"),
Slot("tp_as_buffer"),
Slot("tp_flags", default="Py_TPFLAGS_DEFAULT"),
Slot("tp_doc"),
Slot("tp_traverse", "traverseprox"),
Slot("tp_clear", "inquiry"),
Slot("tp_richcompare", "richcmpfunc"),
Slot("tp_weaklistoffset"),
Slot("tp_iter", "getiterfunc", "__iter__"),
Slot("tp_iternext", "iternextfunc", "__next__"), # XXX
Slot("tp_methods"),
Slot("tp_members"),
Slot("tp_getset"),
Slot("tp_base"),
Slot("tp_dict"),
Slot("tp_descr_get", "descrgetfunc"),
Slot("tp_descr_set", "descrsetfunc"),
Slot("tp_dictoffset"),
Slot("tp_init", "initproc", "__init__"),
Slot("tp_alloc", "allocfunc"),
Slot("tp_new", "newfunc"),
Slot("tp_free", "freefunc"),
Slot("tp_is_gc", "inquiry"),
Slot("tp_bases"),
Slot("tp_mro"),
Slot("tp_cache"),
Slot("tp_subclasses"),
Slot("tp_weaklist"),
)
# give some slots symbolic names
TP_NAME = Slots[1]
TP_BASICSIZE = Slots[2]
TP_DEALLOC = Slots[4]
TP_DOC = Slots[20]
TP_METHODS = Slots[27]
TP_MEMBERS = Slots[28]
"""Rudimentary parser for C struct definitions."""
import re
PyObject_HEAD = "PyObject_HEAD"
PyObject_VAR_HEAD = "PyObject_VAR_HEAD"
rx_name = re.compile("} (\w+);")
class Struct:
def __init__(self, name, head, members):
self.name = name
self.head = head
self.members = members
def get_type(self, name):
for _name, type in self.members:
if name == _name:
return type
raise ValueError("no member named %s" % name)
def parse(s):
"""Parse a C struct definition.
The parser is very restricted in what it will accept.
"""
lines = [_f for _f in s.split("\n") if _f] # get non-empty lines
assert lines[0].strip() == "typedef struct {"
pyhead = lines[1].strip()
assert (pyhead.startswith("PyObject") and
pyhead.endswith("HEAD"))
members = []
for line in lines[2:]:
line = line.strip()
if line.startswith("}"):
break
assert line.endswith(";")
line = line[:-1]
words = line.split()
name = words[-1]
type = " ".join(words[:-1])
if name[0] == "*":
name = name[1:]
type += " *"
members.append((name, type))
name = None
mo = rx_name.search(line)
assert mo is not None
name = mo.group(1)
return Struct(name, pyhead, members)
"""Rudimentary parser for C struct definitions."""
import re
PyObject_HEAD = "PyObject_HEAD"
PyObject_VAR_HEAD = "PyObject_VAR_HEAD"
rx_name = re.compile("} (\w+);")
class Struct:
def __init__(self, name, head, members):
self.name = name
self.head = head
self.members = members
def parse(s):
"""Parse a C struct definition.
The parser is very restricted in what it will accept.
"""
lines = [_f for _f in s.split("\n") if _f] # get non-empty lines
assert lines[0].strip() == "typedef struct {"
pyhead = lines[1].strip()
assert (pyhead.startswith("PyObject") and
pyhead.endswith("HEAD"))
members = []
for line in lines[2:]:
line = line.strip()
if line.startswith("}"):
break
assert line.endswith(";")
line = line[:-1]
words = line.split()
name = words[-1]
type = " ".join(words[:-1])
if name[0] == "*":
name = name[1:]
type += " *"
members.append((name, type))
name = None
mo = rx_name.search(line)
assert mo is not None
name = mo.group(1)
return Struct(name, pyhead, members)
"""framer's C code templates.
Templates use the following variables:
FileName: name of the file that contains the C source code
ModuleName: name of the module, as in "import ModuleName"
ModuleDocstring: C string containing the module doc string
"""
module_start = '#include "Python.h"'
member_include = '#include "structmember.h"'
module_doc = """\
PyDoc_STRVAR(%(ModuleName)s_doc,
%(ModuleDocstring)s);
"""
methoddef_start = """\
static struct PyMethodDef %(MethodDefName)s[] = {"""
methoddef_def = """\
{"%(PythonName)s", (PyCFunction)%(CName)s, %(MethType)s},"""
methoddef_def_doc = """\
{"%(PythonName)s", (PyCFunction)%(CName)s, %(MethType)s,
%(DocstringVar)s},"""
methoddef_end = """\
{NULL, NULL}
};
"""
memberdef_start = """\
#define OFF(X) offsetof(%(StructName)s, X)
static struct PyMemberDef %(MemberDefName)s[] = {"""
memberdef_def_doc = """\
{"%(PythonName)s", %(Type)s, OFF(%(CName)s), %(Flags)s,
%(Docstring)s},"""
memberdef_def = """\
{"%(PythonName)s", %(Type)s, OFF(%(CName)s), %(Flags)s},"""
memberdef_end = """\
{NULL}
};
#undef OFF
"""
dealloc_func = """static void
%(name)s(PyObject *ob)
{
}
"""
docstring = """\
PyDoc_STRVAR(%(DocstringVar)s,
%(Docstring)s);
"""
funcdef_start = """\
static PyObject *
%(name)s(%(args)s)
{"""
funcdef_end = """\
}
"""
varargs = """\
if (!PyArg_ParseTuple(args, \"%(ArgParse)s:%(PythonName)s\",
%(ArgTargets)s))
return NULL;"""
module_init_start = """\
PyMODINIT_FUNC
PyInit_%(ModuleName)s(void)
{
PyObject *mod;
mod = Py_InitModule3("%(ModuleName)s", %(MethodDefName)s,
%(ModuleName)s_doc);
if (mod == NULL)
return;
"""
type_init_type = " %(CTypeName)s.ob_type = &PyType_Type;"
module_add_type = """\
if (!PyObject_SetAttrString(mod, "%(TypeName)s",
(PyObject *)&%(CTypeName)s))
return;
"""
type_struct_start = """\
static PyTypeObject %(CTypeName)s = {
PyObject_HEAD_INIT(0)"""
type_struct_end = """\
};
"""
def cstring(s, width=70):
"""Return C string representation of a Python string.
width specifies the maximum width of any line of the C string.
"""
L = []
for l in s.split("\n"):
if len(l) < width:
L.append(r'"%s\n"' % l)
return "\n".join(L)
def unindent(s, skipfirst=True):
"""Return an unindented version of a docstring.
Removes indentation on lines following the first one, using the
leading whitespace of the first indented line that is not blank
to determine the indentation.
"""
lines = s.split("\n")
if skipfirst:
first = lines.pop(0)
L = [first]
else:
L = []
indent = None
for l in lines:
ls = l.strip()
if ls:
indent = len(l) - len(ls)
break
L += [l[indent:] for l in lines]
return "\n".join(L)
world -- Print mappings between country names and DNS country codes.
Contact: Barry Warsaw
Email: bwarsaw@python.org
This script will take a list of Internet addresses and print out where in the
world those addresses originate from, based on the top-level domain country
code found in the address. Addresses can be in any of the following forms:
xx -- just the country code or top-level domain identifier
host.domain.xx -- any Internet host or network name
somebody@where.xx -- an Internet email address
If no match is found, the address is interpreted as a regular expression [*]
and a reverse lookup is attempted. This script will search the country names
and print a list of matching entries. You can force reverse mappings with the
`-r' flag (see below).
For example:
%% world tz us
tz originated from Tanzania, United Republic of
us originated from United States
%% world united
united matches 6 countries:
ae: United Arab Emirates
uk: United Kingdom (common practice)
um: United States Minor Outlying Islands
us: United States
tz: Tanzania, United Republic of
gb: United Kingdom
[*] Note that regular expressions must conform to Python 1.5's re.py module
syntax. The comparison is done with the search() method.
Country codes are maintained by the RIPE Network Coordination Centre,
in coordination with the ISO 3166 Maintenance Agency at DIN Berlin. The
authoritative source of counry code mappings is:
<url:ftp://info.ripe.net/iso3166-countrycodes>
The latest known change to this information was:
Thu Aug 7 17:59:51 MET DST 1997
This script also knows about non-geographic top-level domains.
Usage: world [-d] [-p file] [-o] [-h] addr [addr ...]
--dump
-d
Print mapping of all top-level domains.
--parse file
-p file
Parse an iso3166-countrycodes file extracting the two letter country
code followed by the country name. Note that the three letter country
codes and numbers, which are also provided in the standard format
file, are ignored.
--outputdict
-o
When used in conjunction with the `-p' option, output is in the form
of a Python dictionary, and country names are normalized
w.r.t. capitalization. This makes it appropriate for cutting and
pasting back into this file.
--reverse
-r
Force reverse lookup. In this mode the address can be any Python
regular expression; this is matched against all country names and a
list of matching mappings is printed. In normal mode (e.g. without
this flag), reverse lookup is performed on addresses if no matching
country code is found.
-h
--help
Print this message.
Local Variables:
indent-tabs-mode: nil
End:
This diff is collapsed.
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