nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: cogent3/cogent3
Pull request #1217 (2022-12-06 01:51:45)

ENH: modified DataMember equality considers unique ID and data_store


Patch:
diff --git a/src/cogent3/app/data_store_new.py b/src/cogent3/app/data_store_new.py
index 8152a2853..bd394d780 100644
--- a/src/cogent3/app/data_store_new.py
+++ b/src/cogent3/app/data_store_new.py
@@ -75,7 +75,7 @@ class DataMemberABC(ABC):
     def __eq__(self, other):
         """to check equality of members and check existence of a
         member in a list of members"""
-        return self.unique_id == other.unique_id
+        return (self.data_store, self.unique_id) == (other.data_store, other.unique_id)
 
     @property
     def md5(self):


Test patch:
diff --git a/tests/test_app/test_data_store_new.py b/tests/test_app/test_data_store_new.py
index 3216e9806..2ce5acd9f 100644
--- a/tests/test_app/test_data_store_new.py
+++ b/tests/test_app/test_data_store_new.py
@@ -132,7 +132,7 @@ def nc_objects():
     }
 
 
-@pytest.fixture(scope="function")
+@pytest.fixture(scope="session")
 def log_data():
     path = DATA_DIR / "scitrack.log"
     return path.read_text()
@@ -151,6 +151,14 @@ def full_dstore(write_dir, nc_objects, completed_objects, log_data):
     return dstore
 
 
+def test_data_member_eq(ro_dstore, fasta_dir):
+    ro_dstore2 = DataStoreDirectory(fasta_dir, mode="r", suffix="fasta")
+    name = "brca1.fasta"
+    mem1 = [m for m in ro_dstore.completed if m.unique_id == name][0]
+    mem2 = [m for m in ro_dstore2.completed if m.unique_id == name][0]
+    assert mem1 != mem2
+
+
 @pytest.mark.parametrize("dest", (None, "mydata.sqlitedb"))
 def test_convert_tinydb_to_sqlite(tmp_dir, dest):
     path = tmp_dir / "data1.tinydb"
Repository: cogent3/cogent3
Pull request #1220 (2022-12-06 05:23:03)

Sqlite data store enhancement


Patch:
diff --git a/src/cogent3/app/sqlite_data_store.py b/src/cogent3/app/sqlite_data_store.py
index 04b5c68c6..3182dec59 100644
--- a/src/cogent3/app/sqlite_data_store.py
+++ b/src/cogent3/app/sqlite_data_store.py
@@ -360,3 +360,20 @@ class DataStoreSqlite(DataStoreABC):
         table = super().describe
         table.title = title
         return table
+
+    @property
+    def record_type(self) -> str:
+        """class name of completed results"""
+        result = self.db.execute("SELECT record_type FROM state").fetchone()
+        return result["record_type"]
+
+    @record_type.setter
+    def record_type(self, obj):
+        from cogent3.util.misc import get_object_provenance
+
+        rt = self.record_type
+        if self.mode is OVERWRITE and rt:
+            raise IOError(f"cannot overwrite existing record_type {rt}")
+
+        n = get_object_provenance(obj)
+        self.db.execute("UPDATE state SET record_type=? WHERE state_id=1", (n,))


Test patch:
diff --git a/tests/test_app/test_io.py b/tests/test_app/test_io.py
index b51ce30b7..2768f6e74 100644
--- a/tests/test_app/test_io.py
+++ b/tests/test_app/test_io.py
@@ -506,7 +506,6 @@ class TestIo(TestCase):
             w = io_app.write_db(outdir, create=True, if_exists="skip")
             w.data_store.close()
 
-    @pytest.mark.xfail
     def test_write_db_parallel(self):
         """writing with overwrite in parallel should reset db"""
         with TemporaryDirectory(dir=".") as dirname:
diff --git a/tests/test_app/test_sqlite_data_store.py b/tests/test_app/test_sqlite_data_store.py
index b874b7604..ac9320e40 100644
--- a/tests/test_app/test_sqlite_data_store.py
+++ b/tests/test_app/test_sqlite_data_store.py
@@ -480,3 +480,13 @@ def test_summary_not_completed(full_dstore_sqlite):
     got = full_dstore_sqlite.summary_not_completed
     assert got.shape >= (1, 1)
     assert isinstance(got, Table)
