chiark / gitweb /
@@@ mLib-python Pyke wip
authorMark Wooding <mdw@distorted.org.uk>
Sat, 28 Mar 2020 10:34:57 +0000 (10:34 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 11 Apr 2020 14:23:48 +0000 (15:23 +0100)
14 files changed:
.gitignore
TODO.org [new file with mode: 0644]
atom.c [new file with mode: 0644]
mLib-python.h [new file with mode: 0644]
mLib.c [new file with mode: 0644]
mLib/__init__.py [new file with mode: 0644]
setup.py
sys.c [new file with mode: 0644]
t/t-atom.py [new file with mode: 0644]
t/t-misc.py [new file with mode: 0644]
t/t-sys.py [new file with mode: 0644]
t/t-ui.py [new file with mode: 0644]
t/testutils.py [new file with mode: 0644]
ui.c [new file with mode: 0644]

index fd891649a01113dc4772198dd2e0e26f14b4482d..81aa8624f56d5a021b1ad7c59482ea07e36f2de0 100644 (file)
@@ -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 (file)
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 (file)
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 (file)
index 0000000..cd05d8d
--- /dev/null
@@ -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 <mLib/alloc.h>
+#include <mLib/assoc.h>
+#include <mLib/atom.h>
+#include <mLib/daemonize.h>
+#include <mLib/fdflags.h>
+#include <mLib/fdpass.h>
+#include <mLib/mdup.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+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 (file)
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 (file)
index 0000000..949a6c2
--- /dev/null
@@ -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 --------------------------------------------------
index 66aafc53b2e0a554ad1675ca83dd6614f6eb6e09..55fe09c006d876e1974aeaa5259f9f7ab2922480 100755 (executable)
--- 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 (file)
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 (file)
index 0000000..57fd3e3
--- /dev/null
@@ -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 (file)
index 0000000..66ea0e1
--- /dev/null
@@ -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 (file)
index 0000000..317d61d
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..f490def
--- /dev/null
@@ -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 (file)
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 -------------------------------------------------*/