Repository: pyjanitor-devs/pyjanitor

Issue:
`sheet_name` not required in jn.xlsx_table
got new information based on this [conversation](https://github.com/pandas-dev/pandas/issues/38937#issuecomment-1947694330), and also re-reading the [openpyxl documentation](https://openpyxl.readthedocs.io/en/latest/worksheet_tables.html#worksheet-tables), `sheetname` is not required, and should be deprecated.


Discussion:

Resolving patch:
diff --git a/janitor/io.py b/janitor/io.py
--- a/janitor/io.py
+++ b/janitor/io.py
@@ -3,16 +3,25 @@
 import inspect
 import os
 import subprocess
+import warnings
 from collections import defaultdict
 from glob import glob
 from io import StringIO
 from itertools import chain
-from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, Union
+from typing import IO, TYPE_CHECKING, Any, Iterable, Union
 
 import pandas as pd
 
+from janitor.utils import (
+    check,
+    deprecated_alias,
+    find_stack_level,
+    import_message,
+)
+
 from .errors import JanitorError
-from .utils import check, deprecated_alias, import_message
+
+warnings.simplefilter("always", DeprecationWarning)
 
 
 @deprecated_alias(seperate_df="separate_df", filespath="files_path")
@@ -130,8 +139,8 @@ def read_commandline(cmd: str, **kwargs: Any) -> pd.DataFrame:
 
 
 def xlsx_table(
-    path: Union[str, Workbook],
-    sheetname: str,
+    path: Union[str, IO, Workbook],
+    sheetname: str = None,
     table: Union[str, list, tuple] = None,
 ) -> Union[pd.DataFrame, dict]:
     """Returns a DataFrame of values in a table in the Excel file.
@@ -153,7 +162,7 @@ def xlsx_table(
 
         Single table:
 
-        >>> xlsx_table(filename, sheetname='Tables', table='dCategory')
+        >>> xlsx_table(filename, table='dCategory')
            CategoryID       Category
         0           1       Beginner
         1           2       Advanced
@@ -163,7 +172,7 @@ def xlsx_table(
 
         Multiple tables:
 
-        >>> out=xlsx_table(filename, sheetname="Tables", table=["dCategory", "dSalesReps"])
+        >>> out=xlsx_table(filename, table=["dCategory", "dSalesReps"])
         >>> out["dCategory"]
            CategoryID       Category
         0           1       Beginner
@@ -179,8 +188,6 @@ def xlsx_table(
 
     Args:
           path: Path to the Excel File. It can also be an openpyxl Workbook.
-          sheetname: Name of the sheet from which the tables
-                are to be extracted.
           table: Name of a table, or list of tables in the sheet.
 
     Raises:
@@ -204,70 +211,81 @@ def xlsx_table(
             conda_channel="conda-forge",
             pip_install=True,
         )
+    # TODO: remove in version 1.0
+    if sheetname:
+        warnings.warn(
+            "The keyword argument "
+            "'sheetname' of 'xlsx_tables' is deprecated.",
+            DeprecationWarning,
+            stacklevel=find_stack_level(),
+        )
+    if table is not None:
+        check("table", table, [str, list, tuple])
+        if isinstance(table, (list, tuple)):
+            for num, entry in enumerate(table):
+                check(f"entry{num} in the table argument", entry, [str])
     if isinstance(path, Workbook):
-        ws = path[sheetname]
+        ws = path
     else:
         ws = load_workbook(
             filename=path, read_only=False, keep_links=False, data_only=True
         )
-        ws = ws[sheetname]
+    if ws.read_only:
+        raise ValueError("xlsx_table does not work in read only mode.")
 
-    try:
-        contents = ws.tables
-    except AttributeError as error:
-        raise AttributeError(
-            "Accessing the tables is not supported for ReadOnlyWorksheet"
-        ) from error
-
-    if not contents:
-        raise ValueError(f"There is no table in '{sheetname}' sheet.")
-
-    class TableArgs(NamedTuple):
+    def _create_dataframe_or_dictionary_from_table(
+        table_name_and_worksheet: tuple,
+    ):
         """
-        Named Tuple to easily index values
-        from the tables in the sheet.
+        Create DataFrame/dictionary if table exists in Workbook
         """
-
-        table_name: str
-        ref: str
-        headerRowCount: int
-
-    if isinstance(table, str):
-        table = [table]
-    if table is not None:
-        check("table", table, [str, list, tuple])
-        try:
-            data = []
-            for key in table:
-                outcome = TableArgs(
-                    key, contents[key].ref, contents[key].headerRowCount
-                )
-                data.append(outcome)
-        except KeyError as error:
-            raise KeyError(
-                f"Table {error} is not in the '{sheetname}' sheet."
-            ) from error
-    else:
-        data = (
-            TableArgs(key, contents[key].ref, contents[key].headerRowCount)
-            for key in contents
-        )
-
-    frame = {}
-    for table_arg in data:
-        content = [[cell.value for cell in row] for row in ws[table_arg.ref]]
-
-        if table_arg.headerRowCount:
-            header, *content = content
-        else:
-            header = [f"C{num}" for num in range(len(content[0]))]
-        frame[table_arg.table_name] = pd.DataFrame(
-            content, columns=header, copy=False
+        dictionary = {}
+        for table_name, worksheet in table_name_and_worksheet:
+            contents = worksheet.tables[table_name]
+            header_exist = contents.headerRowCount
+            coordinates = contents.ref
+            data = worksheet[coordinates]
+            data = [[entry.value for entry in cell] for cell in data]
+            if header_exist:
+                header, *data = data
+            else:
+                header = [f"C{num}" for num in range(len(data[0]))]
+            data = pd.DataFrame(data, columns=header)
+            dictionary[table_name] = data
+        return dictionary
+
+    worksheets = [worksheet for worksheet in ws if worksheet.tables.items()]
+    if not any(worksheets):
+        raise ValueError("There are no tables in the Workbook.")
+    table_is_a_string = False
+    if table:
+        if isinstance(table, str):
+            table_is_a_string = True
+            table = [table]
+        table_names = (
+            entry for worksheet in worksheets for entry in worksheet.tables
         )
-
-    if len(frame) == 1:
-        _, frame = frame.popitem()
-    return frame
+        missing = set(table).difference(table_names)
+        if missing:
+            raise KeyError(f"Tables {*missing,} do not exist in the Workbook.")
+        tables = [
+            (entry, worksheet)
+            for worksheet in worksheets
+            for entry in worksheet.tables
+            if entry in table
+        ]
+    else:
+        tables = [
+            (entry, worksheet)
+            for worksheet in worksheets
+            for entry in worksheet.tables
+        ]
+    data = _create_dataframe_or_dictionary_from_table(
+        table_name_and_worksheet=tables
+    )
+    if table_is_a_string:
+        return data[table[0]]
+    return data
 
 
 def xlsx_cells(


Test patch:
diff --git a/tests/io/test_xlsx_table.py b/tests/io/test_xlsx_table.py
--- a/tests/io/test_xlsx_table.py
+++ b/tests/io/test_xlsx_table.py
@@ -1,3 +1,4 @@
+import re
 from pathlib import Path
 
 import pandas as pd
@@ -17,25 +18,54 @@
     .resolve()
 )
 
+no_tables = (
+    Path(
+        TEST_DATA_DIR,
+    )
+    .joinpath("file_example_XLSX_10.xlsx")
+    .resolve()
+)
+
 
+@pytest.mark.xfail(reason="sheetname parameter deprecated.")
 def test_check_sheetname():
     """Raise KeyError if sheetname does not exist."""
     with pytest.raises(KeyError):
-        io.xlsx_table(filename, 1, None)
+        io.xlsx_table(filename, table=None)
+
+
+def test_check_sheetname_warning():
+    """Raise Warning if sheetnameis provided."""
+    with pytest.warns(DeprecationWarning):
+        io.xlsx_table(filename, sheetname="rar")
 
 
 def test_check_filename():
     """Raise error if file does not exist."""
     with pytest.raises(FileNotFoundError):
-        io.xlsx_table("excel.xlsx", 1, None)
+        io.xlsx_table("excel.xlsx", table=None)
 
 
 def test_table_exists():
-    """Raise error if there is no table in the sheet."""
+    """Raise error if there is no table in the workbook."""
+    with pytest.raises(
+        ValueError, match="There are no tables in the Workbook."
+    ):
+        io.xlsx_table(no_tables, table="Cover")
+
+
+def test_check_table_name_str():
+    """Raise error if table name is not a string."""
+    with pytest.raises(TypeError, match="table should be one of.+"):
+        io.xlsx_table(filename, table=1)
+
+
+def test_check_table_name_list():
+    """Raise error if table name is not a string."""
     with pytest.raises(
-        ValueError, match="There is no table in 'Cover' sheet."
+        TypeError, match="entry0 in the table argument should be one of.+"
     ):
-        io.xlsx_table(filename, "Cover")
+        io.xlsx_table(filename, table=[1, "rar"])
 
 
 def test_table_name():
@@ -44,9 +74,10 @@ def test_table_name():
     and the table name cannot be found.
     """
     with pytest.raises(
-        KeyError, match="Table 'fake' is not in the 'Tables' sheet."
+        KeyError,
+        match=re.escape("Tables ('fake',) do not exist in the Workbook."),
     ):
-        io.xlsx_table(filename, "Tables", table="fake")
+        io.xlsx_table(filename, table="fake")
 
 
 def test_wb_read_only():
@@ -55,16 +86,16 @@ def test_wb_read_only():
     """
     wb = load_workbook(filename, read_only=True)
     with pytest.raises(
-        AttributeError,
-        match="Accessing the tables.+",
+        ValueError,
+        match="xlsx_table does not work in read only mode.",
     ):
-        io.xlsx_table(wb, "Tables")
+        io.xlsx_table(wb)
     wb.close()
 
 
 def test_table_str():
     """Test output for single table."""
-    expected = io.xlsx_table(filename, "Tables", "dSupplier")
+    expected = io.xlsx_table(filename, table="dSupplier")
     actual = (
         pd.read_excel(
             filename, engine="openpyxl", sheet_name="Tables", usecols="N:R"
@@ -77,7 +108,7 @@ def test_table_str():
 
 def test_table_no_header():
     """Test output for single table, without header."""
-    expected = io.xlsx_table(no_headers, "Tables", "dSalesReps")
+    expected = io.xlsx_table(no_headers, table="dSalesReps")
     actual = pd.read_excel(
         no_headers,
         engine="openpyxl",
@@ -91,7 +122,7 @@ def test_table_no_header():
 def test_tables():
     """Test output for multiple tables."""
     wb = load_workbook(filename, read_only=False)
-    expected = io.xlsx_table(wb, "Tables", ("dSalesReps", "dSupplier"))
+    expected = io.xlsx_table(wb, table=("dSalesReps", "dSupplier"))
     actual = {
         "dSalesReps": pd.read_excel(
             filename, engine="openpyxl", sheet_name="Tables", usecols="A:C"
@@ -108,7 +139,7 @@ def test_tables():
 
 def test_tables_none():
     """Test output for multiple tables."""
-    expected = io.xlsx_table(filename, "Tables")
+    expected = io.xlsx_table(filename)
     actual = {
         "dSalesReps": pd.read_excel(
             filename, engine="openpyxl", sheet_name="Tables", usecols="A:C"
diff --git a/tests/test_data/file_example_XLSX_10.xlsx b/tests/test_data/file_example_XLSX_10.xlsx
new file mode 100644
Binary files /dev/null and b/tests/test_data/file_example_XLSX_10.xlsx differ