+
+
+def test_set_record_type(full_dstore_sqlite):
+    from cogent3 import make_table
+    from cogent3.util.misc import get_object_provenance
+
+    assert full_dstore_sqlite.record_type is None
+    t = make_table(data={"a": [0, 2]})
+    full_dstore_sqlite.record_type = t
+    assert full_dstore_sqlite.record_type == get_object_provenance(t)
Repository: cogent3/cogent3
Pull request #1248 (2022-12-20 01:12:32)

Add new test to write NotCompleted in DataStoreDirectory


Patch:
diff --git a/src/cogent3/app/data_store_new.py b/src/cogent3/app/data_store_new.py
index 6ab700cf1..3fcb15ee2 100644
--- a/src/cogent3/app/data_store_new.py
+++ b/src/cogent3/app/data_store_new.py
@@ -494,6 +494,7 @@ class DataStoreDirectory(DataStoreABC):
         return member
 
     def write_not_completed(self, *, unique_id: str, data: str) -> DataMember:
+        (self.source / _NOT_COMPLETED_TABLE).mkdir(parents=True, exist_ok=True)
         member = self._write(
             subdir=_NOT_COMPLETED_TABLE, unique_id=unique_id, suffix="json", data=data
         )
@@ -502,6 +503,7 @@ class DataStoreDirectory(DataStoreABC):
         return member
 
     def write_log(self, *, unique_id: str, data: str) -> None:
+        (self.source / _LOG_TABLE).mkdir(parents=True, exist_ok=True)        
         _ = self._write(subdir=_LOG_TABLE, unique_id=unique_id, suffix="log", data=data)
 
     def md5(self, unique_id: str) -> Union[str, NoneType]:


Test patch:
diff --git a/tests/test_app/test_data_store_new.py b/tests/test_app/test_data_store_new.py
index ba4e35442..6036899fa 100644
--- a/tests/test_app/test_data_store_new.py
+++ b/tests/test_app/test_data_store_new.py
@@ -9,6 +9,8 @@ import pytest
 
 from scitrack import get_text_hexdigest
 
