Repository: mwouts/itables

Issue:
AssertionError because downsampled row count is odd
If the row selection is enabled and the table is downsampled, then the number of downsampled rows must be even. Otherwise `warn_if_selected_rows_are_not_visible` throws an assertion error.

I don't see a way to control the number of downsampled rows (except by using `maxRows` instead of `maxBytes`). And even the assertion error is opaque. So effectively `maxBytes` can't safely be used with row selection.


```{python}
import itables
from itables.sample_dfs import get_dict_of_test_dfs
from itables.shiny import DT

print(itables.__version__)  # 2.2.4

df = get_dict_of_test_dfs()['wide']

DT(df, maxBytes = 1e2, selected_rows = [])
```

```
2.2.4
Traceback (most recent call last):
  File "c:\Users\bevilla\projects\satk\tmp2.py", line 9, in <module>
    DT(df, maxBytes = 1e2, selected_rows = [])
  File "C:\Python311\satk_dev_venv\Lib\site-packages\itables\shiny.py", line 35, in DT
    html = to_html_datatable(
           ^^^^^^^^^^^^^^^^^^
  File "C:\Python311\satk_dev_venv\Lib\site-packages\itables\javascript.py", line 366, in to_html_datatable
    kwargs["selected_rows"] = warn_if_selected_rows_are_not_visible(
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\satk_dev_venv\Lib\site-packages\itables\javascript.py", line 603, in warn_if_selected_rows_are_not_visible
    assert data_row_count == 2 * half, data_row_count
AssertionError: 3
```

Discussion:
github-actions[bot]: Thank you for making this pull request.

Did you know? You can try it on Binder: [![Binder:lab](https://img.shields.io/badge/binder-jupyterlab-0172B2.svg)](https://mybinder.org/v2/gh/mwouts/itables/downsampled_table_might_have_an_odd_line_count?urlpath=lab/tree/docs/quick_start.md).

Also, the version of ITables developed in this PR can be installed with `pip`:
```
pip install git+https://github.com/mwouts/itables.git@downsampled_table_might_have_an_odd_line_count
```
(this requires `nodejs`, see more at [Developing ITables](https://mwouts.github.io/itables/developing.html))
<!-- thollander/actions-comment-pull-request "binder_link" -->
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/mwouts/itables/pull/352?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 93.30%. Comparing base [(`a3dfd99`)](https://app.codecov.io/gh/mwouts/itables/commit/a3dfd990ab1081c1022c8adc3a1d0791d21072de?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts) to head [(`e7bfa0b`)](https://app.codecov.io/gh/mwouts/itables/commit/e7bfa0b193465beedd595d14a98ce1589d14fb91?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts).

:x: Your project status has failed because the head coverage (90.48%) is below the target coverage (93.00%). You can increase the head coverage or adjust the [target](https://docs.codecov.com/docs/commit-status#target) coverage.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #352      +/-   ##
==========================================
- Coverage   94.76%   93.30%   -1.47%     
==========================================
  Files          24       26       +2     
  Lines        1280     1329      +49     
==========================================
+ Hits         1213     1240      +27     
- Misses         67       89      +22     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mwouts/itables/pull/352?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marc+Wouts).


Patch:
diff --git a/LICENSE b/LICENSE
index 499aa22..3f76a9e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2019-2024 Marc Wouts
+Copyright (c) 2019-2025 Marc Wouts
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/docs/changelog.md b/docs/changelog.md
index 2d03828..5ace749 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,6 +1,13 @@
 ITables ChangeLog
 =================
 
+2.2.5 (2025-02-22)
+------------------
+
+**Fixed**
+- We have removed an erroneous assertion and added tests on the warnings issued when selected rows are not displayed ([[#351](https://github.com/mwouts/itables/issues/351)])
+
+
 2.2.4 (2024-12-07)
 ------------------
 
diff --git a/src/itables/downsample.py b/src/itables/downsample.py
index 2fdec64..c9f5ce5 100644
--- a/src/itables/downsample.py
+++ b/src/itables/downsample.py
@@ -96,6 +96,7 @@ def _downsample(df, max_rows=0, max_columns=0, max_bytes=0, target_aspect_ratio=
     if len(df) > max_rows > 0:
         second_half = max_rows // 2
         first_half = max_rows - second_half
+        assert first_half >= second_half
         if second_half:
             try:
                 df = pd.concat((df.iloc[:first_half], df.iloc[-second_half:]))
@@ -110,6 +111,7 @@ def _downsample(df, max_rows=0, max_columns=0, max_bytes=0, target_aspect_ratio=
     if len(df.columns) > max_columns > 0:
         second_half = max_columns // 2
         first_half = max_columns - second_half
+        assert first_half >= second_half
         if second_half:
             try:
                 df = pd.concat(
diff --git a/src/itables/javascript.py b/src/itables/javascript.py
index cf69b55..5f9def1 100644
--- a/src/itables/javascript.py
+++ b/src/itables/javascript.py
@@ -599,11 +599,12 @@ def warn_if_selected_rows_are_not_visible(
     if full_row_count == data_row_count:
         return selected_rows
 
-    half = data_row_count // 2
-    assert data_row_count == 2 * half, data_row_count
+    second_half = data_row_count // 2
+    first_half = data_row_count - second_half
+    assert first_half >= second_half
 
-    bottom_limit = half
-    top_limit = full_row_count - half
+    bottom_limit = first_half
+    top_limit = full_row_count - second_half
 
     if warn_on_selected_rows_not_rendered and any(
         bottom_limit <= i < top_limit for i in selected_rows
diff --git a/src/itables/version.py b/src/itables/version.py
index 3468a8f..cd228ab 100644
--- a/src/itables/version.py
+++ b/src/itables/version.py
@@ -1,3 +1,3 @@
 """ITables' version number"""
 
-__version__ = "2.2.4"
+__version__ = "2.2.5"


Test patch:
diff --git a/tests/test_downsample.py b/tests/test_downsample.py
index 5fa2ad8..2fd6d56 100644
--- a/tests/test_downsample.py
+++ b/tests/test_downsample.py
@@ -117,3 +117,10 @@ def test_df_with_many_columns_is_downsampled_preferentially_on_columns(df, max_b
     else:
         # aspect ratio is close to 1
         assert 0.5 < len(dn) / len(dn.columns) < 2
+
+
+def test_downsample_to_odd_number_of_rows():
+    df = pd.DataFrame({"x": range(17)})
+    dn, _ = downsample(df, max_rows=3)
+    assert len(dn) == 3
+    assert dn["x"].to_list() == [0, 1, 16]
diff --git a/tests/test_shiny.py b/tests/test_shiny.py
new file mode 100644
index 0000000..fcd947b
--- /dev/null
+++ b/tests/test_shiny.py
@@ -0,0 +1,29 @@
+import warnings
+
+import pandas as pd
+import pytest
+
+from itables.downsample import downsample
+from itables.shiny import DT
+
+
+def test_select_on_downsampled_df():
+    """
+    When a DF of 17 rows is downsampled to 3 rows,
+    we can only select rows 0, 1, 16
+    """
+    df = pd.DataFrame({"x": range(17)})
+    dn, _ = downsample(df, max_rows=3)
+    assert len(dn) == 3
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        DT(df, maxRows=3, selected_rows=[0, 1, 16])
+
+    for row in [-1, 17]:
+        with pytest.raises(IndexError, match="Selected rows out of range"):
+            DT(df, maxRows=3, selected_rows=[row])
+
+    for row in [2, 15]:
+        with pytest.warns(match="no row with index between 2 and 15 can be selected"):
+            DT(df, maxRows=3, selected_rows=[row])