Commit 1df3a954 authored by Kevin Modzelewski's avatar Kevin Modzelewski

Merge pull request #341 from undingen/cStringIO

Add cStringIO module + basic support for setting member descriptors
parents a8af1ea6 1cbf6512
......@@ -291,9 +291,9 @@ STDLIB_OBJS := stdlib.bc.o stdlib.stripped.bc.o
STDLIB_RELEASE_OBJS := stdlib.release.bc.o
ASM_SRCS := $(wildcard src/runtime/*.S)
STDMODULE_SRCS := errnomodule.c shamodule.c sha256module.c sha512module.c _math.c mathmodule.c md5.c md5module.c _randommodule.c _sre.c operator.c binascii.c pwdmodule.c posixmodule.c _struct.c datetimemodule.c _functoolsmodule.c _collectionsmodule.c itertoolsmodule.c resource.c signalmodule.c selectmodule.c fcntlmodule.c timemodule.c arraymodule.c zlibmodule.c _codecsmodule.c socketmodule.c unicodedata.c _weakref.c $(EXTRA_STDMODULE_SRCS)
STDMODULE_SRCS := errnomodule.c shamodule.c sha256module.c sha512module.c _math.c mathmodule.c md5.c md5module.c _randommodule.c _sre.c operator.c binascii.c pwdmodule.c posixmodule.c _struct.c datetimemodule.c _functoolsmodule.c _collectionsmodule.c itertoolsmodule.c resource.c signalmodule.c selectmodule.c fcntlmodule.c timemodule.c arraymodule.c zlibmodule.c _codecsmodule.c socketmodule.c unicodedata.c _weakref.c cStringIO.c $(EXTRA_STDMODULE_SRCS)
STDOBJECT_SRCS := structseq.c capsule.c stringobject.c exceptions.c unicodeobject.c unicodectype.c bytearrayobject.c bytes_methods.c weakrefobject.c $(EXTRA_STDOBJECT_SRCS)
STDPYTHON_SRCS := pyctype.c getargs.c formatter_string.c pystrtod.c dtoa.c formatter_unicode.c $(EXTRA_STDPYTHON_SRCS)
STDPYTHON_SRCS := pyctype.c getargs.c formatter_string.c pystrtod.c dtoa.c formatter_unicode.c structmember.c $(EXTRA_STDPYTHON_SRCS)
FROM_CPYTHON_SRCS := $(addprefix from_cpython/Modules/,$(STDMODULE_SRCS)) $(addprefix from_cpython/Objects/,$(STDOBJECT_SRCS)) $(addprefix from_cpython/Python/,$(STDPYTHON_SRCS))
# The stdlib objects have slightly longer dependency chains,
......
......@@ -15,13 +15,13 @@ endforeach(STDLIB_FILE)
add_custom_target(copy_stdlib ALL DEPENDS ${STDLIB_TARGETS})
# compile specified files in from_cpython/Modules
file(GLOB_RECURSE STDMODULE_SRCS Modules errnomodule.c shamodule.c sha256module.c sha512module.c _math.c mathmodule.c md5.c md5module.c _randommodule.c _sre.c operator.c binascii.c pwdmodule.c posixmodule.c _struct.c datetimemodule.c _functoolsmodule.c _collectionsmodule.c itertoolsmodule.c resource.c signalmodule.c selectmodule.c fcntlmodule.c timemodule.c arraymodule.c zlibmodule.c _codecsmodule.c socketmodule.c unicodedata.c _weakref.c)
file(GLOB_RECURSE STDMODULE_SRCS Modules errnomodule.c shamodule.c sha256module.c sha512module.c _math.c mathmodule.c md5.c md5module.c _randommodule.c _sre.c operator.c binascii.c pwdmodule.c posixmodule.c _struct.c datetimemodule.c _functoolsmodule.c _collectionsmodule.c itertoolsmodule.c resource.c signalmodule.c selectmodule.c fcntlmodule.c timemodule.c arraymodule.c zlibmodule.c _codecsmodule.c socketmodule.c unicodedata.c _weakref.c cStringIO.c)
# compile specified files in from_cpython/Objects
file(GLOB_RECURSE STDOBJECT_SRCS Objects structseq.c capsule.c stringobject.c exceptions.c unicodeobject.c unicodectype.c bytearrayobject.c bytes_methods.c weakrefobject.c)
# compile specified files in from_cpython/Python
file(GLOB_RECURSE STDPYTHON_SRCS Python getargs.c pyctype.c formatter_string.c pystrtod.c dtoa.c formatter_unicode.c)
file(GLOB_RECURSE STDPYTHON_SRCS Python getargs.c pyctype.c formatter_string.c pystrtod.c dtoa.c formatter_unicode.c structmember.c)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-missing-field-initializers -Wno-tautological-compare -Wno-type-limits")
add_library(FROM_CPYTHON OBJECT ${STDMODULE_SRCS} ${STDOBJECT_SRCS} ${STDPYTHON_SRCS})
// PYSTON_NOEXCEPT This file is originally from CPython 2.7, with modifications for Pyston
#ifndef Py_CSTRINGIO_H
#define Py_CSTRINGIO_H
#ifdef __cplusplus
extern "C" {
#endif
/*
This header provides access to cStringIO objects from C.
Functions are provided for calling cStringIO objects and
macros are provided for testing whether you have cStringIO
objects.
Before calling any of the functions or macros, you must initialize
the routines with:
PycString_IMPORT
This would typically be done in your init function.
*/
#define PycStringIO_CAPSULE_NAME "cStringIO.cStringIO_CAPI"
#define PycString_IMPORT \
PycStringIO = ((struct PycStringIO_CAPI*)PyCapsule_Import(\
PycStringIO_CAPSULE_NAME, 0))
/* Basic functions to manipulate cStringIO objects from C */
static struct PycStringIO_CAPI {
/* Read a string from an input object. If the last argument
is -1, the remainder will be read.
*/
int(*cread)(PyObject *, char **, Py_ssize_t);
/* Read a line from an input object. Returns the length of the read
line as an int and a pointer inside the object buffer as char** (so
the caller doesn't have to provide its own buffer as destination).
*/
int(*creadline)(PyObject *, char **);
/* Write a string to an output object*/
int(*cwrite)(PyObject *, const char *, Py_ssize_t);
/* Get the output object as a Python string (returns new reference). */
PyObject *(*cgetvalue)(PyObject *);
/* Create a new output object */
PyObject *(*NewOutput)(int);
/* Create an input object from a Python string
(copies the Python string reference).
*/
PyObject *(*NewInput)(PyObject *);
/* The Python types for cStringIO input and output objects.
Note that you can do input on an output object.
*/
PyTypeObject *InputType, *OutputType;
} *PycStringIO;
/* These can be used to test if you have one */
#define PycStringIO_InputCheck(O) \
(Py_TYPE(O)==PycStringIO->InputType)
#define PycStringIO_OutputCheck(O) \
(Py_TYPE(O)==PycStringIO->OutputType)
#ifdef __cplusplus
}
#endif
#endif /* !Py_CSTRINGIO_H */
// This file is originally from CPython 2.7, with modifications for Pyston
/* Map C struct members to Python object attributes */
#include "Python.h"
#include "structmember.h"
static PyObject *
listmembers(struct memberlist *mlist)
{
int i, n;
PyObject *v;
for (n = 0; mlist[n].name != NULL; n++)
;
v = PyList_New(n);
if (v != NULL) {
for (i = 0; i < n; i++)
PyList_SetItem(v, i,
PyString_FromString(mlist[i].name));
if (PyErr_Occurred()) {
Py_DECREF(v);
v = NULL;
}
else {
PyList_Sort(v);
}
}
return v;
}
PyObject *
PyMember_Get(const char *addr, struct memberlist *mlist, const char *name)
{
struct memberlist *l;
if (strcmp(name, "__members__") == 0)
return listmembers(mlist);
for (l = mlist; l->name != NULL; l++) {
if (strcmp(l->name, name) == 0) {
PyMemberDef copy;
copy.name = l->name;
copy.type = l->type;
copy.offset = l->offset;
copy.flags = l->flags;
copy.doc = NULL;
return PyMember_GetOne(addr, &copy);
}
}
PyErr_SetString(PyExc_AttributeError, name);
return NULL;
}
PyObject *
PyMember_GetOne(const char *addr, PyMemberDef *l)
{
PyObject *v;
if ((l->flags & READ_RESTRICTED) &&
PyEval_GetRestricted()) {
PyErr_SetString(PyExc_RuntimeError, "restricted attribute");
return NULL;
}
addr += l->offset;
switch (l->type) {
case T_BOOL:
v = PyBool_FromLong(*(char*)addr);
break;
case T_BYTE:
v = PyInt_FromLong(*(char*)addr);
break;
case T_UBYTE:
v = PyLong_FromUnsignedLong(*(unsigned char*)addr);
break;
case T_SHORT:
v = PyInt_FromLong(*(short*)addr);
break;
case T_USHORT:
v = PyLong_FromUnsignedLong(*(unsigned short*)addr);
break;
case T_INT:
v = PyInt_FromLong(*(int*)addr);
break;
case T_UINT:
v = PyLong_FromUnsignedLong(*(unsigned int*)addr);
break;
case T_LONG:
v = PyInt_FromLong(*(long*)addr);
break;
case T_ULONG:
v = PyLong_FromUnsignedLong(*(unsigned long*)addr);
break;
case T_PYSSIZET:
v = PyInt_FromSsize_t(*(Py_ssize_t*)addr);
break;
case T_FLOAT:
v = PyFloat_FromDouble((double)*(float*)addr);
break;
case T_DOUBLE:
v = PyFloat_FromDouble(*(double*)addr);
break;
case T_STRING:
if (*(char**)addr == NULL) {
Py_INCREF(Py_None);
v = Py_None;
}
else
v = PyString_FromString(*(char**)addr);
break;
case T_STRING_INPLACE:
v = PyString_FromString((char*)addr);
break;
case T_CHAR:
v = PyString_FromStringAndSize((char*)addr, 1);
break;
case T_OBJECT:
v = *(PyObject **)addr;
if (v == NULL)
v = Py_None;
Py_INCREF(v);
break;
case T_OBJECT_EX:
v = *(PyObject **)addr;
if (v == NULL)
PyErr_SetString(PyExc_AttributeError, l->name);
Py_XINCREF(v);
break;
#ifdef HAVE_LONG_LONG
case T_LONGLONG:
v = PyLong_FromLongLong(*(PY_LONG_LONG *)addr);
break;
case T_ULONGLONG:
v = PyLong_FromUnsignedLongLong(*(unsigned PY_LONG_LONG *)addr);
break;
#endif /* HAVE_LONG_LONG */
default:
PyErr_SetString(PyExc_SystemError, "bad memberdescr type");
v = NULL;
}
return v;
}
int
PyMember_Set(char *addr, struct memberlist *mlist, const char *name, PyObject *v)
{
struct memberlist *l;
for (l = mlist; l->name != NULL; l++) {
if (strcmp(l->name, name) == 0) {
PyMemberDef copy;
copy.name = l->name;
copy.type = l->type;
copy.offset = l->offset;
copy.flags = l->flags;
copy.doc = NULL;
return PyMember_SetOne(addr, &copy, v);
}
}
PyErr_SetString(PyExc_AttributeError, name);
return -1;
}
#define WARN(msg) \
do { \
if (PyErr_Warn(PyExc_RuntimeWarning, msg) < 0) \
return -1; \
} while (0)
int
PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
{
PyObject *oldv;
addr += l->offset;
if ((l->flags & READONLY))
{
PyErr_SetString(PyExc_TypeError, "readonly attribute");
return -1;
}
if ((l->flags & PY_WRITE_RESTRICTED) && PyEval_GetRestricted()) {
PyErr_SetString(PyExc_RuntimeError, "restricted attribute");
return -1;
}
if (v == NULL) {
if (l->type == T_OBJECT_EX) {
/* Check if the attribute is set. */
if (*(PyObject **)addr == NULL) {
PyErr_SetString(PyExc_AttributeError, l->name);
return -1;
}
}
else if (l->type != T_OBJECT) {
PyErr_SetString(PyExc_TypeError,
"can't delete numeric/char attribute");
return -1;
}
}
switch (l->type) {
case T_BOOL:{
if (!PyBool_Check(v)) {
PyErr_SetString(PyExc_TypeError,
"attribute value type must be bool");
return -1;
}
if (v == Py_True)
*(char*)addr = (char) 1;
else
*(char*)addr = (char) 0;
break;
}
case T_BYTE:{
long long_val = PyInt_AsLong(v);
if ((long_val == -1) && PyErr_Occurred())
return -1;
*(char*)addr = (char)long_val;
/* XXX: For compatibility, only warn about truncations
for now. */
if ((long_val > CHAR_MAX) || (long_val < CHAR_MIN))
WARN("Truncation of value to char");
break;
}
case T_UBYTE:{
long long_val = PyInt_AsLong(v);
if ((long_val == -1) && PyErr_Occurred())
return -1;
*(unsigned char*)addr = (unsigned char)long_val;
if ((long_val > UCHAR_MAX) || (long_val < 0))
WARN("Truncation of value to unsigned char");
break;
}
case T_SHORT:{
long long_val = PyInt_AsLong(v);
if ((long_val == -1) && PyErr_Occurred())
return -1;
*(short*)addr = (short)long_val;
if ((long_val > SHRT_MAX) || (long_val < SHRT_MIN))
WARN("Truncation of value to short");
break;
}
case T_USHORT:{
long long_val = PyInt_AsLong(v);
if ((long_val == -1) && PyErr_Occurred())
return -1;
*(unsigned short*)addr = (unsigned short)long_val;
if ((long_val > USHRT_MAX) || (long_val < 0))
WARN("Truncation of value to unsigned short");
break;
}
case T_INT:{
long long_val = PyInt_AsLong(v);
if ((long_val == -1) && PyErr_Occurred())
return -1;
*(int *)addr = (int)long_val;
if ((long_val > INT_MAX) || (long_val < INT_MIN))
WARN("Truncation of value to int");
break;
}
case T_UINT:{
unsigned long ulong_val = PyLong_AsUnsignedLong(v);
if ((ulong_val == (unsigned long)-1) && PyErr_Occurred()) {
/* XXX: For compatibility, accept negative int values
as well. */
PyErr_Clear();
ulong_val = PyLong_AsLong(v);
if ((ulong_val == (unsigned long)-1) &&
PyErr_Occurred())
return -1;
*(unsigned int *)addr = (unsigned int)ulong_val;
WARN("Writing negative value into unsigned field");
} else
*(unsigned int *)addr = (unsigned int)ulong_val;
if (ulong_val > UINT_MAX)
WARN("Truncation of value to unsigned int");
break;
}
case T_LONG:{
*(long*)addr = PyLong_AsLong(v);
if ((*(long*)addr == -1) && PyErr_Occurred())
return -1;
break;
}
case T_ULONG:{
*(unsigned long*)addr = PyLong_AsUnsignedLong(v);
if ((*(unsigned long*)addr == (unsigned long)-1)
&& PyErr_Occurred()) {
/* XXX: For compatibility, accept negative int values
as well. */
PyErr_Clear();
*(unsigned long*)addr = PyLong_AsLong(v);
if ((*(unsigned long*)addr == (unsigned long)-1)
&& PyErr_Occurred())
return -1;
WARN("Writing negative value into unsigned field");
}
break;
}
case T_PYSSIZET:{
*(Py_ssize_t*)addr = PyInt_AsSsize_t(v);
if ((*(Py_ssize_t*)addr == (Py_ssize_t)-1)
&& PyErr_Occurred())
return -1;
break;
}
case T_FLOAT:{
double double_val = PyFloat_AsDouble(v);
if ((double_val == -1) && PyErr_Occurred())
return -1;
*(float*)addr = (float)double_val;
break;
}
case T_DOUBLE:
*(double*)addr = PyFloat_AsDouble(v);
if ((*(double*)addr == -1) && PyErr_Occurred())
return -1;
break;
case T_OBJECT:
case T_OBJECT_EX:
Py_XINCREF(v);
oldv = *(PyObject **)addr;
*(PyObject **)addr = v;
Py_XDECREF(oldv);
break;
case T_CHAR:
if (PyString_Check(v) && PyString_Size(v) == 1) {
*(char*)addr = PyString_AsString(v)[0];
}
else {
PyErr_BadArgument();
return -1;
}
break;
case T_STRING:
case T_STRING_INPLACE:
PyErr_SetString(PyExc_TypeError, "readonly attribute");
return -1;
#ifdef HAVE_LONG_LONG
case T_LONGLONG:{
PY_LONG_LONG value;
*(PY_LONG_LONG*)addr = value = PyLong_AsLongLong(v);
if ((value == -1) && PyErr_Occurred())
return -1;
break;
}
case T_ULONGLONG:{
unsigned PY_LONG_LONG value;
/* ??? PyLong_AsLongLong accepts an int, but PyLong_AsUnsignedLongLong
doesn't ??? */
if (PyLong_Check(v))
*(unsigned PY_LONG_LONG*)addr = value = PyLong_AsUnsignedLongLong(v);
else
*(unsigned PY_LONG_LONG*)addr = value = PyInt_AsLong(v);
if ((value == (unsigned PY_LONG_LONG)-1) && PyErr_Occurred())
return -1;
break;
}
#endif /* HAVE_LONG_LONG */
default:
PyErr_Format(PyExc_SystemError,
"bad memberdescr type for %s", l->name);
return -1;
}
return 0;
}
......@@ -1471,6 +1471,10 @@ extern "C" PyObject* PyBuffer_FromMemory(void* ptr, Py_ssize_t size) noexcept {
Py_FatalError("unimplemented");
}
extern "C" int PyEval_GetRestricted(void) noexcept {
return 0; // We don't support restricted mode
}
BoxedModule* importTestExtension(const std::string& name) {
std::string pathname_name = "test/test_extension/" + name + ".pyston.so";
const char* pathname = pathname_name.c_str();
......
......@@ -1098,7 +1098,7 @@ void setupFile() {
file_cls->giveAttr("next", new BoxedFunction(boxRTFunction((void*)fileIterNext, STR, 1)));
file_cls->giveAttr("softspace",
new BoxedMemberDescriptor(BoxedMemberDescriptor::INT, offsetof(BoxedFile, f_softspace)));
new BoxedMemberDescriptor(BoxedMemberDescriptor::INT, offsetof(BoxedFile, f_softspace), false));
file_cls->giveAttr("__new__", new BoxedFunction(boxRTFunction((void*)fileNew, UNKNOWN, 3, 1, false, false),
{ boxStrConstant("r") }));
......
......@@ -175,13 +175,13 @@ extern "C" bool softspace(Box* b, bool newval) {
}
bool r;
Box* gotten = b->getattr("softspace");
Box* gotten = getattrInternal(b, "softspace", NULL);
if (!gotten) {
r = 0;
} else {
r = nonzero(gotten);
}
b->setattr("softspace", boxInt(newval), NULL);
setattrInternal(b, "softspace", boxInt(newval), NULL);
return r;
}
......@@ -853,9 +853,12 @@ Box* dataDescriptorInstanceSpecialCases(GetattrRewriteArgs* rewrite_args, const
if (rewrite_args) {
// TODO we could use offset as the index in the assembly lookup rather than hardcoding
// the value in the assembly and guarding on it be the same.
r_descr->addAttrGuard(offsetof(BoxedMemberDescriptor, offset), member_desc->offset);
// This could be optimized if addAttrGuard supported things < 64 bits
static_assert(sizeof(member_desc->offset) == 4, "assumed by assembly instruction below");
r_descr->getAttr(offsetof(BoxedMemberDescriptor, offset), Location::any(), assembler::MovType::ZLQ)
->addGuard(member_desc->offset);
static_assert(sizeof(member_desc->type) == 4, "assumed by assembly instruction below");
r_descr->getAttr(offsetof(BoxedMemberDescriptor, type), Location::any(), assembler::MovType::ZLQ)
->addGuard(member_desc->type);
......@@ -940,7 +943,6 @@ Box* dataDescriptorInstanceSpecialCases(GetattrRewriteArgs* rewrite_args, const
CASE_INTEGER_TYPE(LONGLONG, long long, PyLong_FromLongLong, long long)
CASE_INTEGER_TYPE(ULONGLONG, unsigned long long, PyLong_FromUnsignedLongLong, unsigned long long)
CASE_INTEGER_TYPE(PYSSIZET, Py_ssize_t, boxInt, Py_ssize_t)
case BoxedMemberDescriptor::STRING: {
if (rewrite_args) {
RewriterVar* r_interm = rewrite_args->obj->getAttr(member_desc->offset, rewrite_args->destination);
......@@ -1565,6 +1567,17 @@ bool dataDescriptorSetSpecialCases(Box* obj, Box* val, Box* descr, SetattrRewrit
getset_descr->set(obj, val, getset_descr->closure);
return true;
} else if (descr->cls == member_cls) {
BoxedMemberDescriptor* member_desc = static_cast<BoxedMemberDescriptor*>(descr);
PyMemberDef member_def;
memset(&member_def, 0, sizeof(member_def));
member_def.offset = member_desc->offset;
member_def.type = member_desc->type;
if (member_desc->readonly)
member_def.flags |= READONLY;
PyMember_SetOne((char*)obj, &member_def, val);
checkAndThrowCAPIException();
return true;
}
......@@ -1636,6 +1649,8 @@ void setattrInternal(Box* obj, const std::string& attr, Box* val, SetattrRewrite
// We don't need to to the invalidation stuff in this case.
return;
} else {
if (!obj->cls->instancesHaveHCAttrs() && !obj->cls->instancesHaveDictAttrs())
raiseAttributeError(obj, attr.c_str());
obj->setattr(attr, val, rewrite_args);
}
......@@ -1670,10 +1685,6 @@ extern "C" void setattr(Box* obj, const char* attr, Box* attr_val) {
static StatCounter slowpath_setattr("slowpath_setattr");
slowpath_setattr.log();
if (!obj->cls->instancesHaveHCAttrs() && !obj->cls->instancesHaveDictAttrs()) {
raiseAttributeError(obj, attr);
}
if (obj->cls == type_cls) {
BoxedClass* cobj = static_cast<BoxedClass*>(obj);
if (!isUserDefined(cobj)) {
......
......@@ -67,6 +67,7 @@ extern "C" void init_socket();
extern "C" void _PyUnicode_Init();
extern "C" void initunicodedata();
extern "C" void init_weakref();
extern "C" void initcStringIO();
namespace pyston {
......@@ -1379,8 +1380,8 @@ void setupRuntime() {
function_cls->giveAttr("__name__", new (pyston_getset_cls) BoxedGetsetDescriptor(funcName, funcSetName, NULL));
function_cls->giveAttr("__repr__", new BoxedFunction(boxRTFunction((void*)functionRepr, STR, 1)));
function_cls->giveAttr("__module__",
new BoxedMemberDescriptor(BoxedMemberDescriptor::OBJECT, offsetof(BoxedFunction, modname)));
function_cls->giveAttr("__module__", new BoxedMemberDescriptor(BoxedMemberDescriptor::OBJECT,
offsetof(BoxedFunction, modname), false));
function_cls->giveAttr("__get__", new BoxedFunction(boxRTFunction((void*)functionGet, UNKNOWN, 3)));
function_cls->giveAttr("__call__",
new BoxedFunction(boxRTFunction((void*)functionCall, UNKNOWN, 1, 0, true, true)));
......@@ -1473,6 +1474,7 @@ void setupRuntime() {
init_socket();
initunicodedata();
init_weakref();
initcStringIO();
// some additional setup to ensure weakrefs participate in our GC
BoxedClass* weakref_ref_cls = &_PyWeakref_RefType;
......
......@@ -495,9 +495,12 @@ public:
} type;
int offset;
bool readonly;
BoxedMemberDescriptor(MemberType type, int offset) : type(type), offset(offset) {}
BoxedMemberDescriptor(PyMemberDef* member) : type((MemberType)member->type), offset(member->offset) {}
BoxedMemberDescriptor(MemberType type, int offset, bool readonly = true)
: type(type), offset(offset), readonly(readonly) {}
BoxedMemberDescriptor(PyMemberDef* member)
: type((MemberType)member->type), offset(member->offset), readonly(member->flags & READONLY) {}
DEFAULT_CLASS_SIMPLE(member_cls);
};
......
......@@ -17,3 +17,8 @@ print (0.5j + 1.5).real
print (0.5j + 1.5).imag
print complex(1, 1.0) / 2.0
try:
complex(1, 1.0).real = 1
except TypeError, e:
print e
import cStringIO
output = cStringIO.StringIO()
output.write("Hello\n")
print >>output, "World"
print output.getvalue()
output.close()
......@@ -50,5 +50,6 @@ except IOError as e:
f = open("/dev/null", "w")
print f.write("hello world")
f.softspace = 0
print f.flush()
print f.close()
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