Repository: chezou/tabula-py
Pull request #200 (2020-01-04 07:19:33)

Ensure read_pdf() always returns list of DataFrame
Ensure `read_pdf()` returns list of DataFrame except by setting `output_format`

## Description

This PR changes:

- `read_pdf()` returns a list of DataFrame even if there is a table in a PDF without setting `output_format`
- `multiple_tables` becomes `True` by default
- renaming `wrapper` module to `io`
- stop using kwargs for `build_options` to avoid unexpected arguments
- add validation for `output_format` option

## Motivation and Context
After the `multiple_tables` option allows extracting multiple DataFrames at once, the return type of `read_pdf` function may vary with the existence of the option. 
It should be better to return a consistent type for extracted data frames.


## How Has This Been Tested?
Unit test and manual test.

## Screenshots (if appropriate):

## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## Checklist:
- [x] I read [the contributing document](https://tabula-py.readthedocs.io/en/latest/contributing.html).
- [x] My code follows the code style of this project with running linter.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.



Patch:
diff --git a/README.md b/README.md
index 23e0ff3..e338fcf 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,9 @@ You can help by:
 - [@rmnevesLH](https://github.com/rmnevesLH)
 - [@red-bin](https://github.com/red-bin)
 - [@Gallaecio](https://github.com/Gallaecio)
+- [@red-bin](https://github.com/red-bin)
+- [@alexandreio](https://github.com/alexandreio)
+- [@bpben](https://github.com/bpben)
 
 ### Another support
 
diff --git a/docs/conf.py b/docs/conf.py
index 8c0f1dc..23c5048 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -73,11 +73,12 @@ def linkcode_resolve(domain, info):
         GH_ORGANIZATION, GH_PROJECT, revision, MODULE, relpath, linenum
     )
 
+
 # -- Project information -----------------------------------------------------
 
-project = 'tabula-py'
-copyright = '2019, Aki Ariga'
-author = 'Aki Ariga'
+project = "tabula-py"
+copyright = "2019, Aki Ariga"
+author = "Aki Ariga"
 
 
 # -- General configuration ---------------------------------------------------
@@ -93,12 +94,12 @@ extensions = [
 ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -111,8 +112,9 @@ html_theme = "sphinx_rtd_theme"
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+# html_static_path = ["_static"]
+html_static_path = []
 
 # Work around for contents.rst not found error
 # See also: https://github.com/readthedocs/readthedocs.org/issues/2569
-master_doc = 'index'
+master_doc = "index"
diff --git a/docs/index.rst b/docs/index.rst
index 976ff98..e8ce23a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -4,9 +4,11 @@ tabula-py: Read tables in a PDF into DataFrame
 ``tabula-py`` is a simple Python wrapper of `tabula-java <https://github.com/tabulapdf/tabula-java>`_\ , which can read table of PDF.
 You can read tables from PDF and convert into pandas's DataFrame. tabula-py also enables you to convert a PDF file into CSV/TSV/JSON file.
 
-You can see `the example notebook <https://nbviewer.jupyter.org/github/chezou/tabula-py/blob/master/examples/tabula_example.ipynb>`_ and try it on `Google Colab <https://colab.research.google.com/github/chezou/tabula-py/blob/master/examples/tabula_example.ipynb>`_.
+We highly recommend to look at `the example notebook <https://nbviewer.jupyter.org/github/chezou/tabula-py/blob/master/examples/tabula_example.ipynb>`_ and try it on `Google Colab <https://colab.research.google.com/github/chezou/tabula-py/blob/master/examples/tabula_example.ipynb>`_.
 
 
+For high level API reference, see :ref:`high-level-interface`.
+
 
 .. toctree::
    :maxdepth: 2
diff --git a/docs/tabula.rst b/docs/tabula.rst
index 947be8d..c2cecee 100644
--- a/docs/tabula.rst
+++ b/docs/tabula.rst
@@ -2,13 +2,15 @@ tabula
 ======
 
 
+.. _high-level-interface:
+
 High level interfaces
 ----------------------
 
-tabula.wrapper
+tabula.io
 ^^^^^^^^^^^^^^^^^^^^
 
-.. automodule:: tabula.wrapper
+.. automodule:: tabula.io
    :members:
    :undoc-members:
    :show-inheritance:
diff --git a/setup.py b/setup.py
index 7770dbd..7152360 100644
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,4 @@ import os
 
 from setuptools import find_packages, setup
 
-setup(
-    use_scm_version=True,
-)
+setup(use_scm_version=True,)
diff --git a/tabula/__init__.py b/tabula/__init__.py
index 80f4a22..20c508e 100644
--- a/tabula/__init__.py
+++ b/tabula/__init__.py
@@ -1,12 +1,7 @@
 from pkg_resources import DistributionNotFound, get_distribution
 
+from .io import convert_into, convert_into_by_batch, read_pdf, read_pdf_with_template
 from .util import environment_info
-from .wrapper import (
-    convert_into,
-    convert_into_by_batch,
-    read_pdf,
-    read_pdf_with_template,
-)
 
 try:
     __version__ = get_distribution("tabula-py").version
diff --git a/tabula/wrapper.py b/tabula/io.py
similarity index 83%
rename from tabula/wrapper.py
rename to tabula/io.py
index 365a828..804ef69 100644
--- a/tabula/wrapper.py
+++ b/tabula/io.py
@@ -32,7 +32,6 @@ import pandas as pd
 from .errors import CSVParseError, JavaNotFoundError
 from .file_util import localize_file
 from .template import load_template
-from .util import deprecated_option
 
 logger = getLogger(__name__)
 
@@ -71,7 +70,7 @@ def _run(java_options, options, path=None, encoding="utf-8"):
             )
         )
 
-    built_options = build_options(options)
+    built_options = build_options(**options)
     args = ["java"] + java_options + ["-jar", _jar_path()] + built_options
     if path:
         args.append(path)
@@ -96,36 +95,36 @@ def _run(java_options, options, path=None, encoding="utf-8"):
 
 def read_pdf(
     input_path,
-    output_format="dataframe",
+    output_format=None,
     encoding="utf-8",
     java_options=None,
     pandas_options=None,
-    multiple_tables=False,
+    multiple_tables=True,
     user_agent=None,
     **kwargs
 ):
     """Read tables in PDF.
 
     Args:
-        input_path (file_like_obj or str):
+        input_path (str, path object or file-like object):
             File like object of tareget PDF file.
             It can be URL, which is downloaded by tabula-py automatically.
         output_format (str, optional):
-            Output format for returned object (``"dataframe" or ``"json"``)
+            Output format for returned object (``dataframe`` or ``json``)
         encoding (str, optional):
-            Encoding type for pandas. Default: 'utf-8'
+            Encoding type for pandas. Default: ``utf-8``
         java_options (list, optional):
             Set java options.
 
             Example:
-                `-Xmx256m`
+                ``["-Xmx256m"]``
         pandas_options (dict, optional):
             Set pandas options.
 
             Example:
                 ``{'header': None}``
-        multiple_tables (bool, optional):
-            It enables to handle multiple tables within a page.
+        multiple_tables (bool):
+            It enables to handle multiple tables within a page. Default: ``True``
 
             Note:
                 If `multiple_tables` option is enabled, tabula-py uses not
@@ -139,17 +138,39 @@ def read_pdf(
             :func:`build_options()`
 
     Returns:
-        Extracted pandas DataFrame or list of DataFrame.
+        list of DataFrames or dict.
+
+    Raises:
+        FileNotFoundError:
+            If downloaded remote file doesn't exist.
+
+        ValueError:
+            If output_format is unknown format, or if downloaded remote file size is 0.
+
+        tabula.errors.CSVParseError:
+            If pandas CSV parsing failed.
+
+        tabula.errors.JavaNotFoundError:
+            If java is not installed or found.
+
+        subprocess.CalledProcessError:
+            If tabula-java execution failed.
 
 
     Examples:
 
-        Here is the basic example. Note that ``read_pdf()`` only extract page 1 by default.
+        Here is a simple example.
+        Note that :func:`read_pdf()` only extract page 1 by default.
+
+        Notes:
+            As of tabula-py 2.0.0, :func:`read_pdf()` sets `multiple_tables=True` by
+            default. If you want to get consistent output with previous version, set
+            `multiple_tables=False`.
 
         >>> import tabula
         >>> pdf_path = "https://github.com/chezou/tabula-py/raw/master/tests/resources/data.pdf"
         >>> tabula.read_pdf(pdf_path, stream=True)
-                    Unnamed: 0   mpg  cyl   disp   hp  drat     wt   qsec  vs  am  gear  carb
+        [             Unnamed: 0   mpg  cyl   disp   hp  drat     wt   qsec  vs  am  gear  carb
         0             Mazda RX4  21.0    6  160.0  110  3.90  2.620  16.46   0   1     4     4
         1         Mazda RX4 Wag  21.0    6  160.0  110  3.90  2.875  17.02   0   1     4     4
         2            Datsun 710  22.8    4  108.0   93  3.85  2.320  18.61   1   1     4     1
@@ -181,12 +202,11 @@ def read_pdf(
         28       Ford Pantera L  15.8    8  351.0  264  4.22  3.170  14.50   0   1     5     4
         29         Ferrari Dino  19.7    6  145.0  175  3.62  2.770  15.50   0   1     5     6
         30        Maserati Bora  15.0    8  301.0  335  3.54  3.570  14.60   0   1     5     8
-        31           Volvo 142E  21.4    4  121.0  109  4.11  2.780  18.60   1   1     4     2
+        31           Volvo 142E  21.4    4  121.0  109  4.11  2.780  18.60   1   1     4     2]
 
-        If you want to extract all pages, it'd be better to set ``mutiple_tables=True``.
-        ``read_pdf()`` returns list of ``pd.DataFrame``
+        If you want to extract all pages, set ``pages="all"``.
 
-        >>> dfs = tabula.read_pdf(pdf_path, pages="all", multiple_tables=True)
+        >>> dfs = tabula.read_pdf(pdf_path, pages="all")
         >>> len(dfs)
         4
         >>> dfs
@@ -254,18 +274,22 @@ def read_pdf(
         14    VC]
     """  # noqa
 
-    if output_format == "dataframe":
-        kwargs.pop("format", None)
+    if output_format:
+        # Respects explicit output_format
+        multiple_tables = False
 
-    elif output_format == "json":
-        kwargs["format"] = "JSON"
+        if output_format.lower() == "dataframe":
+            kwargs.pop("format", None)
+        elif output_format.lower() == "json":
+            kwargs["format"] = "JSON"
+        else:
+            raise ValueError("Unknown output_format {}".format(output_format))
 
     if multiple_tables:
         kwargs["format"] = "JSON"
 
     if java_options is None:
         java_options = []
-
     elif isinstance(java_options, str):
         java_options = shlex.split(java_options)
 
@@ -296,7 +320,7 @@ def read_pdf(
 
     if len(output) == 0:
         logger.warning("The output file is empty.")
-        return
+        return []
 
     if pandas_options is None:
         pandas_options = {}
@@ -306,7 +330,6 @@ def read_pdf(
         raw_json = json.loads(output.decode(encoding))
         if multiple_tables:
             return _extract_from(raw_json, pandas_options)
-
         else:
             return raw_json
 
@@ -314,8 +337,7 @@ def read_pdf(
         pandas_options["encoding"] = pandas_options.get("encoding", encoding)
 
         try:
-            return pd.read_csv(io.BytesIO(output), **pandas_options)
-
+            return [pd.read_csv(io.BytesIO(output), **pandas_options)]
         except pd.errors.ParserError as e:
             message = "Error failed to create DataFrame with different column tables.\n"
             message += (
@@ -338,10 +360,10 @@ def read_pdf_with_template(
     """Read tables in PDF with a Tabula App template.
 
     Args:
-        input_path (file_like_obj or str):
-            File like object of tareget PDF file.
+        input_path (str, path object or file-like object):
+            File like object of target PDF file.
             It can be URL, which is downloaded by tabula-py automatically.
-        template_path (file_like_obj or str):
+        template_path (str, path object or file-like object):
             File like object for Tabula app template.
             It can be URL, which is downloaded by tabula-py automatically.
         pandas_options (dict, optional):
@@ -349,7 +371,7 @@ def read_pdf_with_template(
         encoding (str, optional):
             Encoding type for pandas. Default is 'utf-8'
         java_options (list, optional):
-            Set java options like `-Xmx256m`.
+            Set java options like ``["-Xmx256m"]``.
         user_agent (str, optional):
             Set a custom user-agent when download a pdf from a url. Otherwise
             it uses the default ``urllib.request`` user-agent.
@@ -358,7 +380,23 @@ def read_pdf_with_template(
             :func:`build_options()`
 
     Returns:
-        Extracted pandas DataFrame or list.
+        list of DataFrame.
+
+    Raises:
+        FileNotFoundError:
+            If downloaded remote file doesn't exist.
+
+        ValueError:
+            If output_format is unknown format, or if downloaded remote file size is 0.
+
+        tabula.errors.CSVParseError:
+            If pandas CSV parsing failed.
+
+        tabula.errors.JavaNotFoundError:
+            If java is not installed or found.
+
+        subprocess.CalledProcessError:
+            If tabula-java execution failed.
 
 
     Examples:
@@ -463,6 +501,7 @@ def convert_into(
     input_path, output_path, output_format="csv", java_options=None, **kwargs
 ):
     """Convert tables from PDF into a file.
+    Output file will be saved into `output_path`.
 
     Args:
         input_path (file like obj):
@@ -476,32 +515,32 @@ def convert_into(
             Set java options
 
             Example:
-                ``-Xmx256m``.
+                ``"-Xmx256m"``.
         kwargs:
             Dictionary of option for tabula-java. Details are shown in
             :func:`build_options()`
 
-    Returns:
-        Nothing. Output file will be saved into `output_path`
+    Raises:
+        FileNotFoundError:
+            If downloaded remote file doesn't exist.
+
+        ValueError:
+            If output_format is unknown format, or if downloaded remote file size is 0.
+
+        tabula.errors.JavaNotFoundError:
+            If java is not installed or found.
+
+        subprocess.CalledProcessError:
+            If tabula-java execution failed.
     """
 
     if output_path is None or len(output_path) == 0:
-        raise AttributeError("'output_path' shoud not be None or empty")
+        raise ValueError("'output_path' shoud not be None or empty")
 
     kwargs["output_path"] = output_path
     kwargs["format"] = _extract_format_for_conversion(output_format)
 
-    if java_options is None:
-        java_options = []
-
-    elif isinstance(java_options, str):
-        java_options = shlex.split(java_options)
-
-    # to prevent tabula-py from stealing focus on every call on mac
-    if platform.system() == "Darwin":
-        r = "java.awt.headless"
-        if not any(filter(r.find, java_options)):
-            java_options = java_options + ["-Djava.awt.headless=true"]
+    java_options = _build_java_options(java_options)
 
     path, temporary = localize_file(input_path)
 
@@ -536,43 +575,55 @@ def convert_into_by_batch(input_dir, output_format="csv", java_options=None, **k
 
     Returns:
         Nothing. Outputs are saved into the same directory with `input_dir`
+
+    Raises:
+        ValueError:
+            If input_dir doesn't exist.
+
+        tabula.errors.JavaNotFoundError:
+            If java is not installed or found.
+
+        subprocess.CalledProcessError:
+            If tabula-java execution failed.
     """
 
     if input_dir is None or not os.path.isdir(input_dir):
-        raise AttributeError("'input_dir' shoud be directory path")
+        raise ValueError("'input_dir' should be an existing directory path")
 
     kwargs["format"] = _extract_format_for_conversion(output_format)
 
-    if java_options is None:
-        java_options = []
+    java_options = _build_java_options(java_options)
+
+    # Option for batch
+    kwargs["batch"] = input_dir
+
+    _run(java_options, kwargs)
 
-    elif isinstance(java_options, str):
-        java_options = shlex.split(java_options)
+
+def _build_java_options(_java_options=None):
+    if _java_options is None:
+        _java_options = []
+    elif isinstance(_java_options, str):
+        _java_options = shlex.split(_java_options)
 
     # to prevent tabula-py from stealing focus on every call on mac
     if platform.system() == "Darwin":
         r = "java.awt.headless"
-        if not any(filter(r.find, java_options)):
-            java_options = java_options + ["-Djava.awt.headless=true"]
+        if not any(filter(r.find, _java_options)):
+            _java_options = _java_options + ["-Djava.awt.headless=true"]
 
-    # Option for batch
-    kwargs["batch"] = input_dir
-
-    _run(java_options, kwargs)
+    return _java_options
 
 
 def _extract_format_for_conversion(output_format="csv"):
-    if output_format == "csv":
+    if output_format.lower() == "csv":
         return "CSV"
-
-    if output_format == "json":
+    elif output_format.lower() == "json":
         return "JSON"
-
-    if output_format == "tsv":
+    elif output_format.lower() == "tsv":
         return "TSV"
-
-    if output_format == "dataframe":
-        raise AttributeError("'output_format' has no attribute 'dataframe'")
+    else:
+        raise ValueError("Unknown 'output_format': '{}'".format(output_format))
 
 
 def _extract_from(raw_json, pandas_options=None):
@@ -601,11 +652,17 @@ def _extract_from(raw_json, pandas_options=None):
 
         if isinstance(header_line_number, int) and not columns:
             _columns = list_data.pop(header_line_number)
-            _columns = ["" if e is np.nan else e for e in _columns]
+            _unname_idx = 0
+            for idx, e in enumerate(_columns):
+                if e is np.nan:
+                    _columns[idx] = "Unnamed: {}".format(_unname_idx)
+                    _unname_idx += 1
 
-        data_frames.append(
-            pd.DataFrame(data=list_data, columns=_columns, **pandas_options)
-        )
+        df = pd.DataFrame(data=list_data, columns=_columns, **pandas_options)
+
+        for c in df.columns:
+            df[c] = pd.to_numeric(df[c], errors="ignore")
+        data_frames.append(df)
 
     return data_frames
 
@@ -621,7 +678,7 @@ def _convert_pandas_csv_options(pandas_options, columns):
     """
 
     _columns = pandas_options.pop("names", columns)
-    header = pandas_options.pop("header", None)
+    header = pandas_options.pop("header", "infer")
     pandas_options.pop("encoding", None)
 
     if header == "infer":
@@ -632,7 +689,21 @@ def _convert_pandas_csv_options(pandas_options, columns):
     return _columns, header_line_number
 
 
-def build_options(kwargs=None):
+def build_options(
+    pages=None,
+    guess=True,
+    area=None,
+    relative_area=False,
+    lattice=False,
+    stream=False,
+    password=None,
+    silent=None,
+    columns=None,
+    format=None,
+    batch=None,
+    output_path=None,
+    options="",
+):
     """Build options for tabula-java
 
     Args:
@@ -655,6 +726,10 @@ def build_options(kwargs=None):
             Portion of the page to analyze(top,left,bottom,right).
             Default is entire page.
 
+            Note:
+                If you want to use multiple area options and extract in one table, it
+                should be better to set ``multiple_tables=False`` for :func:`read_pdf()`
+
             Examples:
                 ``[269.875,12.75,790.5,561]``,
                 ``[[12.1,20.5,30.1,50.2], [1.0,3.2,10.5,40.2]]``
@@ -684,8 +759,8 @@ def build_options(kwargs=None):
             Format for output file or extracted object.
             (``"CSV"``, ``"TSV"``, ``"JSON"``)
         batch (str, optional):
-            Convert all .pdfs in the provided directory. This argument should be
-            directory.
+            Convert all PDF files in the provided directory. This argument should be
+            directory path.
         output_path (str, optional):
             Output file path. File format of it is depends on ``format``.
             Same as ``--outfile`` option of tabula-java.
@@ -698,18 +773,9 @@ def build_options(kwargs=None):
     """
 
     __options = []
-    if kwargs is None:
-        kwargs = {}
-    options = kwargs.get("options", "")
     # handle options described in string for backward compatibility
     __options += shlex.split(options)
 
-    DEPRECATED_OPTIONS = set(["spreadsheet", "nospreadsheet"])
-    for option in set(kwargs.keys()) & DEPRECATED_OPTIONS:
-        deprecated_option(option)
-
-    # parse options
-    pages = kwargs.get("pages", 1)
     if pages:
         __pages = pages
         if isinstance(pages, int):
@@ -718,13 +784,14 @@ def build_options(kwargs=None):
             __pages = ",".join(map(str, pages))
 
         __options += ["--pages", __pages]
+    else:
+        logger.warning(
+            "'pages' argument isn't specified."
+            "Will extract only from page 1 by default."
+        )
 
-    area = kwargs.get("area")
-    relative_area = kwargs.get("relative_area")
     multiple_areas = False
 
-    guess = kwargs.get("guess", True)
-
     if area:
         guess = False
         __area = area
@@ -746,39 +813,31 @@ def build_options(kwargs=None):
                 )
                 __options += ["--area", __area]
 
-    lattice = kwargs.get("lattice") or kwargs.get("spreadsheet")
     if lattice:
         __options.append("--lattice")
 
-    stream = kwargs.get("stream") or kwargs.get("nospreadsheet")
     if stream:
         __options.append("--stream")
 
     if guess and not multiple_areas:
         __options.append("--guess")
 
-    fmt = kwargs.get("format")
-    if fmt:
-        __options += ["--format", fmt]
+    if format:
+        __options += ["--format", format]
 
-    output_path = kwargs.get("output_path")
     if output_path:
         __options += ["--outfile", output_path]
 
-    columns = kwargs.get("columns")
     if columns:
         __columns = ",".join(map(str, columns))
         __options += ["--columns", __columns]
 
-    password = kwargs.get("password")
     if password:
         __options += ["--password", password]
 
-    batch = kwargs.get("batch")
     if batch:
         __options += ["--batch", batch]
 
-    silent = kwargs.get("silent")
     if silent:
         __options.append("--silent")
 
diff --git a/tabula/template.py b/tabula/template.py
index 8db26b5..50d57d8 100644
--- a/tabula/template.py
+++ b/tabula/template.py
@@ -7,7 +7,8 @@ def load_template(path_or_buffer):
     """Build tabula-py option from template file
 
     Args:
-        file_like_obj: File like object of Tabula app template
+        path_or_buffer (str, path object or file-like object):
+            File like object of Tabula app template.
 
     Returns:
         dict: tabula-py options
diff --git a/tabula/util.py b/tabula/util.py
index 55c5488..1ebafbf 100644
--- a/tabula/util.py
+++ b/tabula/util.py
@@ -1,36 +1,8 @@
 """
-Utility module providing some convinient functions.
+Utility module providing some convenient functions.
 """
 
 import platform
-import warnings
-
-
-def deprecated(func):
-    """This is a decorator which can be used to mark functions
-    as deprecated. It will result in a warning being emmitted
-    when the function is used."""
-
-    def newFunc(*args, **kwargs):
-        warnings.warn(
-            "Call to deprecated function {}.".format(func.__name__),
-            category=DeprecationWarning,
-            stacklevel=2,
-        )
-        return func(*args, **kwargs)
-
-    newFunc.__name__ = func.__name__
-    newFunc.__doc__ = func.__doc__
-    newFunc.__dict__.update(func.__dict__)
-    return newFunc
-
-
-def deprecated_option(option):
-    warnings.warn(
-        "Call to deprecated option {}.".format(option),
-        category=DeprecationWarning,
-        stacklevel=2,
-    )
 
 
 def java_version():


Test patch:
diff --git a/tests/test_read_pdf_table.py b/tests/test_read_pdf_table.py
index 40773e5..47eb522 100644
--- a/tests/test_read_pdf_table.py
+++ b/tests/test_read_pdf_table.py
@@ -24,20 +24,25 @@ class TestReadPdfTable(unittest.TestCase):
 
     def test_read_pdf(self):
         df = tabula.read_pdf(self.pdf_path, stream=True)
-        self.assertTrue(isinstance(df, pd.DataFrame))
-        self.assertTrue(df.equals(pd.read_csv(self.expected_csv1)))
+        self.assertTrue(len(df), 1)
+        self.assertTrue(isinstance(df[0], pd.DataFrame))
+        self.assertTrue(df[0].equals(pd.read_csv(self.expected_csv1)))
 
     def test_read_remote_pdf(self):
         df = tabula.read_pdf(self.uri)
-        self.assertTrue(isinstance(df, pd.DataFrame))
+        self.assertTrue(len(df), 1)
+        self.assertTrue(isinstance(df[0], pd.DataFrame))
 
     def test_read_remote_pdf_with_custom_user_agent(self):
         df = tabula.read_pdf(self.uri, user_agent="Mozilla/5.0", stream=True)
-        self.assertTrue(isinstance(df, pd.DataFrame))
+        self.assertTrue(len(df), 1)
+        self.assertTrue(isinstance(df[0], pd.DataFrame))
 
     def test_read_pdf_into_json(self):
         expected_json = "tests/resources/data_1.json"
-        json_data = tabula.read_pdf(self.pdf_path, output_format="json", stream=True)
+        json_data = tabula.read_pdf(
+            self.pdf_path, output_format="json", stream=True, multiple_tables=False
+        )
         self.assertTrue(isinstance(json_data, list))
         with open(expected_json) as json_file:
             data = json.load(json_file)
@@ -47,38 +52,43 @@ class TestReadPdfTable(unittest.TestCase):
         expected_csv2 = "tests/resources/data_2-3.csv"
         expected_df2 = pd.read_csv(expected_csv2)
         self.assertTrue(
-            tabula.read_pdf(self.pdf_path, pages=1, stream=True).equals(
+            tabula.read_pdf(self.pdf_path, pages=1, stream=True)[0].equals(
                 pd.read_csv(self.expected_csv1)
             )
         )
         self.assertTrue(
             tabula.read_pdf(
-                self.pdf_path, pages="2-3", stream=True, guess=False
-            ).equals(expected_df2)
-        )
-        self.assertTrue(
-            tabula.read_pdf(
-                self.pdf_path, pages=(2, 3), stream=True, guess=False
-            ).equals(expected_df2)
+                self.pdf_path,
+                pages="2-3",
+                stream=True,
+                guess=False,
+                multiple_tables=False,
+            )[0].equals(expected_df2)
         )
         self.assertTrue(
             tabula.read_pdf(
-                self.pdf_path, pages=(2, 3), stream=True, guess=False
-            ).equals(expected_df2)
+                self.pdf_path,
+                pages=(2, 3),
+                stream=True,
+                guess=False,
+                multiple_tables=False,
+            )[0].equals(expected_df2)
         )
 
     def test_read_pdf_file_like_obj(self):
         with open(self.pdf_path, "rb") as f:
             df = tabula.read_pdf(f, stream=True)
-            self.assertTrue(isinstance(df, pd.DataFrame))
-            self.assertTrue(df.equals(pd.read_csv(self.expected_csv1)))
+            self.assertTrue(len(df), 1)
+            self.assertTrue(isinstance(df[0], pd.DataFrame))
+            self.assertTrue(df[0].equals(pd.read_csv(self.expected_csv1)))
 
     def test_read_pdf_pathlib(self):
         from pathlib import Path
 
         df = tabula.read_pdf(Path(self.pdf_path), stream=True)
-        self.assertTrue(isinstance(df, pd.DataFrame))
-        self.assertTrue(df.equals(pd.read_csv(self.expected_csv1)))
+        self.assertTrue(len(df), 1)
+        self.assertTrue(isinstance(df[0], pd.DataFrame))
+        self.assertTrue(df[0].equals(pd.read_csv(self.expected_csv1)))
 
     def test_read_pdf_with_multiple_areas(self):
         # Original files are taken from
@@ -92,19 +102,23 @@ class TestReadPdfTable(unittest.TestCase):
                 pages=1,
                 area=[[0, 0, 100, 50], [0, 50, 100, 100]],
                 relative_area=True,
-            ).equals(expected_df)
+                multiple_tables=False,
+            )[0].equals(expected_df)
         )
         self.assertTrue(
             tabula.read_pdf(
-                pdf_path, pages=1, area=[[0, 0, 451, 212], [0, 212, 451, 425]]
-            ).equals(expected_df)
+                pdf_path,
+                pages=1,
+                area=[[0, 0, 451, 212], [0, 212, 451, 425]],
+                multiple_tables=False,
+            )[0].equals(expected_df)
         )
 
     def test_read_pdf_with_java_option(self):
         self.assertTrue(
             tabula.read_pdf(
                 self.pdf_path, pages=1, stream=True, java_options=["-Xmx256m"]
-            ).equals(pd.read_csv(self.expected_csv1))
+            )[0].equals(pd.read_csv(self.expected_csv1))
         )
 
     def test_read_pdf_with_pandas_option(self):
@@ -112,17 +126,17 @@ class TestReadPdfTable(unittest.TestCase):
         self.assertTrue(
             tabula.read_pdf(
                 self.pdf_path, pages=1, stream=True, pandas_options={"header": None}
-            ).equals(pd.read_csv(self.expected_csv1, header=None))
+            )[0].equals(pd.read_csv(self.expected_csv1, header=None))
         )
         self.assertTrue(
             tabula.read_pdf(
                 self.pdf_path, pages=1, stream=True, pandas_options={"header": 0}
-            ).equals(pd.read_csv(self.expected_csv1, header=0))
+            )[0].equals(pd.read_csv(self.expected_csv1, header=0))
         )
         self.assertTrue(
             tabula.read_pdf(
                 self.pdf_path, pages=1, stream=True, pandas_options={"header": "infer"}
-            ).equals(pd.read_csv(self.expected_csv1, header="infer"))
+            )[0].equals(pd.read_csv(self.expected_csv1, header="infer"))
         )
         self.assertTrue(
             tabula.read_pdf(
@@ -130,7 +144,9 @@ class TestReadPdfTable(unittest.TestCase):
                 pages=1,
                 stream=True,
                 pandas_options={"header": "infer", "names": column_name},
-            ).equals(pd.read_csv(self.expected_csv1, header="infer", names=column_name))
+            )[0].equals(
+                pd.read_csv(self.expected_csv1, header="infer", names=column_name)
+            )
         )
         self.assertTrue(
             tabula.read_pdf(
@@ -167,15 +183,19 @@ class TestReadPdfTable(unittest.TestCase):
         self.assertTrue(
             tabula.read_pdf(self.pdf_path, pages=1, multiple_tables=True, stream=True)[
                 0
-            ].equals(pd.read_csv(self.expected_csv1, header=None))
+            ].equals(pd.read_csv(self.expected_csv1))
         )
         with self.assertRaises(tabula.errors.CSVParseError):
-            tabula.read_pdf(self.pdf_path, pages=2)
+            tabula.read_pdf(self.pdf_path, pages=2, multiple_tables=False)
 
     def test_read_pdf_exception(self):
         invalid_pdf_path = "notexist.pdf"
         with self.assertRaises(FileNotFoundError):
             tabula.read_pdf(invalid_pdf_path)
+        with self.assertRaises(TypeError):
+            tabula.read_pdf(self.pdf_path, unknown_option="foo")
+        with self.assertRaises(ValueError):
+            tabula.read_pdf(self.pdf_path, output_format="unknown")
 
     def test_convert_from(self):
         expected_tsv = "tests/resources/data_1.tsv"
@@ -200,17 +220,20 @@ class TestReadPdfTable(unittest.TestCase):
         finally:
             shutil.rmtree(temp_dir)
 
+        with self.assertRaises(ValueError):
+            tabula.convert_into_by_batch(None, output_format="csv")
+
     def test_convert_remote_file(self):
         temp = tempfile.NamedTemporaryFile()
         tabula.convert_into(self.uri, temp.name, output_format="csv")
         self.assertTrue(os.path.exists(temp.name))
 
     def test_convert_into_exception(self):
-        with self.assertRaises(AttributeError):
+        with self.assertRaises(ValueError):
             tabula.convert_into(self.pdf_path, "test.csv", output_format="dataframe")
-        with self.assertRaises(AttributeError):
+        with self.assertRaises(ValueError):
             tabula.convert_into(self.pdf_path, None)
-        with self.assertRaises(AttributeError):
+        with self.assertRaises(ValueError):
             tabula.convert_into(self.pdf_path, "")
 
     def test_read_pdf_with_template(self):
@@ -231,7 +254,7 @@ class TestReadPdfTable(unittest.TestCase):
         self.assertTrue(dfs[0].equals(pd.read_csv(self.expected_csv1)))
 
     @patch("subprocess.run")
-    @patch("tabula.wrapper._jar_path")
+    @patch("tabula.io._jar_path")
     def test_read_pdf_with_jar_path(self, jar_func, mock_fun):
         jar_func.return_value = "/tmp/tabula-java.jar"
 
@@ -244,9 +267,9 @@ class TestReadPdfTable(unittest.TestCase):
             "-Dfile.encoding=UTF8",
             "-jar",
             "/tmp/tabula-java.jar",
-            "--pages",
-            "1",
             "--guess",
+            "--format",
+            "JSON",
             "tests/resources/data.pdf",
         ]
         subp_args = {