+import cogent3.app.io_new as io_app
+
 from cogent3.app.composable import NotCompleted
 from cogent3.app.data_store_new import (
     _MD5_TABLE,
@@ -574,3 +576,15 @@ def test_load_record_from_json():
         assert Id == "some.json"
         assert data_ == expected
         assert compl == True
+
+
+def test_write_read_not_completed(nc_dstore):
+    nc_dstore.drop_not_completed()
+    assert len(nc_dstore.not_completed) == 0
+    nc = NotCompleted("ERROR", "test", "for tracing", source="blah")
+    writer = io_app.write_seqs(data_store=nc_dstore)
+    writer.main(nc, identifier="blah")
+    assert len(nc_dstore.not_completed) == 1
+    got = nc_dstore.not_completed[0].read()
+    assert nc.to_json() == got
+
diff --git a/tests/test_app/test_io_new.py b/tests/test_app/test_io_new.py
index f7f8c85d9..ac1d7e746 100644
--- a/tests/test_app/test_io_new.py
+++ b/tests/test_app/test_io_new.py
@@ -452,7 +452,7 @@ def test_write_db_load_db(fasta_dir, tmp_dir):
     assert data_store.record_type == get_object_provenance(orig)
 
 
-def test_write_db_not_completed(tmp_dir):
+def test_write_read_db_not_completed(tmp_dir):
     from cogent3.app.sqlite_data_store import DataStoreSqlite
 
     nc = NotCompleted("ERROR", "test", "for tracing", source="blah")
Repository: cogent3/cogent3
Pull request #1265 (2023-01-16 00:19:05)

ENH: get_object_provenance() now allows builtins
[CHANGED] handles built-in functions and objects


Patch:
diff --git a/src/cogent3/util/misc.py b/src/cogent3/util/misc.py
index f41de70f3..8f54489bc 100644
--- a/src/cogent3/util/misc.py
+++ b/src/cogent3/util/misc.py
@@ -1,5 +1,6 @@
 """Generally useful utility classes and methods.
 """
+import inspect
 import os
 import re
 import warnings
@@ -930,7 +931,7 @@ def get_object_provenance(obj):
     # algorithm inspired by Greg Baacon's answer to
     # https://stackoverflow.com/questions/2020014/get-fully-qualified-class
     # -name-of-an-object-in-python
-    if isinstance(obj, type):
+    if isinstance(obj, type) or inspect.isfunction(obj):
         mod = obj.__module__
         name = obj.__name__
     else:


Test patch:
diff --git a/tests/test_util/test_misc.py b/tests/test_util/test_misc.py
index 42467d8c4..f9b4aeba2 100644
--- a/tests/test_util/test_misc.py
+++ b/tests/test_util/test_misc.py
@@ -396,29 +396,45 @@ class UtilsTests(TestCase):
 
     def test_get_object_provenance(self):
         """correctly deduce object provenance"""
+        from cogent3 import DNA, SequenceCollection, get_model
+
         result = get_object_provenance("abncd")
         self.assertEqual(result, "str")
-        from cogent3 import DNA
 
         got = get_object_provenance(DNA)
         self.assertEqual(got, "cogent3.core.moltype.MolType")
-        from cogent3.evolve.models import HKY85
 
-        sm = HKY85()
+        sm = get_model("HKY85")
         got = get_object_provenance(sm)
         self.assertEqual(
-            got, "cogent3.evolve.substitution_model." "TimeReversibleNucleotide"
+            got, "cogent3.evolve.substitution_model.TimeReversibleNucleotide"
         )
 
         # handle a type
-        from cogent3 import SequenceCollection
-
         instance = SequenceCollection(dict(a="ACG", b="GGG"))
         instance_prov = get_object_provenance(instance)
         self.assertEqual(instance_prov, "cogent3.core.alignment.SequenceCollection")
         type_prov = get_object_provenance(SequenceCollection)
         self.assertEqual(instance_prov, type_prov)
 
+    def test_get_object_provenance_builtins(self):
+        """allow identifying builtins too"""
+        from gzip import GzipFile, compress
+
+        obj_prov = get_object_provenance(compress)
+
+        self.assertEqual(obj_prov, "gzip.compress")
+
+        obj_prov = get_object_provenance(GzipFile)
+        self.assertEqual(obj_prov, "gzip.GzipFile")
+
+        d = dict(a=23, b=1)
+        obj_prov = get_object_provenance(d)
+        self.assertEqual(obj_prov, "dict")
+
+        obj_prov = get_object_provenance(dict)
+        self.assertEqual(obj_prov, "dict")
+
     def test_NestedSplitter(self):
         """NestedSplitter should make a function which return expected list"""
         # test delimiters, constructor, filter_
Repository: cogent3/cogent3
Pull request #1267 (2023-01-16 00:57:40)

ENH: improve readability of app_help display
[NEW] identify provenance of callable default values

[CHANGED] split across multiple lines (as per black) for
    readability


Patch:
diff --git a/src/cogent3/app/__init__.py b/src/cogent3/app/__init__.py
index 50a9a9e8d..90d17ab22 100644
--- a/src/cogent3/app/__init__.py
+++ b/src/cogent3/app/__init__.py
@@ -89,18 +89,41 @@ _get_param = re.compile('(?<=").+(?=")')
 
 
 def _make_signature(app: type) -> str:
+    from cogent3.util.misc import get_object_provenance
+
     init_sig = inspect.signature(app.__init__)
-    params = ", ".join(
-        [f'"{app.__name__}"']
-        + [
-            _get_param.findall(repr(v))[0]
-            for k, v in init_sig.parameters.items()
-            if k != "self"
-        ]
-    )
-    sig = f"{app.__name__} = get_app({params})"
-    subsequent_indent = " " * (sig.find("(") + 1)
-    return "\n".join(textwrap.wrap(sig, subsequent_indent=subsequent_indent))
+    app_name = app.__name__
+    params = [f'"{app_name}"']
+    empty_default = inspect._empty
+    for k, v in init_sig.parameters.items():
+        if k == "self":
+            continue
+
+        txt = repr(v).replace("\n", " ")
+        # clean up text when callable() used  as a type hint
+        txt = txt.replace("<built-in function callable>", "callable")
+
+        val = _get_param.findall(txt)[0]
+        if v.default is not empty_default and callable(v.default):
+            val = val.split("=", maxsplit=1)
+            if hasattr(v.default, "app_type"):
+                val[-1] = f" {str(v.default)}"
+            else:
+                val[-1] = f" {get_object_provenance(v.default)}"
+            val = "=".join(val)
+        params.append(val.replace("\n", " "))
+
+    sig_prefix = f"{app_name}_app = get_app"
+    sig_prefix_length = len(sig_prefix)
+
+    if len(", ".join(params)) + sig_prefix_length <= 68:  # plus 2 parentheses makes 70
+        params = ", ".join(params)
+        return f"{sig_prefix}({params})"
+
+    indent = " " * 4
+    params = ",\n".join([f"{indent}{p}" for p in params])
+
+    return f"{sig_prefix}(\n{params},\n)"
 
 
 def _doc_summary(doc):


Test patch:
diff --git a/tests/test_app/test_init.py b/tests/test_app/test_init.py
index c9de50134..291ca510b 100644
--- a/tests/test_app/test_init.py
+++ b/tests/test_app/test_init.py
@@ -201,5 +201,19 @@ def test_app_help_no_docs(capsys, app_doc, init_doc):
         assert "Options" in got
 
 
+@pytest.mark.parametrize(
+    "app_name", ("bootstrap", "from_primitive", "load_db", "take_named_seqs")[:1]
+)
+def test_app_help_signature(capsys, app_name):
+    from cogent3.app import _get_app_matching_name, _make_signature
+
+    got = _make_signature(_get_app_matching_name(app_name))
+    # app name is in quotes
+    assert f'"{app_name}"' in got
+    # check split across multiple lines if long signature
+    if len(got) > 70:
+        assert got.count("\n") > 1
+
+
 if __name__ == "__main__":
     main()
Repository: cogent3/cogent3
Pull request #1270 (2023-01-16 11:10:12)

MAINT: changes to allow testing of append to data store
[CHANGED] refactor the composable._make_logfile_name function so it
    generates unique log ID's even if called on the same thread. Instead
    of using the process ID, it puts the first 8 characters of a uuid4
    string onto the end of the name.

[NEW] test that append operation creates a new logfile for directory
    and sqlite data stores


Patch:
diff --git a/src/cogent3/app/composable.py b/src/cogent3/app/composable.py
index 1ea7cd773..a07ba32fa 100644
--- a/src/cogent3/app/composable.py
+++ b/src/cogent3/app/composable.py
@@ -56,9 +56,8 @@ def _make_logfile_name(process):
     text = re.split(r"\s+\+\s+", text)
     parts = [part[: part.find("(")] for part in text]
     result = "-".join(parts)
-    pid = os.getpid()
-    result = f"{result}-pid{pid}.log"
-    return result
+    uid = str(uuid4())
+    return f"{result}-{uid[:8]}.log"
 
 
 def _get_origin(origin):


Test patch:
diff --git a/tests/test_app/test_sqlite_data_store.py b/tests/test_app/test_sqlite_data_store.py
index a90d0f047..2ceebba1e 100644
--- a/tests/test_app/test_sqlite_data_store.py
+++ b/tests/test_app/test_sqlite_data_store.py
@@ -6,7 +6,7 @@ import pytest
 
 from scitrack import get_text_hexdigest
 
-from cogent3 import open_data_store
+from cogent3 import get_app, open_data_store
 from cogent3.app.composable import NotCompleted
 from cogent3.app.data_store_new import (
     APPEND,
@@ -548,3 +548,41 @@ def test_open_data_store_sqlitedb_err():
     # cannot create an in-mmemory db to read only
     with pytest.raises(NotImplementedError):
         open_data_store(":memory:", mode="r")
+
+
+def _make_appendable_dstore(path, suffix):
+    return open_data_store(path, suffix=suffix, mode="a")
+
+
+def _make_and_run_proc(out_path, suffix, members):
+    out_dstore = _make_appendable_dstore(out_path, suffix)
+    loader = get_app("load_unaligned", moltype="dna", format="fasta")
+    mlength = get_app("min_length", 400)
+
+    if suffix:
+        writer = get_app("write_seqs", out_dstore, format="fasta")
+    else:
+        writer = get_app("write_db", out_dstore)
+
+    app = loader + mlength + writer
+    return app.apply_to(members, cleanup=True)
+
+
+@pytest.mark.parametrize(
+    "name,suffix", (("appended", "fa"), ("appended.sqlitedb", None))
+)
+def test_append_makes_logs(tmp_dir, ro_dir_dstore, name, suffix):
+    # do half the records in the first call
+    num = len(ro_dir_dstore.completed) // 2
+    # make a path for writeable dstore
+    out_path = tmp_dir / name
+    got1 = _make_and_run_proc(out_path, suffix, ro_dir_dstore[:num])
+    assert len(got1.logs) == 1
+
+    # creating a separate instance should result in a
+    # new log file
+    got2 = _make_and_run_proc(out_path, suffix, ro_dir_dstore[num:])
+    assert len(got2.logs) == 2
+    # should be a row for each log
+    summary = got2.summary_logs
+    assert summary.shape[0] == 2
Repository: cogent3/cogent3
Pull request #1275 (2023-01-18 06:17:28)

ENH: DataStoreABC.summary_not_completed skips bytes records
[NEW] If using sqlitedb and pickling records, then the data member
    read returns the raw data for both completed and not completed types.


Patch:
diff --git a/src/cogent3/app/data_store_new.py b/src/cogent3/app/data_store_new.py
index e5b29f1d1..7aee5da1e 100644
--- a/src/cogent3/app/data_store_new.py
+++ b/src/cogent3/app/data_store_new.py
@@ -235,14 +235,25 @@ class DataStoreABC(ABC):
         err_pat = re.compile(r"[A-Z][a-z]+[A-Z][a-z]+\:.+")
         types = defaultdict(list)
         indices = "type", "origin"
+        num_bytes = 0
         for member in self.not_completed:
             record = member.read()
+            if isinstance(record, bytes):
+                num_bytes += 1
+                continue
             record = deserialise_object(record)
             key = tuple(getattr(record, k, None) for k in indices)
             match = err_pat.findall(record.message)
             types[key].append([match[-1] if match else record.message, record.source])
 
         header = list(indices) + ["message", "num", "source"]
+        if num_bytes == len(self.not_completed):
+            return Table(
+                header=header,
+                title="Cannot summarise not_completed as they are all bytes, "
+                "use an appropriate reader",
+            )
+
         rows = []
         maxtring = reprlib.aRepr.maxstring
         reprlib.aRepr.maxstring = 45


Test patch:
diff --git a/tests/test_app/test_io_new.py b/tests/test_app/test_io_new.py
index 78398aab8..fd1caed75 100644
--- a/tests/test_app/test_io_new.py
+++ b/tests/test_app/test_io_new.py
@@ -467,6 +467,17 @@ def test_write_read_db_not_completed(tmp_dir):
     assert got.to_rich_dict() == nc.to_rich_dict()
 
 
+def test_write_read_db_summary_not_completed(tmp_dir):
+    # should not fail, even if not completed content is compressed
+    from cogent3.app.sqlite_data_store import DataStoreSqlite
+
+    nc = NotCompleted("ERROR", "test", "for tracing", source="blah")
+    data_store = DataStoreSqlite(tmp_dir / "test.sqlitedb", mode="w")
+    writer = get_app("write_db", data_store=data_store)
+    writer.main(nc, identifier="blah")
+    assert isinstance(writer.data_store.summary_not_completed, Table)
+
+
 def test_write_db_parallel(tmp_dir, fasta_dir):
     """writing with overwrite in parallel should reset db"""
     dstore = open_data_store(fasta_dir, suffix="fasta")
Repository: cogent3/cogent3
Pull request #1280 (2023-01-27 08:44:13)

ENH: open_data_store correctly handles directories


Patch:
diff --git a/src/cogent3/app/io_new.py b/src/cogent3/app/io_new.py
index 0d24680c6..431f5980d 100644
--- a/src/cogent3/app/io_new.py
+++ b/src/cogent3/app/io_new.py
@@ -140,16 +140,17 @@ def open_data_store(
     if not isinstance(suffix, (str, type(None))):
         raise ValueError(f"suffix {type(suffix)} not one of string or None")
 
-    kwargs = dict(limit=limit, mode=mode, suffix=suffix)
+    kwargs = {"limit": limit, "mode": mode, "suffix": suffix, **kwargs}
     base_path = Path(base_path)
     base_path = (
         base_path if base_path.name == ":memory:" else base_path.expanduser().absolute()
     )
-    if base_path.suffix == ".tinydb":
+    if base_path.is_dir():
+        ds_suffix = None
+    elif base_path.suffix == ".tinydb":
         kwargs["suffix"] = "json"
         kwargs.pop("mode")
-
-    if base_path.suffix == ".sqlitedb" or base_path.name == _MEMORY:
+    elif base_path.suffix == ".sqlitedb" or base_path.name == _MEMORY:
         ds_suffix = ".sqlitedb"
         kwargs.pop("suffix")
     elif zipfile.is_zipfile(base_path):
@@ -158,6 +159,7 @@ def open_data_store(
     elif base_path.suffix:
         ds_suffix = base_path.suffix
     else:
+        # triggered when mode="w"
         ds_suffix = None
 
     if base_path.name == _MEMORY and mode is READONLY:
diff --git a/src/cogent3/app/sqlite_data_store.py b/src/cogent3/app/sqlite_data_store.py
index 2640c4afd..996aa8e2f 100644
--- a/src/cogent3/app/sqlite_data_store.py
+++ b/src/cogent3/app/sqlite_data_store.py
@@ -301,7 +301,8 @@ class DataStoreSqlite(DataStoreABC):
         locked = result[0]["lock_pid"] if result else None
         if locked and self.mode is OVERWRITE:
             raise IOError(
-                "You are trying to OVERWRITE a database which is locked. Use APPEND mode or unlock"
+                f"You are trying to OVERWRITE {str(self.source)!r} which is "
+                "locked. Use APPEND mode or unlock."
             )
 
         if result:


Test patch:
diff --git a/tests/test_app/test_io_new.py b/tests/test_app/test_io_new.py
index 7046e9053..2222ab962 100644
--- a/tests/test_app/test_io_new.py
+++ b/tests/test_app/test_io_new.py
@@ -570,3 +570,11 @@ def test_writer_unique_id_arg(tmp_dir, writer, data, dstore):
     suffix = f".{suffix}" if suffix else suffix
     expect = f"blah{suffix}"
     assert m.unique_id == expect
+
+
+def test_open_suffix_dirname(tmp_dir):
+    # open_data_store correctly identifies this as a directory data store
+    outpath = tmp_dir / "melsubgroup_aln_flydivas_v1.2"
+    outpath.mkdir(exist_ok=True)
+    dstore = open_data_store(outpath, suffix="txt")
+    assert isinstance(dstore, DataStoreDirectory)
Repository: cogent3/cogent3
Pull request #1287 (2023-02-03 00:27:00)

MAINT: better error messages for loading tabular data


Patch:
diff --git a/src/cogent3/util/table.py b/src/cogent3/util/table.py
index d37b3b4ff..ab3b2e72d 100644
--- a/src/cogent3/util/table.py
+++ b/src/cogent3/util/table.py
@@ -514,7 +514,14 @@ class Table:
                     f"different number of elements in header {hlen} and data row 0 {dcols}"
                 )
 
-            data = dict(zip(header, zip(*data)))
+            try:
+                data = dict(zip(header, zip(*data, strict=True), strict=True))
+            except TypeError:  # handle python versions <3.10
+                # check that number of elements per row is correct
+                if set(len(r) for r in data) != {hlen}:
+                    raise ValueError(f"not all rows have {hlen} elements")
+
+                data = dict(zip(header, zip(*data)))
 
         if header is None:
             header = list(data) if isinstance(data, dict) else []


Test patch:
diff --git a/tests/test_maths/test_stats/test_test.py b/tests/test_maths/test_stats/test_test.py
index d60aaedda..fbfe29cef 100644
--- a/tests/test_maths/test_stats/test_test.py
+++ b/tests/test_maths/test_stats/test_test.py
@@ -239,7 +239,6 @@ class TestsTests(TestCase):
         assert_allclose(tail(0.25, "a" == "a"), 0.25 / 2)
         assert_allclose(tail(0.25, "a" != "a"), 1 - (0.25 / 2))
 
-
     def test_multiple_comparisons(self):
         """multiple_comparisons should match values from R"""
         assert_allclose(
diff --git a/tests/test_util/test_table.py b/tests/test_util/test_table.py
index 34a271d84..43396a5e2 100644
--- a/tests/test_util/test_table.py
+++ b/tests/test_util/test_table.py
@@ -12,6 +12,7 @@ from tempfile import TemporaryDirectory
 from unittest import TestCase, main, skipIf
 
 import numpy
+import pytest
 
 from numpy import arange
 from numpy.testing import assert_equal
@@ -1975,5 +1976,12 @@ class TableTests(TestCase):
         self.assertFalse(is_html_markup("blah > blah"))
 
 
+@pytest.mark.parametrize("data", ([[0, 1, 2], [0, 1]], [[0, 1, 2], [0, 1, 2, 3]]))
+def test_mixed_row_lengths(data):
+    """should fail"""
+    with pytest.raises(ValueError):
+        _ = Table(header=["a", "b", "c"], data=data)
+
+
 if __name__ == "__main__":
     main()