From 81f68b64afdb80f8ee34e1294ecc5fb452a3e58b Mon Sep 17 00:00:00 2001 Message-Id: <81f68b64afdb80f8ee34e1294ecc5fb452a3e58b.1717797995.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sat, 28 Mar 2020 10:34:57 +0000 Subject: [PATCH] @@@ mLib-python Pyke wip Organization: Straylight/Edgeware From: Mark Wooding --- .gitignore | 20 +- TODO.org | 75 ++++++ atom.c | 587 +++++++++++++++++++++++++++++++++++++++++++++++ mLib-python.h | 69 ++++++ mLib.c | 88 +++++++ mLib/__init__.py | 93 ++++++++ setup.py | 21 +- sys.c | 206 +++++++++++++++++ t/t-atom.py | 104 +++++++++ t/t-misc.py | 41 ++++ t/t-sys.py | 109 +++++++++ t/t-ui.py | 70 ++++++ t/testutils.py | 263 +++++++++++++++++++++ ui.c | 112 +++++++++ 14 files changed, 1835 insertions(+), 23 deletions(-) create mode 100644 TODO.org create mode 100644 atom.c create mode 100644 mLib-python.h create mode 100644 mLib.c create mode 100644 mLib/__init__.py create mode 100644 sys.c create mode 100644 t/t-atom.py create mode 100644 t/t-misc.py create mode 100644 t/t-sys.py create mode 100644 t/t-ui.py create mode 100644 t/testutils.py create mode 100644 ui.c diff --git a/.gitignore b/.gitignore index fd89164..81aa862 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,10 @@ -base32.pyx -base64.pyx -hex.pyx -build -MANIFEST -dist -mLib.c -COPYING -mdwsetup.py *.pyc -auto-version -pysetup.mk + +/COPYING +/MANIFEST +/TODO.pdf +/TODO.tex +/auto-version +/build/ +/mdwsetup.py +/pysetup.mk diff --git a/TODO.org b/TODO.org new file mode 100644 index 0000000..1f1402f --- /dev/null +++ b/TODO.org @@ -0,0 +1,75 @@ +#+TITLE: mLib-python progress + +* [0/2] =buf= + + [ ] =buf/lbuf.h= :: + + [ ] =buf/pkbuf.h= :: + +* [0/5] =codec= + + [ ] =codec/base32.h= :: + + [ ] =codec/base64.h= :: + + [ ] =codec/codec.h= :: + + [ ] =codec/hex.h= :: + + [ ] =codec/url.h= :: + +* [0/2] =hash= + + [ ] =hash/crc32.h= :: + + [ ] =hash/unihash.h= :: + +* [4/4] =mem= + + [X] =mem/alloc.h= :: pointless + + [X] =mem/arena.h= :: pointless + + [X] =mem/pool.h= :: pointless + + [X] =mem/sub.h= :: pointless + +* [0/7] =sel= + + [ ] =sel/bres.h= :: + + [ ] =sel/conn.h= :: + + [ ] =sel/ident.h= :: + + [ ] =sel/sel.h= :: + + [ ] =sel/selbuf.h= :: + + [ ] =sel/selpk.h= :: + + [ ] =sel/sig.h= :: + +* [5/8] =struct= + + [X] =struct/assoc.h= :: + + [X] =struct/atom.h= :: + + [ ] =struct/buf.h= :: + + [ ] =struct/darray.h= :: + + [X] =struct/dspool.h= :: pointless + + [X] =struct/dstr.h= :: pointless + + [X] =struct/hash.h= :: pointless + + [ ] =struct/sym.h= :: + +* [5/8] =sys= + + [X] =sys/daemonize.h= :: + + [X] =sys/env.h= :: pointless + + [X] =sys/fdflags.h= :: + + [X] =sys/fdpass.h= :: + + [ ] =sys/fwatch.h= :: + + [ ] =sys/lock.h= :: + + [X] =sys/mdup.h= :: + + [ ] =sys/tv.h= :: + +* [0/1] =test= + + [ ] =test/testrig.h= :: + +* [0/1] =trace= + + [ ] =trace/trace.h= :: + +* [2/3] =ui= + + [ ] =ui/mdwopt.h= :: interface needs thinking about + + [X] =ui/quis.h= :: + + [X] =ui/report.h= :: + +* [5/7] =utils= + + [X] =utils/align.h= :: not applicable + + [X] =utils/bits.h= :: pointless + + [X] =utils/compiler.h= :: pointless + + [X] =utils/exc.h= :: pointless + + [X] =utils/macros.h= :: pointless + + [ ] =utils/str.h= :: + + [ ] =utils/versioncmp.h= :: + +* COMMENT Emacs cruft + +#+LaTeX_CLASS: strayman diff --git a/atom.c b/atom.c new file mode 100644 index 0000000..1046ef2 --- /dev/null +++ b/atom.c @@ -0,0 +1,587 @@ +/* -*-c-*- + * + * Atoms and obarrays + * + * (c) 2019 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Python interface to mLib. + * + * mLib/Python is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * mLib/Python is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mLib/Python. If not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +/*----- Header files ------------------------------------------------------*/ + +#include "mLib-python.h" + +/*----- Atoms -------------------------------------------------------------*/ + +typedef struct { + PyObject_HEAD + atom *a; + PyObject *oawk; +} atom_pyobj; +static PyTypeObject *atom_pytype; +#define ATOM_PYCHECK(o) PyObject_TypeCheck((o), atom_pytype) +#define ATOM_A(o) (((atom_pyobj *)(o))->a) +#define ATOM_OAWK(o) (((atom_pyobj *)(o))->oawk) +#define ATOM_OAOBJ(o) (PyWeakref_GET_OBJECT(ATOM_OAWK(o))) + +struct obentry { + assoc_base _b; + PyObject *aobj; +}; + +typedef struct { + GMAP_PYOBJ_HEAD + atom_table tab; + assoc_table map; + PyObject *wkls; +} obarray_pyobj; +static PyTypeObject *obarray_pytype; +#define OBARRAY_PYCHECK(o) PyObject_TypeCheck((o), obarray_pytype) +#define OBARRAY_TAB(o) (&((obarray_pyobj *)(o))->tab) +#define OBARRAY_MAP(o) (&((obarray_pyobj *)(o))->map) + +static PyObject *default_obarray(void) +{ + PyObject *oa = 0, *rc = 0; + + if (!home_module) SYSERR("home module not set"); + oa = PyObject_GetAttrString(home_module, "DEFAULT_ATOMTABLE"); + if (!oa) goto end; + if (!OBARRAY_PYCHECK(oa)) TYERR("DEFAULT_ATOMTABLE isn't an AtomTable"); + rc = oa; oa = 0; +end: + Py_XDECREF(oa); + return (rc); +} + +static PyObject *atom_pywrap(PyObject *oaobj, atom *a) +{ + struct obentry *e; + PyObject *oawk = 0, *rc = 0; + atom_pyobj *aobj; + unsigned f; + + e = assoc_find(OBARRAY_MAP(oaobj), a, sizeof(*e), &f); + if (f) + rc = e->aobj; + else { + oawk = PyWeakref_NewRef(oaobj, 0); if (!oawk) goto end; + aobj = PyObject_NEW(atom_pyobj, atom_pytype); + aobj->a = a; + aobj->oawk = oawk; oawk = 0; + e->aobj = (PyObject *)aobj; + rc = (PyObject *)aobj; + } + Py_INCREF(rc); +end: + Py_XDECREF(oawk); + if (e && !rc) assoc_remove(OBARRAY_MAP(oaobj), e); + return (rc); +} + +static PyObject *atom_pyintern(PyObject *oaobj, PyObject *x) +{ + atom *a; + const char *p; + size_t sz; + PyObject *rc = 0; + + if (ATOM_PYCHECK(x)) { + if (ATOM_OAOBJ(x) != oaobj) VALERR("wrong table for existing atom"); + RETURN_OBJ(x); + } + if (x == Py_None) + a = atom_gensym(OBARRAY_TAB(oaobj)); + else if (TEXT_CHECK(x)) + { TEXT_PTRLEN(x, p, sz); a = atom_nintern(OBARRAY_TAB(oaobj), p, sz); } + else + TYERR("expected string or `None'"); + rc = atom_pywrap(oaobj, a); +end: + return (rc); +} + +static PyObject *atom_pynew(PyTypeObject *cls, PyObject *arg, PyObject *kw) +{ + static const char *const kwlist[] = { "name", "table", 0 }; + PyObject *name = Py_None, *oaobj = 0, *rc = 0; + + if (!PyArg_ParseTupleAndKeywords(arg, kw, "|OO!:new", KWLIST, + &name, obarray_pytype, &oaobj)) + { oaobj = 0; goto end; } + if (oaobj) Py_INCREF(oaobj); + else { oaobj = default_obarray(); if (!oaobj) goto end; } + rc = atom_pyintern(oaobj, name); +end: + Py_XDECREF(oaobj); + return (rc); +} + +static void atom_pydealloc(PyObject *me) + { assert(!ATOM_OAWK(me)); FREEOBJ(me); } + +static int atom_check(PyObject *me) +{ + if (!ATOM_A(me)) VALERR("atom is stale"); + return (0); +end: + return (-1); +} + +static PyObject *atomget_name(PyObject *me, void *hunoz) +{ + atom *a; + + if (atom_check(me)) return (0); + a = ATOM_A(me); return (TEXT_FROMSTRLEN(ATOM_NAME(a), ATOM_LEN(a))); +} + +static PyObject *atomget_home(PyObject *me, void *hunoz) +{ + PyObject *rc; + + if (atom_check(me)) return (0); + rc = ATOM_OAOBJ(me); assert(rc != Py_None); RETURN_OBJ(rc); +} + +static PyObject *atomget_internedp(PyObject *me, void *hunoz) +{ + if (atom_check(me)) return (0); + return (getbool(!(ATOM_A(me)->f&ATOMF_GENSYM))); +} + +static PyObject *atomget_livep(PyObject *me, void *hunoz) + { return (getbool(!!ATOM_A(me))); } + +static PyObject *atom_pyrichcompare(PyObject *x, PyObject *y, int op) +{ + int r; + + switch (op) { + case Py_EQ: r = (x == y); break; + case Py_NE: r = (x != y); break; + default: TYERR("atoms are unordered"); + } + return (getbool(r)); +end: + return (0); +} + +static Py_hash_t atom_pyhash(PyObject *me) + { return (atom_check(me) ? -1 : ATOM_HASH(ATOM_A(me))); } + +static const PyGetSetDef atom_pygetset[] = { +#define GETSETNAME(op, name) atom##op##_##name + GET (name, "A.name -> STR: atom name") + GET (home, "A.home -> ATAB: atom home table") + GET (internedp, "A.internedp -> BOOL: atom interned (not gensym)?") + GET (livep, "A.livep -> BOOL: atom table still alive?") +#undef GETSETNAME + { 0 } +}; + +static const PyTypeObject atom_pytype_skel = { + PyVarObject_HEAD_INIT(0, 0) /* Header */ + "Atom", /* @tp_name@ */ + sizeof(atom_pyobj), /* @tp_basicsize@ */ + 0, /* @tp_itemsize@ */ + + atom_pydealloc, /* @tp_dealloc@ */ + 0, /* @tp_print@ */ + 0, /* @tp_getattr@ */ + 0, /* @tp_setattr@ */ + 0, /* @tp_compare@ */ + 0, /* @tp_repr@ */ + 0, /* @tp_as_number@ */ + 0, /* @tp_as_sequence@ */ + 0, /* @tp_as_mapping@ */ + atom_pyhash, /* @tp_hash@ */ + 0, /* @tp_call@ */ + 0, /* @tp_str@ */ + 0, /* @tp_getattro@ */ + 0, /* @tp_setattro@ */ + 0, /* @tp_as_buffer@ */ + Py_TPFLAGS_DEFAULT | /* @tp_flags@ */ + Py_TPFLAGS_BASETYPE, + + /* @tp_doc@ */ + "Atom([name = None], [table = DEFAULT_ATOMTABLE])", + + 0, /* @tp_traverse@ */ + 0, /* @tp_clear@ */ + atom_pyrichcompare, /* @tp_richcompare@ */ + 0, /* @tp_weaklistoffset@ */ + 0, /* @tp_iter@ */ + 0, /* @tp_iternext@ */ + 0, /* @tp_methods@ */ + 0, /* @tp_members@ */ + PYGETSET(atom), /* @tp_getset@ */ + 0, /* @tp_base@ */ + 0, /* @tp_dict@ */ + 0, /* @tp_descr_get@ */ + 0, /* @tp_descr_set@ */ + 0, /* @tp_dictoffset@ */ + 0, /* @tp_init@ */ + PyType_GenericAlloc, /* @tp_alloc@ */ + atom_pynew, /* @tp_new@ */ + 0, /* @tp_free@ */ + 0 /* @tp_is_gc@ */ +}; + +static void *obarray_gmlookup(PyObject *me, PyObject *key, unsigned *f) +{ + atom *a = 0; + const char *p; + size_t sz; + + if (!TEXT_CHECK(key)) TYERR("expected string"); + TEXT_PTRLEN(key, p, sz); a = sym_find(&OBARRAY_TAB(me)->t, p, sz, 0, f); +end: + return (a); +} + +static void obarray_gmiterinit(PyObject *me, void *i) + { atom_mkiter(i, OBARRAY_TAB(me)); } + +static void *obarray_gmiternext(PyObject *me, void *i) + { return (atom_next(i)); } + +static PyObject *obarray_gmentrykey(PyObject *me, void *e) + { return (TEXT_FROMSTRLEN(ATOM_NAME(e), ATOM_LEN(e))); } + +static PyObject *obarray_gmentryvalue(PyObject *me, void *e) + { return (atom_pywrap(me, e)); } + +static const gmap_ops obarray_gmops = { + sizeof(atom_iter), + obarray_gmlookup, + obarray_gmiterinit, + obarray_gmiternext, + obarray_gmentrykey, + obarray_gmentryvalue +}; + +static PyObject *obarray_pynew(PyTypeObject *cls, + PyObject *arg, PyObject *kw) +{ + obarray_pyobj *rc = 0; + static const char *const kwlist[] = { 0 }; + + if (!PyArg_ParseTupleAndKeywords(arg, kw, ":new", KWLIST)) goto end; + rc = PyObject_NEW(obarray_pyobj, cls); if (!rc) goto end; + rc->gmops = &obarray_gmops; rc->wkls = 0; + atom_createtable(&rc->tab); assoc_create(&rc->map); +end: + return ((PyObject *)rc); +} + +static void obarray_pydealloc(PyObject *me) +{ + assoc_iter it; + struct obentry *e; + + ASSOC_MKITER(&it, OBARRAY_MAP(me)); + for (;;) { + ASSOC_NEXT(&it, e); if (!e) break; + ATOM_A(e->aobj) = 0; + Py_DECREF(ATOM_OAWK(e->aobj)); ATOM_OAWK(e->aobj) = 0; + Py_DECREF(e->aobj); + } + assoc_destroy(OBARRAY_MAP(me)); + atom_destroytable(OBARRAY_TAB(me)); + FREEOBJ(me); +} + +static PyObject *oameth_intern(PyObject *me, PyObject *arg) +{ + char *p; + Py_ssize_t sz; + atom *a; + PyObject *rc = 0; + + if (!PyArg_ParseTuple(arg, "s#:intern", &p, &sz)) goto end; + a = atom_nintern(OBARRAY_TAB(me), p, sz); + rc = atom_pywrap(me, a); +end: + return (rc); +} + +static PyObject *oameth_gensym(PyObject *me) + { return (atom_pywrap(me, atom_gensym(OBARRAY_TAB(me)))); } + +static const PyMappingMethods obarray_pymapping = { + gmap_pysize, + atom_pyintern, + 0 +}; + +static const PyMethodDef obarray_pymethods[] = { + GMAP_ROMETHODS +#define METHNAME(name) oameth_##name + METH (intern, "ATAB.intern(STR) -> A: atom with given name") + NAMETH(gensym, "ATAB.gensym() -> A: fresh uninterned atom") +#undef METHNAME + { 0 } +}; + +static const PyTypeObject obarray_pytype_skel = { + PyVarObject_HEAD_INIT(0, 0) /* Header */ + "AtomTable", /* @tp_name@ */ + sizeof(obarray_pyobj), /* @tp_basicsize@ */ + 0, /* @tp_itemsize@ */ + + obarray_pydealloc, /* @tp_dealloc@ */ + 0, /* @tp_print@ */ + 0, /* @tp_getattr@ */ + 0, /* @tp_setattr@ */ + 0, /* @tp_compare@ */ + 0, /* @tp_repr@ */ + 0, /* @tp_as_number@ */ + PYSEQUENCE(gmap), /* @tp_as_sequence@ */ + PYMAPPING(obarray), /* @tp_as_mapping@ */ + 0, /* @tp_hash@ */ + 0, /* @tp_call@ */ + 0, /* @tp_str@ */ + 0, /* @tp_getattro@ */ + 0, /* @tp_setattro@ */ + 0, /* @tp_as_buffer@ */ + Py_TPFLAGS_DEFAULT | /* @tp_flags@ */ + Py_TPFLAGS_BASETYPE, + + /* @tp_doc@ */ + "AtomTable()", + + 0, /* @tp_traverse@ */ + 0, /* @tp_clear@ */ + 0, /* @tp_richcompare@ */ + offsetof(obarray_pyobj, wkls), /* @tp_weaklistoffset@ */ + gmap_pyiter, /* @tp_iter@ */ + 0, /* @tp_iternext@ */ + PYMETHODS(obarray), /* @tp_methods@ */ + 0, /* @tp_members@ */ + 0, /* @tp_getset@ */ + 0, /* @tp_base@ */ + 0, /* @tp_dict@ */ + 0, /* @tp_descr_get@ */ + 0, /* @tp_descr_set@ */ + 0, /* @tp_dictoffset@ */ + 0, /* @tp_init@ */ + PyType_GenericAlloc, /* @tp_alloc@ */ + obarray_pynew, /* @tp_new@ */ + 0, /* @tp_free@ */ + 0 /* @tp_is_gc@ */ +}; + +/*----- Association tables ------------------------------------------------*/ + +typedef struct { + GMAP_PYOBJ_HEAD + assoc_table t; + PyObject *oaobj; +} assoc_pyobj; +static PyTypeObject *assoc_pytype; +#define ASSOC_PYCHECK(o) PyObject_TypeCheck((o), assoc_pytype) +#define ASSOC_T(o) (&((assoc_pyobj *)(o))->t) +#define ASSOC_OAOBJ(o) (((assoc_pyobj *)(o))->oaobj) + +struct aentry { + assoc_base _b; + PyObject *obj; +}; + +static void *assoc_gmlookup(PyObject *me, PyObject *key, unsigned *f) +{ + struct aentry *e = 0; + char *p; + size_t sz; + atom *a; + + if (TEXT_CHECK(key)) { + TEXT_PTRLEN(key, p, sz); + a = atom_nintern(OBARRAY_TAB(ASSOC_OAOBJ(me)), p, sz); + } else if (ATOM_PYCHECK(key)) { + if (atom_check(key)) goto end; + if (ATOM_OAOBJ(key) != ASSOC_OAOBJ(me)) + VALERR("wrong atom table for assoc"); + a = ATOM_A(key); + } else + TYERR("expected atom or string"); + e = assoc_find(ASSOC_T(me), a, f ? sizeof(*e) : 0, f); + if (!e) goto end; + if (f && !*f) e->obj = 0; +end: + return (e); +} + +static void assoc_gmiterinit(PyObject *me, void *i) + { assoc_iter *it = i; ASSOC_MKITER(it, ASSOC_T(me)); } + +static void *assoc_gmiternext(PyObject *me, void *i) + { assoc_iter *it = i; void *e; ASSOC_NEXT(it, e); return (e); } + +static PyObject *assoc_gmentrykey(PyObject *me, void *e) + { return (atom_pywrap(ASSOC_OAOBJ(me), ASSOC_ATOM(e))); } + +static PyObject *assoc_gmentryvalue(PyObject *me, void *e) + { struct aentry *ae = e; RETURN_OBJ(ae->obj); } + +static int assoc_gmsetentry(PyObject *me, void *e, PyObject *val) +{ + struct aentry *ae = e; + + Py_XDECREF(ae->obj); Py_INCREF(val); ae->obj = val; + return (0); +} + +static int assoc_gmdelentry(PyObject *me, void *e) + { assoc_remove(ASSOC_T(me), e); return (0); } + +static const gmap_ops assoc_gmops = { + sizeof(atom_iter), + assoc_gmlookup, + assoc_gmiterinit, + assoc_gmiternext, + assoc_gmentrykey, + assoc_gmentryvalue, + assoc_gmsetentry, + assoc_gmdelentry +}; + +static PyObject *assoc_pynew(PyTypeObject *cls, PyObject *arg, PyObject *kw) +{ + PyObject *oaobj = 0, *map = Py_None; + assoc_pyobj *me = 0; + + if (!PyArg_ParseTuple(arg, "|OO!:new", &map, obarray_pytype, &oaobj)) + { oaobj = 0; goto end; } + if (oaobj) Py_INCREF(oaobj); + else { oaobj = default_obarray(); if (!oaobj) goto end; } + me = PyObject_NEW(assoc_pyobj, cls); + me->gmops = &assoc_gmops; + assoc_create(&me->t); + me->oaobj = oaobj; oaobj = 0; + if ((map != Py_None && gmap_pyupdate((PyObject *)me, map)) || + gmap_pyupdate((PyObject *)me, kw)) + { Py_DECREF(me); me = 0; goto end; } +end: + Py_XDECREF(oaobj); + return ((PyObject *)me); +} + +static void assoc_pydealloc(PyObject *me) +{ + assoc_iter it; + struct aentry *ae; + + ASSOC_MKITER(&it, ASSOC_T(me)); + for (;;) { + ASSOC_NEXT(&it, ae); if (!ae) break; + Py_DECREF(ae->obj); + } + Py_DECREF(ASSOC_OAOBJ(me)); + FREEOBJ(me); +} + +static Py_ssize_t assoc_pysize(PyObject *me) +{ + assoc_table *t = ASSOC_T(me); + return (SYM_LIMIT(t->t.mask + 1) - t->load); +} + +static const PyMemberDef assoc_pymembers[] = { +#define MEMBERSTRUCT assoc_pyobj + MEMRNM(table, T_OBJECT, oaobj, READONLY, + "AS.table -> ATAB: home atom table") +#undef MEMBERSTRUCT + { 0 } +}; + +static const PyMappingMethods assoc_pymapping = { + assoc_pysize, + gmap_pylookup, + gmap_pystore +}; + +static const PyTypeObject assoc_pytype_skel = { + PyVarObject_HEAD_INIT(0, 0) /* Header */ + "AssocTable", /* @tp_name@ */ + sizeof(assoc_pyobj), /* @tp_basicsize@ */ + 0, /* @tp_itemsize@ */ + + assoc_pydealloc, /* @tp_dealloc@ */ + 0, /* @tp_print@ */ + 0, /* @tp_getattr@ */ + 0, /* @tp_setattr@ */ + 0, /* @tp_compare@ */ + 0, /* @tp_repr@ */ + 0, /* @tp_as_number@ */ + PYSEQUENCE(gmap), /* @tp_as_sequence@ */ + PYMAPPING(assoc), /* @tp_as_mapping@ */ + 0, /* @tp_hash@ */ + 0, /* @tp_call@ */ + 0, /* @tp_str@ */ + 0, /* @tp_getattro@ */ + 0, /* @tp_setattro@ */ + 0, /* @tp_as_buffer@ */ + Py_TPFLAGS_DEFAULT | /* @tp_flags@ */ + Py_TPFLAGS_BASETYPE, + + /* @tp_doc@ */ + "AssocTable([MAP], [ATAB], [ATOM = VALUE, ...])", + + 0, /* @tp_traverse@ */ + 0, /* @tp_clear@ */ + 0, /* @tp_richcompare@ */ + 0, /* @tp_weaklistoffset@ */ + gmap_pyiter, /* @tp_iter@ */ + 0, /* @tp_iternext@ */ + PYMETHODS(gmap), /* @tp_methods@ */ + PYMEMBERS(assoc), /* @tp_members@ */ + 0, /* @tp_getset@ */ + 0, /* @tp_base@ */ + 0, /* @tp_dict@ */ + 0, /* @tp_descr_get@ */ + 0, /* @tp_descr_set@ */ + 0, /* @tp_dictoffset@ */ + 0, /* @tp_init@ */ + PyType_GenericAlloc, /* @tp_alloc@ */ + assoc_pynew, /* @tp_new@ */ + 0, /* @tp_free@ */ + 0 /* @tp_is_gc@ */ +}; + +/*----- Main code ---------------------------------------------------------*/ + +void atom_pyinit(void) +{ + INITTYPE(atom, root); + INITTYPE(obarray, root); + INITTYPE(assoc, root); +} + +void atom_pyinsert(PyObject *mod) +{ + INSERT("Atom", atom_pytype); + INSERT("AtomTable", obarray_pytype); + INSERT("AssocTable", assoc_pytype); +}; + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/mLib-python.h b/mLib-python.h new file mode 100644 index 0000000..cd05d8d --- /dev/null +++ b/mLib-python.h @@ -0,0 +1,69 @@ +/* -*-c-*- + * + * Definitions for mLib bindings + * + * (c) 2019 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Python interface to mLib. + * + * mLib/Python is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * mLib/Python is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mLib/Python. If not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef MLIB_PYTHON_H +#define MLIB_PYTHON_H + +#ifdef __cplusplus + extern "C" { +#endif + +/*----- Header files ------------------------------------------------------*/ + +#include "pyke/pyke-mLib.h" + +PUBLIC_SYMBOLS; +#include +#include +#include +#include +#include +#include +#include +#include +#include +PRIVATE_SYMBOLS; + +/*----- Miscellaneous preliminaries ---------------------------------------*/ + +/* Submodules. */ +#define MODULES(_) \ + _(atom) _(sys) _(ui) \ + _(pyke_gmap) +MODULES(DECLARE_MODINIT) + +/*----- User interface functions ------------------------------------------*/ + +extern int ui_pyready(void); + +/*----- That's all, folks -------------------------------------------------*/ + +#ifdef __cplusplus + } +#endif + +#endif diff --git a/mLib.c b/mLib.c new file mode 100644 index 0000000..2acb4c6 --- /dev/null +++ b/mLib.c @@ -0,0 +1,88 @@ +/* -*-c-*- + * + * Where the fun begins + * + * (c) 2019 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Python interface to mLib. + * + * mLib/Python is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * mLib/Python is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mLib/Python. If not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +/*----- Header files ------------------------------------------------------*/ + +#include "mLib-python.h" + +/*----- Main code ---------------------------------------------------------*/ + +static PyObject *meth__ready(PyObject *me, PyObject *arg) +{ + PyObject *mod; + + if (!PyArg_ParseTuple(arg, "O!:_ready", &PyModule_Type, &mod)) + goto end; + Py_XDECREF(home_module); home_module = mod; Py_INCREF(home_module); + if (ui_pyready()) goto end; + RETURN_NONE; +end: + return (0); +} + +static const PyMethodDef methods[] = { +#define METHNAME(name) meth_##name + METH (_ready, 0) +#undef METHNAME + { 0 } +}; + +#ifdef PY3 +static PyModuleDef moddef = { + PyModuleDef_HEAD_INIT, + "mLib._base", /* @m_name@ */ + "Low-level module for mLib bindings. Use `mLib' instead.", + /* @m_doc@ */ + 0, /* @m_size@ */ + 0, /* @m_methods@ */ + 0, /* @m_slots@ */ + 0, /* @m_traverse@ */ + 0, /* @m_clear@ */ + 0 /* @m_free@ */ +}; +#endif + +EXPORT PyMODINIT_FUNC PY23(init_base, PyInit__base)(void) +{ + PyObject *mod; + + modname = TEXT_FROMSTR("mLib"); + addmethods(methods); + INIT_MODULES; +#ifdef PY3 + moddef.m_methods = donemethods(); + mod = PyModule_Create(&moddef); +#else + mod = Py_InitModule("mLib._base", donemethods()); +#endif + INSERT_MODULES; +#ifdef PY3 + return (mod); +#endif +} + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/mLib/__init__.py b/mLib/__init__.py new file mode 100644 index 0000000..949a6c2 --- /dev/null +++ b/mLib/__init__.py @@ -0,0 +1,93 @@ +### -*-python-*- +### +### Setup for mLib bindings +### +### (c) 2019 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to mLib. +### +### mLib/Python is free software: you can redistribute it and/or modify it +### under the terms of the GNU General Public License as published by the +### Free Software Foundation; either version 2 of the License, or (at your +### option) any later version. +### +### mLib/Python is distributed in the hope that it will be useful, but +### WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with mLib/Python. If not, write to the Free Software +### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +### USA. + +import sys as _sys +import types as _types + +###-------------------------------------------------------------------------- +### Import the main C extension module. + +if _sys.version_info >= (3,): from . import _base +else: import _base + +###-------------------------------------------------------------------------- +### Basic stuff. + +## Register our module. +_base._ready(_sys.modules[__name__]) +def default_lostexchook(why, ty, val, tb): + """`mLib.lostexchook(WHY, TY, VAL, TB)' reports lost exceptions.""" + _sys.stderr.write("\n\n!!! LOST EXCEPTION: %s\n" % why) + _sys.excepthook(ty, val, tb) + _sys.stderr.write("\n") +lostexchook = default_lostexchook + +## For the benefit of the reporter functions, we need the program name. +_base.ego(_sys.argv[0]) + +## Initialize the module. +def _init(): + d = globals() + b = _base.__dict__; + for i in b: + if i[0] != '_': d[i] = b[i]; +_init() + +## A handy function for our work: add the methods of a named class to an +## existing class. This is how we write the Python-implemented parts of our +## mostly-C types. +def _augment(c, cc): + for i in cc.__dict__: + a = cc.__dict__[i] + if type(a) is _types.MethodType: + a = a.im_func + elif type(a) not in (_types.FunctionType, staticmethod, classmethod): + continue + setattr(c, i, a) + +###-------------------------------------------------------------------------- +### Atoms. + +DEFAULT_ATOMTABLE = AtomTable() + +if _sys.version_info >= (3,): + def atoms(): return iter(DEFAULT_ATOMTABLE.values()) +else: + def atoms(): return DEFAULT_ATOMTABLE.itervalues() + +class _tmp: + def __repr__(me): return "Atom(%r)" % me.name + __str__ = __repr__ +_augment(Atom, _tmp) + +###-------------------------------------------------------------------------- +### User-interface funtions. + +def pquis(msg, file = _sys.stdout): + "pquis(MSG, [file = sys.stdout]): write MSG, replacing `$' by program name" + file.write(msg.replace("$", quis)) + +###----- That's all, folks -------------------------------------------------- diff --git a/setup.py b/setup.py index 66aafc5..55fe09c 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,13 @@ #! /usr/bin/python import distutils.core as DC -import Pyrex.Distutils as PXD import mdwsetup as MS -MS.pkg_config('mLib', '2.1.0') +MS.pkg_config('mLib', '2.2.2.1') -mLib = DC.Extension('mLib', ['mLib.pyx', 'atom-base.c', 'array.c'], +mLib = DC.Extension('mLib._base', + ['mLib.c', 'atom.c', 'sys.c', 'ui.c', + 'pyke/pyke.c', 'pyke/mapping.c'], ##extra_compile_args = ['-O0'], include_dirs = MS.uniquify(MS.INCLUDEDIRS), library_dirs = MS.uniquify(MS.LIBDIRS), @@ -14,15 +15,11 @@ mLib = DC.Extension('mLib', ['mLib.pyx', 'atom-base.c', 'array.c'], MS.setup(name = 'mLib-python', description = 'Python interface to mLib utilities library', + url = 'https://git.distorted.org.uk/~mdw/mLib-python/', author = 'Straylight/Edgeware', author_email = 'mdw@distorted.org.uk', license = 'GNU General Public License', - ext_modules = [mLib], - genfiles = [MS.Derive('base64.pyx', 'codec.pyx.in', - {'CLASS': 'Base64', 'PREFIX': 'base64'}), - MS.Derive('base32.pyx', 'codec.pyx.in', - {'CLASS': 'Base32', 'PREFIX': 'base32'}), - MS.Derive('hex.pyx', 'codec.pyx.in', - {'CLASS': 'Hex', 'PREFIX': 'hex'})], - cleanfiles = ['mLib.c'], - cmdclass = { 'build_ext': PXD.build_ext }) + packages = ['mLib'], + unittest_dir = "t", + unittests = ["t-misc", "t-atom", "t-ui"], + ext_modules = [mLib]) diff --git a/sys.c b/sys.c new file mode 100644 index 0000000..9dc59aa --- /dev/null +++ b/sys.c @@ -0,0 +1,206 @@ +/* -*-c-*- + * + * System-specific functionality + * + * (c) 2019 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Python interface to mLib. + * + * mLib/Python is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * mLib/Python is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mLib/Python. If not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +/*----- Header files ------------------------------------------------------*/ + +#include "mLib-python.h" + +/*----- Main code ---------------------------------------------------------*/ + +static int getint(PyObject *obj, int *i_out) +{ + PyObject *t = 0; + long i; + int rc = -1; + + t = PyNumber_Index(obj); if (!t) goto end; + i = PyInt_AsLong(t); if (i == -1 && PyErr_Occurred()) goto end; + if (INT_MIN > i || i > INT_MAX) OVFERR("out of range"); + *i_out = i; + rc = 0; +end: + Py_XDECREF(t); + return (rc); +} + +static int convfd(PyObject *obj, void *p) +{ + int *fd_out = p; + PyObject *t = 0; + int rc = 0; + + if (getint(obj, fd_out)) { + PyErr_Clear(); + t = PyObject_CallMethod(obj, "fileno", 0); if (!t) goto end; + if (getint(t, fd_out)) goto end; + } + rc = 1; +end: + Py_XDECREF(t); + return (rc); +} + +static PyObject *meth_detachtty(PyObject *me) + { detachtty(); RETURN_NONE; } + +static PyObject *meth_daemonize(PyObject *me) +{ + if (daemonize()) OSERR(0); + RETURN_NONE; +end: + return (0); +} + +static PyObject *meth_fdflags(PyObject *me, PyObject *arg, PyObject *kw) +{ + int fd; + unsigned fbic = 0, fxor = 0, fdbic = 0, fdxor = 0; + static const char *const kwlist[] = + { "file", "fbic", "fxor", "fdbic", "fdxor", 0 }; + + if (!PyArg_ParseTupleAndKeywords(arg, kw, "O&|O&O&O&O&:fdflags", KWLIST, + convfd, &fd, + convuint, &fbic, convuint, &fxor, + convuint, &fdbic, convuint, &fdxor)) + goto end; + if (fdflags(fd, fbic, fxor, fdbic, fdxor)) OSERR(0); + RETURN_NONE; +end: + return (0); +} + +static PyObject *meth_fdsend(PyObject *me, PyObject *arg, PyObject *kw) +{ + int sock, fd; + struct bin buf; + ssize_t n; + PyObject *rc = 0; + static const char *const kwlist[] = { "sock", "file", "buffer", 0 }; + + if (!PyArg_ParseTupleAndKeywords(arg, kw, "O&O&O&:fdsend", KWLIST, + convfd, &sock, convfd, &fd, + convbin, &buf)) + goto end; + n = fdpass_send(sock, fd, buf.p, buf.sz); if (n < 0) OSERR(0); + rc = PyInt_FromLong(n); +end: + return (rc); +} + +static PyObject *meth_fdrecv(PyObject *me, PyObject *arg, PyObject *kw) +{ + int sock, fd; + size_t sz; + ssize_t n; + void *p; + PyObject *buf = 0, *rc = 0; + static const char *const kwlist[] = { "sock", "size", 0 }; + + if (!PyArg_ParseTupleAndKeywords(arg, kw, "O&O&:fdrecv", KWLIST, + convfd, &sock, convszt, &sz)) + goto end; + BIN_PREPAREWRITE(buf, p, sz); + n = fdpass_recv(sock, &fd, p, sz); if (n < 0) OSERR(0); + BIN_DONEWRITE(buf, n); + rc = Py_BuildValue("(iN)", fd, buf); buf = 0; +end: + Py_XDECREF(buf); + return (rc); +} + +static PyObject *meth_mdup(PyObject *me, PyObject *arg) +{ + PyObject *v; + PyObject *t = 0, *u = 0, *rc = 0; + Py_ssize_t n, m; + mdup_fd *vv; + size_t i; + int err; + + if (!PyArg_ParseTuple(arg, "O:mdup", &v)) goto end; + n = PySequence_Size(v); if (n < 0) goto end; + vv = xmalloc(n*sizeof(*vv)); + for (i = 0; i < n; i++) { + t = PySequence_GetItem(v, i); if (!t) goto end; + m = PySequence_Size(t); if (m < 0) goto end; + if (m != 2) VALERR("expected a list of pairs"); + + u = PySequence_GetItem(t, 0); + if (getint(u, &vv[i].cur)) goto end; + Py_DECREF(u); u = 0; + + u = PySequence_GetItem(t, 1); + if (getint(u, &vv[i].want)) goto end; + Py_DECREF(u); u = 0; + + Py_DECREF(t); t = 0; + } + + err = mdup(vv, n); + + for (i = 0; i < n; i++) { + t = Py_BuildValue("(ii)", vv[i].cur, vv[i].want); + if (PySequence_SetItem(v, i, t)) goto end; + Py_DECREF(t); t = 0; + } + + if (err) OSERR(0); + rc = v; Py_INCREF(rc); + +end: + Py_XDECREF(t); Py_XDECREF(u); + return (rc); +} + +static const PyMethodDef methods[] = { +#define METHNAME(name) meth_##name + NAMETH(detachtty, "detachtty(): fork, detatch controlling terminal") + NAMETH(daemonize, "daemonize(): fork and become a daemon") + KWMETH(fdflags, "fdflags(FILE, [fbic = 0], [fxor = 0], " + "[fdbic = 0], [fdxor = 0])") + KWMETH(fdsend, "fdsend(FILE, FD, BUFFER) -> N") + KWMETH(fdrecv, "fdrecv(FILE, SIZE) -> FD, BUFFER") + METH (mdup, "mdup(LIST) -> LIST:\n" + " LIST is a list (mutable sequence) of pairs (CUR, WANT). Duplicate\n" + " each CUR file descriptor as WANT (may be -1 to mean `don't care'),\n" + " closing original CUR. Works even if there are cycles. LIST is\n" + " updated in place with CUR reflecting the new file descriptors even\n" + " on error. Returns the same LIST on success.") +#undef METHNAME + { 0 } +}; + +void sys_pyinit(void) +{ + addmethods(methods); +} + +void sys_pyinsert(PyObject *mod) +{ +} + +/*----- That's all, folks -------------------------------------------------*/ diff --git a/t/t-atom.py b/t/t-atom.py new file mode 100644 index 0000000..57fd3e3 --- /dev/null +++ b/t/t-atom.py @@ -0,0 +1,104 @@ +### -*-python-*- +### +### Test atoms and related functionality +### +### (c) 2019 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to mLib. +### +### mLib/Python is free software: you can redistribute it and/or modify it +### under the terms of the GNU General Public License as published by the +### Free Software Foundation; either version 2 of the License, or (at your +### option) any later version. +### +### mLib/Python is distributed in the hope that it will be useful, but +### WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with mLib/Python. If not, write to the Free Software +### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +### USA. + +import mLib as M +import testutils as T +import unittest as U + +###-------------------------------------------------------------------------- +class TestAtoms (U.TestCase): + + def test_simple(me): + foo = M.Atom("foo") + bar = M.Atom("bar") + me.assertTrue(foo is M.Atom("foo")) + me.assertTrue(foo is not bar) + me.assertEqual(foo, foo) + me.assertNotEqual(foo, bar) + me.assertEqual(set(M.atoms()), set([foo, bar])) + me.assertEqual(foo.name, "foo") + me.assertTrue(foo.internedp) + me.assertEqual(foo.home, M.DEFAULT_ATOMTABLE) + + def test_obarray(me): + tab = M.AtomTable() + foo = tab.intern("foo") + bar = M.Atom("bar", tab) + g0 = tab.gensym() + g1 = M.Atom(table = tab) + me.assertEqual(bar, tab.intern("bar")) + me.assertNotEqual(foo, M.Atom("foo")) + for sym in [foo, bar, g0, g1]: + me.assertEqual(sym.home, tab) + me.assertTrue(sym.livep) + for sym in [foo, bar]: me.assertTrue(sym.internedp) + for sym in [g0, g1]: me.assertFalse(sym.internedp) + me.assertEqual(tab["foo"], foo) + me.assertEqual(tab.get("spong"), None) + me.assertFalse("spong" in tab) + spong = tab["spong"] + me.assertTrue("spong" in tab) + me.assertEqual(set(T.itervalues(tab)), set([foo, bar, spong, g0, g1])) + del tab + for sym in [foo, bar, spong, g0, g1]: me.assertFalse(sym.livep) + me.assertRaises(ValueError, getattr, foo, "name") + + tab = M.AtomTable() + a = tab["a"] + me.assertRaises(TypeError, M.Atom, None, tab, foo = "extra!") + me.assertRaises(TypeError, M.Atom, table = tab, bar = "extra!") + me.assertRaises(TypeError, M.Atom, None, tab, "extra!") + me.assertTrue(a.livep) + del tab + me.assertFalse(a.livep) + + +###-------------------------------------------------------------------------- +class TestAssoc (T.MutableMappingTestMixin): + + def setUp(me): + me._obarray = M.AtomTable() + + def _mkkey(me, i): return me._obarray["k#%d" % i] + def _getkey(me, k): return int(k.name[2:]) + + def test_mapping(me): + me.check_mapping(lambda: M.AssocTable(None, me._obarray)) + + def test_assoc(me): + obarray = me._obarray + foo = obarray["foo"] + bar = obarray["bar"] + baz = obarray["baz"] + tab = M.AssocTable({ foo: 1 }, obarray, bar = 2) + me.assertEqual(tab.table, obarray) + me.assertEqual(tab["foo"], 1) + me.assertEqual(tab[bar], 2) + me.assertRaises(ValueError, tab.get, M.Atom("foo")) + +###----- That's all, folks -------------------------------------------------- + +if __name__ == "__main__": U.main() diff --git a/t/t-misc.py b/t/t-misc.py new file mode 100644 index 0000000..66ea0e1 --- /dev/null +++ b/t/t-misc.py @@ -0,0 +1,41 @@ +### -*- mode: python, coding: utf-8 -*- +### +### Miscellaneous tests +### +### (c) 2019 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to mLib. +### +### mLib/Python is free software: you can redistribute it and/or +### modify it under the terms of the GNU General Public License as +### published by the Free Software Foundation; either version 2 of the +### License, or (at your option) any later version. +### +### mLib/Python is distributed in the hope that it will be useful, but +### WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with mLib/Python. If not, write to the Free Software +### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +### USA. + +###-------------------------------------------------------------------------- +### Imported modules. + +import mLib as M +import unittest as U + +###-------------------------------------------------------------------------- +class Tests (U.TestCase): + + def test_path(me): + me.assertTrue(M.__file__.startswith("build")) + +###----- That's all, folks -------------------------------------------------- + +if __name__ == "__main__": U.main() diff --git a/t/t-sys.py b/t/t-sys.py new file mode 100644 index 0000000..317d61d --- /dev/null +++ b/t/t-sys.py @@ -0,0 +1,109 @@ +### -*-python-*- +### +### Test system-specific functionality +### +### (c) 2019 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to mLib. +### +### mLib/Python is free software: you can redistribute it and/or modify it +### under the terms of the GNU General Public License as published by the +### Free Software Foundation; either version 2 of the License, or (at your +### option) any later version. +### +### mLib/Python is distributed in the hope that it will be useful, but +### WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with mLib/Python. If not, write to the Free Software +### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +### USA. + +import mLib as M +import fcntl as FC +import os as OS +import socket as SK +import sys as SYS +import testutils as T +import unittest as U + +## Cygwin leaves cruft in the top bits of inode numbers for some reason. +if SYS.platform.startswith('cygwin'): + def _hack_inode(ino): return ino&0x0000ffffffffffff +else: + def _hack_inode(ino): return ino + +###-------------------------------------------------------------------------- +class TestSys (U.TestCase): + + def _fdid(me, fd): + st = OS.fstat(fd) + return st.st_dev, _hack_inode(st.st_ino) + + def _check_same_file(me, fd0, fd1): + me.assertEqual(me._fdid(fd0), me._fdid(fd1)) + + def _make_fd(me): + fd, other = OS.pipe() + OS.close(other) + return fd + + def test_fdflags(me): + fd = me._make_fd() + f = OS.fdopen(fd, "r") + try: + of, ofd = FC.fcntl(fd, FC.F_GETFL), FC.fcntl(fd, FC.F_GETFD) + M.fdflags(fd, fbic = OS.O_NONBLOCK, fxor = OS.O_NONBLOCK) + me.assertEqual(FC.fcntl(fd, FC.F_GETFL), of | OS.O_NONBLOCK) + M.fdflags(f, fbic = OS.O_NONBLOCK) + me.assertEqual(FC.fcntl(fd, FC.F_GETFL), of&~OS.O_NONBLOCK) + M.fdflags(fd, fdbic = FC.FD_CLOEXEC, fdxor = FC.FD_CLOEXEC) + me.assertEqual(FC.fcntl(fd, FC.F_GETFD), ofd | FC.FD_CLOEXEC) + M.fdflags(f, fdbic = FC.FD_CLOEXEC) + me.assertEqual(FC.fcntl(fd, FC.F_GETFD), ofd&~FC.FD_CLOEXEC) + me.assertRaises(AttributeError, M.fdflags, "bzzt") + me.assertRaises(OSError, M.fdflags, -1, 1, 1) + finally: + f.close() + + def test_fdpass(me): + sk0, sk1 = SK.socketpair(SK.AF_UNIX, SK.SOCK_STREAM) + fd = me._make_fd() + nfd = -1 + try: + msg = T.bin("some unnecessary message") + n = M.fdsend(sk0, fd, msg) + me.assertEqual(n, len(msg)) + nfd, buf = M.fdrecv(sk1, 32) + me.assertEqual(buf, msg) + me._check_same_file(fd, nfd) + OS.close(nfd); nfd = -1 + finally: + sk0.close(); sk1.close() + OS.close(fd) + if nfd != -1: OS.close(nfd) + + def test_mdup(me): + NFD = 5 + fds = [me._make_fd() for i in T.range(NFD)] + v = [(fds[i], fds[(i + 1)%5]) for i in T.range(NFD)] + try: + id = [me._fdid(fds[i]) for i in T.range(NFD)] + vv = M.mdup(v) + me.assertTrue(vv is v) + for i in T.range(NFD): + cur, want = v[i] + me.assertEqual(cur, want) + me.assertEqual(cur, fds[(i + 1)%NFD]) + me.assertEqual(me._fdid(cur), id[i]) + finally: + for fd, _ in v: OS.close(fd) + +###----- That's all, folks -------------------------------------------------- + +if __name__ == "__main__": U.main() diff --git a/t/t-ui.py b/t/t-ui.py new file mode 100644 index 0000000..60e49b6 --- /dev/null +++ b/t/t-ui.py @@ -0,0 +1,70 @@ +### -*-python-*- +### +### Test ui functionality +### +### (c) 2019 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to mLib. +### +### mLib/Python is free software: you can redistribute it and/or modify it +### under the terms of the GNU General Public License as published by the +### Free Software Foundation; either version 2 of the License, or (at your +### option) any later version. +### +### mLib/Python is distributed in the hope that it will be useful, but +### WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with mLib/Python. If not, write to the Free Software +### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +### USA. + +import mLib as M +import os as OS +import sys as SYS +import testutils as T +import unittest as U + +###-------------------------------------------------------------------------- +class TestUI (U.TestCase): + + def test_quis(me): + old = M.quis + M.ego(OS.path.join("test", "thing")); me.assertEqual(M.quis, "thing") + buf = T.StringIO(); M.pquis("a simple $ test", file = buf) + me.assertEqual(buf.getvalue(), "a simple thing test") + M.ego(OS.path.join("other-thing")); me.assertEqual(M.quis, "other-thing") + M.ego(old) + + def _capture_stderr(me): + pin, pout = OS.pipe(); OS.dup2(pout, 2); OS.close(pout) + return pin + + @T.subprocess + def test_report(me): + pin = me._capture_stderr() + ref = T.bin("%s: all ok really\n" % M.quis) + M.moan("all ok really") + stuff = OS.read(pin, 32) + assert stuff == ref + + @T.subprocess + def test_die(me): + pin = me._capture_stderr() + ref = T.bin("%s: so this is it\n" % M.quis) + try: M.die("so this is it", 123) + except SystemExit: + stuff = OS.read(pin, 32) + assert stuff == ref + assert SYS.exc_info()[1].code == 123 + else: + raise AssertionError("die didn't exit") + +###----- That's all, folks -------------------------------------------------- + +if __name__ == "__main__": U.main() diff --git a/t/testutils.py b/t/testutils.py new file mode 100644 index 0000000..f490def --- /dev/null +++ b/t/testutils.py @@ -0,0 +1,263 @@ +### -*-python-*- +### +### Test utilities +### +### (c) 2019 Straylight/Edgeware +### + +###----- Licensing notice --------------------------------------------------- +### +### This file is part of the Python interface to mLib. +### +### mLib/Python is free software: you can redistribute it and/or modify it +### under the terms of the GNU General Public License as published by the +### Free Software Foundation; either version 2 of the License, or (at your +### option) any later version. +### +### mLib/Python is distributed in the hope that it will be useful, but +### WITHOUT ANY WARRANTY; without even the implied warranty of +### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +### General Public License for more details. +### +### You should have received a copy of the GNU General Public License +### along with mLib/Python. If not, write to the Free Software +### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, +### USA. + +###-------------------------------------------------------------------------- +### Imported modules. + +import mLib as M +import contextlib as CTX +import os as OS +import sys as SYS +if SYS.version_info >= (3,): import builtins as B +else: import __builtin__ as B +import unittest as U + +###-------------------------------------------------------------------------- +### Main code. + +## Some compatibility hacks. +if SYS.version_info >= (3,): + PY2, PY3 = False, True + def bin(x): return x.encode('iso8859-1') + def py23(x, y): return y + range = range + byteseq = bytes + long = int + imap = map + def iterkeys(m): return m.keys() + def itervalues(m): return m.values() + def iteritems(m): return m.items() + from io import StringIO + MAXFIXNUM = SYS.maxsize +else: + import itertools as I + PY2, PY3 = True, False + def bin(x): return x + def py23(x, y): return x + range = xrange + long = long + imap = I.imap + def byteseq(seq): return "".join(map(chr, seq)) + def iterkeys(m): return m.iterkeys() + def itervalues(m): return m.itervalues() + def iteritems(m): return m.iteritems() + from cStringIO import StringIO + MAXFIXNUM = SYS.maxint + +def subprocess(fn): + def func(test): + for f in [SYS.stdout, SYS.stderr]: f.flush() + kid = OS.fork() + if kid == 0: + old_stderr_fd = OS.dup(2) + try: fn(test) + except: + for f in [SYS.stdout, SYS.stderr]: f.flush() + OS.dup2(old_stderr_fd, 2) + SYS.excepthook(*SYS.exc_info()) + SYS.stderr.flush() + OS._exit(111) + else: + for f in [SYS.stdout, SYS.stderr]: f.flush() + OS._exit(0) + _, st = OS.waitpid(kid, 0) + if OS.WIFSIGNALED(st): + test.fail("child killed by signal %d" % OS.WTERMSIG(st)) + elif not OS.WIFEXITED(st): + test.fail("child terminated with unknown status %x" % st) + else: + rc = OS.WEXITSTATUS(st) + if rc != 0: test.fail("child exited with status %d" % rc) + return func + + +class ImmutableMappingTextMixin (U.TestCase): + + ## Subclass stubs. + def _mkkey(me, i): return "k#%d" % i + def _getkey(me, k): return int(k[2:]) + def _getvalue(me, v): return int(v[2:]) + def _getitem(me, it): k, v = it; return me._getkey(k), me._getvalue(v) + + def check_immutable_mapping(me, map, model): + + ## Lookup. + limk = 0 + any = False + me.assertEqual(len(map), len(model)) + for k, v in iteritems(model): + any = True + if k >= limk: limk = k + 1 + me.assertTrue(me._mkkey(k) in map) + if PY2: me.assertTrue(map.has_key(me._mkkey(k))) + me.assertEqual(me._getvalue(map[me._mkkey(k)]), v) + me.assertEqual(me._getvalue(map.get(me._mkkey(k))), v) + if any: me.assertTrue(me._mkkey(k) in map) + if PY2: me.assertFalse(map.has_key(me._mkkey(limk))) + me.assertRaises(KeyError, lambda: map[me._mkkey(limk)]) + me.assertEqual(map.get(me._mkkey(limk)), None) + + if PY3: + empty = set() + + for k, v in iteritems(map): + me.assertTrue(k in map.keys()) + me.assertTrue((k, v) in map.items()) + me.assertFalse(me._mkkey(limk) in map.keys()) + + for viewfn, getfn in [(lambda x: x.keys(), me._getkey), + (lambda x: x.items(), me._getitem)]: + rview, rview2, mview = viewfn(map), viewfn(map), viewfn(model) + me.assertEqual(set(imap(getfn, rview)), set(mview)) + me.assertEqual(rview, rview2) + me.assertEqual(rview, set(rview2)) + me.assertEqual(rview | empty, set(rview)) + me.assertEqual(rview | rview2, set(rview)) + me.assertEqual(rview ^ empty, set(rview)) + me.assertEqual(rview ^ rview, empty) + me.assertEqual(rview & empty, empty) + me.assertEqual(len(rview), len(model)) + + if any: subset = set(rview2); subset.pop() + superset = set(rview2); superset.add(object()) + + me.assertFalse(rview < rview2) + me.assertTrue(rview < superset) + me.assertFalse(superset < rview) + me.assertFalse(rview < empty) + if any: + me.assertTrue(empty < rview) + me.assertTrue(subset < rview) + me.assertFalse(rview < subset) + + me.assertTrue(rview <= rview2) + me.assertTrue(rview <= superset) + me.assertFalse(superset <= rview) + if any: + me.assertTrue(empty <= rview) + me.assertFalse(rview <= empty) + me.assertTrue(subset <= rview) + me.assertFalse(rview <= subset) + + me.assertTrue(rview >= rview2) + me.assertTrue(superset >= rview) + me.assertFalse(rview >= superset) + if any: + me.assertTrue(rview >= empty) + me.assertFalse(empty >= rview) + me.assertTrue(rview >= subset) + me.assertFalse(subset >= rview) + + me.assertFalse(rview > rview2) + me.assertTrue(superset > rview) + me.assertFalse(rview > superset) + me.assertFalse(empty > rview) + if any: + me.assertTrue(rview > empty) + me.assertTrue(rview > subset) + me.assertFalse(subset > rview) + + else: + for listfn, getfn in [(lambda x: x.keys(), me._getkey), + (lambda x: x.values(), me._getvalue), + (lambda x: x.items(), me._getitem)]: + rlist, mlist = listfn(map), listfn(model) + me.assertEqual(type(rlist), list) + rlist = B.map(getfn, rlist) + rlist.sort(); mlist.sort(); me.assertEqual(rlist, mlist) + for iterfn, getfn in [(lambda x: x.iterkeys(), me._getkey), + (lambda x: x.itervalues(), me._getvalue), + (lambda x: x.iteritems(), me._getitem)]: + me.assertEqual(set(imap(getfn, iterfn(map))), set(iterfn(model))) + +class MutableMappingTestMixin (ImmutableMappingTextMixin): + + ## Subclass stubs. + def _mkvalue(me, i): return "v#%d" % i + + def check_mapping(me, emptymapfn): + + map = emptymapfn() + me.assertEqual(len(map), 0) + + if not PY3: + def check_views(): + me.check_immutable_mapping(map, model) + else: + kview, iview, vview = map.keys(), map.items(), map.values() + def check_views(): + me.check_immutable_mapping(map, model) + me.assertEqual(set(imap(me._getkey, kview)), model.keys()) + me.assertEqual(set(imap(me._getitem, iview)), model.items()) + me.assertEqual(set(imap(me._getvalue, vview)), set(model.values())) + + model = { 1: 101, 2: 202, 4: 404 } + for k, v in iteritems(model): map[me._mkkey(k)] = me._mkvalue(v) + check_views() + + model.update({ 2: 212, 6: 606, 7: 707 }) + map.update({ me._mkkey(2): me._mkvalue(212), + me._mkkey(6): me._mkvalue(606) }, + **{ me._mkkey(7): me._mkvalue(707) }) + check_views() + + model[9] = 909 + map[me._mkkey(9)] = me._mkvalue(909) + check_views() + + model[9] = 919 + map[me._mkkey(9)] = me._mkvalue(919) + check_views() + + map.setdefault(me._mkkey(9), me._mkvalue(929)) + check_views() + + model[8] = 808 + map.setdefault(me._mkkey(8), me._mkvalue(808)) + check_views() + + me.assertRaises(KeyError, map.pop, me._mkkey(5)) + obj = object() + me.assertEqual(map.pop(me._mkkey(5), obj), obj) + me.assertEqual(me._getvalue(map.pop(me._mkkey(8))), 808) + del model[8] + check_views() + + del model[9] + del map[me._mkkey(9)] + check_views() + + k, v = map.popitem() + mk, mv = me._getkey(k), me._getvalue(v) + me.assertEqual(model[mk], mv) + del model[mk] + check_views() + + map.clear() + model = {} + check_views() + +###----- That's all, folks -------------------------------------------------- diff --git a/ui.c b/ui.c new file mode 100644 index 0000000..82a1769 --- /dev/null +++ b/ui.c @@ -0,0 +1,112 @@ +/* -*-c-*- + * + * mLib user interface + * + * (c) 2019 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the Python interface to mLib. + * + * mLib/Python is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * mLib/Python is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with mLib/Python. If not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +/*----- Header files ------------------------------------------------------*/ + +#include "mLib-python.h" + +/*----- Program name ------------------------------------------------------*/ + +static int set_program_name(void) +{ + PyObject *p = TEXT_FROMSTR(pn__name); + int rc = -1; + + if (!home_module) SYSERR("home module not set"); + if (PyObject_SetAttrString(home_module, "quis", p)) goto end; + pn__name = TEXT_PTR(p); p = 0; rc = 0; +end: + Py_XDECREF(p); + return (rc); +} + +static PyObject *meth_ego(PyObject *me, PyObject *arg) +{ + char *p; + const char *old; + + if (!PyArg_ParseTuple(arg, "s:ego", &p)) goto end; + old = pn__name; ego(p); + if (set_program_name()) { pn__name = old; goto end; } + RETURN_NONE; +end: + return (0); +} + +/*----- Error reporting ---------------------------------------------------*/ + +static PyObject *meth_moan(PyObject *me, PyObject *arg) +{ + char *p; + + if (!PyArg_ParseTuple(arg, "s:moan", &p)) goto end; + moan("%s", p); + RETURN_NONE; +end: + return (0); +} + +static PyObject *meth_die(PyObject *me, PyObject *arg, PyObject *kw) +{ + const char *const kwlist[] = { "msg", "rc", 0 }; + char *p; + int rc = 126; + PyObject *rcobj = 0; + + if (!PyArg_ParseTupleAndKeywords(arg, kw, "s|i:moan", KWLIST, &p, &rc)) + goto end; + rcobj = PyInt_FromLong(rc); if (!rcobj) goto end; + moan("%s", p); + PyErr_SetObject(PyExc_SystemExit, rcobj); +end: + Py_XDECREF(rcobj); + return (0); +} + +/*----- Main code ---------------------------------------------------------*/ + +static const PyMethodDef methods[] = { +#define METHNAME(name) meth_##name + METH (ego, "ego(PROG): set program name") + METH (moan, "moan(MSG): report a warning") + KWMETH(die, "die(MSG, [rc = 126]): report a fatal error and exit") +#undef METHNAME + { 0 } +}; + +void ui_pyinit(void) +{ + addmethods(methods); +} + +void ui_pyinsert(PyObject *mod) +{ +} + +int ui_pyready(void) { return (set_program_name()); } + +/*----- That's all, folks -------------------------------------------------*/ -- [mdw]