diff --git a/CHANGE_LOG b/CHANGE_LOG index 67c4cc3f..82470a25 100644 --- a/CHANGE_LOG +++ b/CHANGE_LOG @@ -1,6 +1,8 @@ 2026-XX-XX 3.8.2: ------------------- * drop Python 3.6 support + * expose the `decodeiterator` class, and add the `skipbits` method and `index` + data descriptor 2026-04-02 3.8.1: diff --git a/bitarray/__init__.py b/bitarray/__init__.py index 9b797193..5f1de0b9 100644 --- a/bitarray/__init__.py +++ b/bitarray/__init__.py @@ -12,11 +12,12 @@ from collections import namedtuple from bitarray._bitarray import ( - bitarray, decodetree, bits2bytes, _bitarray_reconstructor, + bitarray, decodetree, bits2bytes, decodeiterator, _bitarray_reconstructor, get_default_endian, _sysinfo, BITARRAY_VERSION as __version__ ) -__all__ = ['bitarray', 'frozenbitarray', 'decodetree', 'bits2bytes'] +__all__ = ['bitarray', 'frozenbitarray', 'decodetree', 'bits2bytes', + 'decodeiterator'] BufferInfo = namedtuple('BufferInfo', ['address', 'nbytes', 'endian', 'padbits', diff --git a/bitarray/__init__.pyi b/bitarray/__init__.pyi index 46670a56..28945fdb 100644 --- a/bitarray/__init__.pyi +++ b/bitarray/__init__.pyi @@ -31,6 +31,13 @@ class decodetree: def todict(self) -> CodeDict: ... +class decodeiterator(Iterator): + @property + def index(self) -> int: ... + + def skipbits(self, count: int = ...) -> bitarray: ... + + class bitarray: def __init__(self, initializer: Union[int, str, Iterable[int], None] = ..., @@ -55,7 +62,7 @@ class bitarray: def encode(self, code: CodeDict, x: Iterable) -> None: ... def decode(self, - code: Union[CodeDict, decodetree]) -> Iterator: ... + code: Union[CodeDict, decodetree]) -> decodeiterator: ... def extend(self, x: Union[str, Iterable[int]]) -> None: ... def fill(self) -> int: ... diff --git a/bitarray/_bitarray.c b/bitarray/_bitarray.c index bff98b26..3ba6a41a 100644 --- a/bitarray/_bitarray.c +++ b/bitarray/_bitarray.c @@ -11,6 +11,7 @@ #define PY_SSIZE_T_CLEAN #include "Python.h" #include "pythoncapi_compat.h" +#include "structmember.h" #include "bitarray.h" /* size used when reading / writing blocks from files (in bytes) */ @@ -2081,18 +2082,13 @@ static PySequenceMethods bitarray_as_sequence = { /* ----------------------- bitarray_as_mapping ------------------------- */ -/* return new bitarray with item in self, specified by slice */ +/* return new bitarray with item in self, specified by slice indices */ static PyObject * -getslice(bitarrayobject *self, PyObject *slice) +getslice_indices(bitarrayobject *self, Py_ssize_t start, Py_ssize_t step, + Py_ssize_t slicelength) { - Py_ssize_t start, stop, step, slicelength; bitarrayobject *res; - assert(PySlice_Check(slice)); - if (PySlice_GetIndicesEx(slice, self->nbits, - &start, &stop, &step, &slicelength) < 0) - return NULL; - res = newbitarrayobject(Py_TYPE(self), slicelength, self->endian); if (res == NULL) return NULL; @@ -2109,6 +2105,20 @@ getslice(bitarrayobject *self, PyObject *slice) return freeze_if_frozen(res); } +/* return new bitarray with item in self, specified by slice */ +static PyObject * +getslice(bitarrayobject *self, PyObject *slice) +{ + Py_ssize_t start, stop, step, slicelength; + + assert(PySlice_Check(slice)); + if (PySlice_GetIndicesEx(slice, self->nbits, + &start, &stop, &step, &slicelength) < 0) + return NULL; + + return getslice_indices(self, start, step, slicelength); +} + static int ensure_mask_size(bitarrayobject *self, bitarrayobject *mask) { @@ -3319,7 +3329,7 @@ bitarray_decode(bitarrayobject *self, PyObject *obj) } PyDoc_STRVAR(decode_doc, -"decode(code, /) -> iterator\n\ +"decode(code, /) -> decodeiterator\n\ \n\ Given a prefix code (a dict mapping symbols to bitarrays, or `decodetree`\n\ object), decode content of bitarray and return an iterator over\n\ @@ -3359,6 +3369,52 @@ decodeiter_traverse(decodeiterobject *it, visitproc visit, void *arg) return 0; } +static PyObject * +decodeiter_skipbits(decodeiterobject *it, PyObject *args) +{ + Py_ssize_t count = 1; + if (!PyArg_ParseTuple(args, "|n:skipbits", &count)) { + return NULL; + } + + if (count < 0) { + return PyErr_Format(PyExc_ValueError, "negative skip count %zd", count); + } + + Py_ssize_t new_index = it->index + count; + if (new_index > it->self->nbits) { + return PyErr_Format(PyExc_ValueError, + "new index %zd out of range %zd", + new_index, it->self->nbits); + } + + PyObject *skipped = getslice_indices(it->self, it->index, 1, count); + it->index = new_index; + return skipped; +} + +PyDoc_STRVAR(decodeiter_skipbits_doc, +"skipbits(count=1, /) -> bitarray\n\ +\n\ +Skips over the next `count` bits (default 1) and returns them.\n\ +Raises `ValueError` if count is out of range."); + +static PyMethodDef decodeiter_methods[] = { + {"skipbits", (PyCFunction) decodeiter_skipbits, METH_VARARGS, + decodeiter_skipbits_doc}, + {NULL} +}; + +PyDoc_STRVAR(decodeiter_index_doc, +"The position in the underlying bitarray to be decoded by the subsequent\n" +"call to `next`."); + +static PyMemberDef decodeiter_members[] = { + {"index", Py_T_PYSSIZET, offsetof(decodeiterobject, index), Py_READONLY, + decodeiter_index_doc}, + {NULL} +}; + static PyTypeObject DecodeIter_Type = { PyVarObject_HEAD_INIT(NULL, 0) "bitarray.decodeiterator", /* tp_name */ @@ -3388,7 +3444,8 @@ static PyTypeObject DecodeIter_Type = { 0, /* tp_weaklistoffset */ PyObject_SelfIter, /* tp_iter */ (iternextfunc) decodeiter_next, /* tp_iternext */ - 0, /* tp_methods */ + decodeiter_methods, /* tp_methods */ + decodeiter_members, /* tp_members */ }; /*********************** (Bitarray) Search Iterator ***********************/ @@ -4283,6 +4340,8 @@ PyInit__bitarray(void) if (PyType_Ready(&DecodeIter_Type) < 0) return NULL; Py_SET_TYPE(&DecodeIter_Type, &PyType_Type); + Py_INCREF((PyObject *) &DecodeIter_Type); + PyModule_AddObject(m, "decodeiterator", (PyObject *) &DecodeIter_Type); if (PyType_Ready(&BitarrayIter_Type) < 0) return NULL; diff --git a/bitarray/test_bitarray.py b/bitarray/test_bitarray.py index 828e8368..65957979 100644 --- a/bitarray/test_bitarray.py +++ b/bitarray/test_bitarray.py @@ -4480,7 +4480,9 @@ def test_decode_type(self): a = bitarray('0110') it = a.decode(alphabet_code) self.assertIsType(it, 'decodeiterator') + self.assertEqual(it.index, 0) self.assertEqual(list(it), ['a']) + self.assertEqual(it.index, len(a)) def test_decode_remove(self): d = {'I': bitarray('1'), 'l': bitarray('01'), @@ -4598,6 +4600,29 @@ def test_decode_ambiguous_code(self): self.assertRaises(ValueError, a.decode, d) self.check_obj(a) + def test_decode_skipbits(self): + d = {'a': bitarray('0'), 'b': bitarray('10'), 'c': bitarray('11')} + a = bitarray('0 10 0 11 10101 0110') + it = a.decode(d) + self.assertEqual(it.index, 0) + self.assertEqual(next(it), 'a') + self.assertEqual(it.index, 1) + self.assertEqual(next(it), 'b') + self.assertEqual(it.index, 3) + self.assertEqual(it.skipbits(), bitarray('0')) + self.assertEqual(it.index, 4) + self.assertEqual(next(it), 'c') + self.assertEqual(it.index, 6) + self.assertEqual(it.skipbits(5), bitarray('10101')) + self.assertEqual(it.index, 11) + self.assertRaises(ValueError, it.skipbits, -1) + self.assertRaises(ValueError, it.skipbits, 5) + self.assertEqual(it.index, 11) + self.assertEqual(it.skipbits(4), bitarray('0110')) + self.assertEqual(it.index, 15) + self.assertRaises(StopIteration, next, it) + self.assertEqual(it.index, 15) + def test_miscitems(self): d = {None : bitarray('00'), 0 : bitarray('110'), diff --git a/update_doc.py b/update_doc.py index 07c251fa..4898b0b7 100644 --- a/update_doc.py +++ b/update_doc.py @@ -34,6 +34,8 @@ '3.0: returns iterator (equivalent to past ' '`.itersearch()`)'], 'bitarray.to01': '3.3: optional `group` and `sep` arguments', + 'decodeiterator.index': '3.8.2', + 'decodeiterator.skipbits':'3.8.2', 'decodetree': '1.6', 'frozenbitarray': '1.1', 'get_default_endian': '1.3', @@ -124,6 +126,7 @@ 'bitarray.nbytes': 'int', 'bitarray.padbits': 'int', 'bitarray.readonly': 'bool', + 'decodeiterator.index':'int', } _NAMES = set() @@ -146,7 +149,7 @@ def get_doc(name): lines = obj.__doc__.splitlines() - if len(lines) == 1: + if name in GETSET: sig = '``%s`` -> %s' % (obj.__name__, GETSET[name]) return sig, lines @@ -187,6 +190,27 @@ def write_doc(fo, name): fo.write('\n\n') +def write_reference_for_class(fo, cl): + class_name = cl.__name__ + heading = "%s methods:" % class_name + fo.write("%s\n%s\n\n" % (heading, '-' * len(heading))) + for method in sorted(dir(cl)): + if method.startswith('_'): + continue + name = '%s.%s' % (class_name, method) + if name not in GETSET: + write_doc(fo, name) + + heading = "%s data descriptors:" % class_name + fo.write("%s\n%s\n\n" % (heading, '-' * len(heading))) + if class_name == "bitarray": + fo.write("Data descriptors were added in version 2.6.\n\n") + for getset in sorted(dir(bitarray.bitarray)): + name = '%s.%s' % (class_name, getset) + if name in GETSET: + write_doc(fo, name) + + def write_reference(fo): fo.write("""\ Reference @@ -206,22 +230,8 @@ def write_reference(fo): """ % (bitarray.__version__, BASE_URL + "/blob/master/doc/changelog.rst")) write_doc(fo, 'bitarray') - fo.write("bitarray methods:\n" - "-----------------\n\n") - for method in sorted(dir(bitarray.bitarray)): - if method.startswith('_'): - continue - name = 'bitarray.%s' % method - if name not in GETSET: - write_doc(fo, name) - - fo.write("bitarray data descriptors:\n" - "--------------------------\n\n" - "Data descriptors were added in version 2.6.\n\n") - for getset in sorted(dir(bitarray.bitarray)): - name = 'bitarray.%s' % getset - if name in GETSET: - write_doc(fo, name) + write_reference_for_class(fo, bitarray.bitarray) + write_reference_for_class(fo, bitarray.decodeiterator) fo.write("Other objects:\n" "--------------\n\n")