Commit 3fd92201 authored by Jerome Flesch's avatar Jerome Flesch

Merge branch 'py3' into 'master'

Drop Python 2 code

Closes #107

See merge request !113
parents ea14f325 cf561139
......@@ -33,7 +33,6 @@ systems (*BSD, etc). It may or may not work on Windows, MacOSX, etc.
## Installation
```sh
sudo pip install pyocr # Python 2.7
sudo pip3 install pyocr # Python 3.X
```
......@@ -262,7 +261,7 @@ Beware this code hasn't been adapted to libtesseract 3 yet.
## Dependencies
* PyOCR requires python 2.7 or later. Python 3 is supported.
* PyOCR requires Python 3.4 or later.
* You will need [Pillow](https://github.com/python-imaging/Pillow)
or Python Imaging Library (PIL). Under Debian/Ubuntu, Pillow is in
the package ```python-pil``` (```python3-pil``` for the Python 3
......
#!/usr/bin/env python
#!/usr/bin/env python3
import sys
from setuptools import setup
# NOTE: This file must remain Python 2 compatible for the foreseeable future,
# to ensure that we error out properly for people with outdated setuptools
# and/or pip.
if sys.version_info < (3, 4):
error = """
Beginning with PyOCR 0.7, Python 3.4 or above is required.
This may be due to an out of date pip.
Make sure you have pip >= 9.0.1.
"""
sys.exit(error)
setup(
name="pyocr",
description=("A Python wrapper for OCR engines (Tesseract, Cuneiform,"
......@@ -16,9 +30,11 @@ setup(
" (GPLv3+)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Multimedia :: Graphics :: Capture :: Scanners",
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
"Topic :: Scientific/Engineering :: Image Recognition",
......@@ -36,9 +52,9 @@ setup(
data_files=[],
scripts=[],
zip_safe=True,
python_requires='>=3.4',
install_requires=[
"Pillow",
"six",
],
setup_requires=[
'setuptools_scm',
......
# NOTE: This file must remain Python 2 compatible for the foreseeable future,
# to ensure that we error out properly for existing editable installs.
import sys
if sys.version_info < (3, 4): # noqa: E402
raise ImportError("""
PyOCR 0.7+ does not support Python 2.x, 3.0, 3.1, 3.2, or 3.3.
Beginning with PyOCR 0.7, Python 3.4 and above is required.
See PyOCR `README.markdown` file for more information:
https://gitlab.gnome.org/World/OpenPaperwork/pyocr/blob/master/README.markdown
""")
from .pyocr import * # noqa
from .error import PyocrException
......
......@@ -6,17 +6,9 @@ words + boxes : WordBoxBuilder
lines + words + boxes : LineBoxBuilder
"""
try:
from HTMLParser import HTMLParser
except ImportError:
from html.parser import HTMLParser
import xml.dom.minidom
from html.parser import HTMLParser
import logging
import six
from .util import to_unicode
import xml.dom.minidom
logger = logging.getLogger(__name__)
......@@ -30,17 +22,16 @@ __all__ = [
'DigitLineBoxBuilder',
]
_XHTML_HEADER = to_unicode("""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
_XHTML_HEADER = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
\t<meta http-equiv="content-type" content="text/html; charset=utf-8" />
\t<title>OCR output</title>
</head>
""")
"""
@six.python_2_unicode_compatible
class Box(object):
"""
Boxes are rectangles around each individual element recognized in the
......@@ -60,20 +51,6 @@ class Box(object):
self.position = position
self.confidence = confidence
def get_unicode_string(self):
"""
Return the string corresponding to the box, in unicode (utf8).
This string can be stored in a file as-is (see write_box_file())
and reread using read_box_file().
"""
return to_unicode("%s %d %d %d %d") % (
self.content,
self.position[0][0],
self.position[0][1],
self.position[1][0],
self.position[1][1],
)
def get_xml_tag(self, parent_doc):
span_tag = parent_doc.createElement("span")
span_tag.setAttribute("class", "ocrx_word")
......@@ -87,7 +64,13 @@ class Box(object):
return span_tag
def __str__(self):
return self.get_unicode_string()
return "{} {} {} {} {}".format(
self.content,
self.position[0][0],
self.position[0][1],
self.position[1][0],
self.position[1][1],
)
def __box_cmp(self, other):
"""
......@@ -132,7 +115,6 @@ class Box(object):
return (position_hash ^ hash(self.content) ^ hash(self.content))
@six.python_2_unicode_compatible
class LineBox(object):
"""
Boxes are rectangles around each individual element recognized in the
......@@ -158,23 +140,6 @@ class LineBox(object):
txt = txt.strip()
return txt
def get_unicode_string(self):
"""
Return the string corresponding to the box, in unicode (utf8).
This string can be stored in a file as-is (see write_box_file())
and reread using read_box_file().
"""
txt = to_unicode("[\n")
for box in self.word_boxes:
txt += to_unicode(" %s\n") % box.get_unicode_string()
return to_unicode("%s] %d %d %d %d") % (
txt,
self.position[0][0],
self.position[0][1],
self.position[1][0],
self.position[1][1],
)
def get_xml_tag(self, parent_doc):
span_tag = parent_doc.createElement("span")
span_tag.setAttribute("class", "ocr_line")
......@@ -191,7 +156,22 @@ class LineBox(object):
return span_tag
def __str__(self):
return self.get_unicode_string()
txt = "[\n"
for box in self.word_boxes:
txt += " {} {} {} {} {}\n".format(
box.content,
box.position[0][0],
box.position[0][1],
box.position[1][0],
box.position[1][1],
)
return "{}] {} {} {} {}".format(
txt,
self.position[0][0],
self.position[0][1],
self.position[1][0],
self.position[1][1],
)
def __box_cmp(self, other):
"""
......@@ -440,7 +420,7 @@ class _WordHTMLParser(HTMLParser):
# invalid position --> old format --> we ignore this tag
self.__tag_types.append("ignore")
return
self.__current_box_text = to_unicode("")
self.__current_box_text = ""
elif tag_type == 'ocr_line':
self.__current_line_position = self.__parse_position(position)
self.__current_line_content = []
......@@ -449,7 +429,6 @@ class _WordHTMLParser(HTMLParser):
def handle_data(self, data):
if self.__current_box_text is None:
return
data = to_unicode("%s") % data
self.__current_box_text += data
def handle_endtag(self, tag):
......@@ -504,7 +483,7 @@ class _LineHTMLParser(HTMLParser):
tag_type = self.TAG_TYPE_POSITIONS
if tag_type == self.TAG_TYPE_CONTENT:
self.__line_text = to_unicode("")
self.__line_text = ""
self.__char_positions = []
return
elif tag_type == self.TAG_TYPE_POSITIONS:
......@@ -585,7 +564,7 @@ class WordBoxBuilder(BaseBuilder):
p.feed(html_str)
if len(p.boxes) > 0:
last_box = p.boxes[-1]
if last_box.content == to_unicode(""):
if last_box.content == "":
# some parser leave an empty box at the end
p.boxes.pop(-1)
return p.boxes
......@@ -606,13 +585,11 @@ class WordBoxBuilder(BaseBuilder):
newdoc = impl.createDocument(None, "root", None)
file_descriptor.write(_XHTML_HEADER)
file_descriptor.write(to_unicode("<body>\n"))
file_descriptor.write("<body>\n")
for box in boxes:
xml_str = to_unicode("%s") % box.get_xml_tag(newdoc).toxml()
file_descriptor.write(
to_unicode("<p>") + xml_str + to_unicode("</p>\n")
)
file_descriptor.write(to_unicode("</body>\n</html>\n"))
xml_str = box.get_xml_tag(newdoc).toxml()
file_descriptor.write("<p>" + xml_str + "</p>\n")
file_descriptor.write("</body>\n</html>\n")
def start_line(self, box):
pass
......@@ -665,7 +642,7 @@ class LineBoxBuilder(BaseBuilder):
parser.feed(html_str)
if len(parser.boxes) > 0:
last_box = parser.boxes[-1]
if last_box.content == to_unicode(""):
if last_box.content == "":
# some parser leave an empty box at the end
parser.boxes.pop(-1)
return convertion(parser)
......@@ -686,18 +663,15 @@ class LineBoxBuilder(BaseBuilder):
newdoc = impl.createDocument(None, "root", None)
file_descriptor.write(_XHTML_HEADER)
file_descriptor.write(to_unicode("<body>\n"))
file_descriptor.write("<body>\n")
for box in boxes:
xml_str = box.get_xml_tag(newdoc).toxml()
xml_str = to_unicode(xml_str)
file_descriptor.write(
to_unicode("<p>") + xml_str + to_unicode("</p>\n")
)
file_descriptor.write(to_unicode("</body>\n</html>\n"))
file_descriptor.write("<p>" + xml_str + "</p>\n")
file_descriptor.write("</body>\n</html>\n")
def start_line(self, box):
# no empty line
if len(self.lines) > 0 and self.lines[-1].content == to_unicode(""):
if len(self.lines) > 0 and self.lines[-1].content == "":
return
self.lines.append(LineBox([], box))
......
......@@ -17,12 +17,12 @@ https://gitlab.gnome.org/World/OpenPaperwork/pyocr#readme
import codecs
from io import BytesIO
import re
import shutil
import subprocess
import tempfile
from . import builders
from .error import CuneiformError
from . import util
# CHANGE THIS IF CUNEIFORM IS NOT IN YOUR PATH, OR IS NAMED DIFFERENTLY
......@@ -108,7 +108,7 @@ def image_to_string(image, lang=None, builder=None):
def is_available():
return util.is_on_path(CUNEIFORM_CMD)
return shutil.which(CUNEIFORM_CMD) is not None
def get_available_languages():
......
......@@ -16,27 +16,18 @@ https://gitlab.gnome.org/World/OpenPaperwork/pyocr#readme
'''
import codecs
import errno
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import contextlib
import shutil
from . import builders
from . import util
from .builders import DigitBuilder # backward compatibility
from .error import TesseractError # backward compatibility
from .util import digits_only
try:
FileNotFoundError
except NameError:
# python2 does not have FileNotFoundError
FileNotFoundError = IOError
# CHANGE THIS IF TESSERACT IS NOT IN YOUR PATH, OR IS NAMED DIFFERENTLY
TESSERACT_CMD = 'tesseract.exe' if os.name == 'nt' else 'tesseract'
......@@ -109,7 +100,7 @@ class CharBoxBuilder(builders.BaseBuilder):
The file_descriptor must support UTF-8 ! (see module 'codecs')
"""
for box in boxes:
file_descriptor.write(box.get_unicode_string() + " 0\n")
file_descriptor.write(str(box) + " 0\n")
def __str__(self):
return "Character boxes"
......@@ -188,7 +179,7 @@ def detect_orientation(image, lang=None):
TesseractError --- if no script detected on the image
"""
_set_environment()
with temp_dir() as tmpdir:
with tempfile.TemporaryDirectory() as tmpdir:
command = [TESSERACT_CMD, "input.bmp", 'stdout', psm_parameter(), "0"]
version = get_version()
if lang is not None:
......@@ -332,20 +323,6 @@ class ReOpenableTempfile(object): # pragma: no cover
self.name = None
@contextlib.contextmanager
def temp_dir():
"""
A context manager for maintaining a temporary directory
"""
# NOTE: Drop this as soon as we don't support Python 2.7 anymore, because
# since Python 3.2 there is a context manager called TemporaryDirectory().
path = tempfile.mkdtemp(prefix='tess_')
try:
yield path
finally:
shutil.rmtree(path)
def image_to_string(image, lang=None, builder=None):
'''
Runs tesseract on the specified image. First, the image is written to disk,
......@@ -367,7 +344,7 @@ def image_to_string(image, lang=None, builder=None):
if builder is None:
builder = builders.TextBuilder()
with temp_dir() as tmpdir:
with tempfile.TemporaryDirectory() as tmpdir:
if image.mode != "RGB":
image = image.convert("RGB")
image.save(os.path.join(tmpdir, "input.bmp"))
......@@ -389,15 +366,7 @@ def image_to_string(image, lang=None, builder=None):
with codecs.open(output_file_name, 'r', encoding='utf-8',
errors='replace') as file_desc:
return builder.read_file(file_desc)
except FileNotFoundError as exc:
if sys.version_info < (3, 0):
# python2 has no FileNotFoundError specifid Exception
# so we rely on the errno of the IOError exception
if exc.errno == errno.ENOENT:
# file not found
continue
else:
raise exc
except FileNotFoundError:
continue
finally:
cleanup(output_file_name)
......@@ -408,7 +377,7 @@ def image_to_string(image, lang=None, builder=None):
def is_available():
_set_environment()
return util.is_on_path(TESSERACT_CMD)
return shutil.which(TESSERACT_CMD) is not None
def get_available_languages():
......
import os
import re
import six
def digits_only(string):
......@@ -10,26 +7,3 @@ def digits_only(string):
if match:
return int(match.group('digits'))
return 0
def to_unicode(string):
try:
return six.u(string)
except: # noqa: E722 # pragma: no cover
# probably already decoded
return string
def is_on_path(exec_name):
"""
Indicates if the command 'exec_name' appears to be installed.
Returns:
True --- if it is installed
False --- if it isn't
"""
for dirpath in os.environ["PATH"].split(os.pathsep):
path = os.path.join(dirpath, exec_name)
if os.path.exists(path) and os.access(path, os.X_OK):
return True
return False
import os
import unittest
from codecs import open
class BaseTest(unittest.TestCase):
tool = None
......
import sys
import unittest
import xml.dom.minidom
......@@ -13,7 +12,7 @@ class TestBox(unittest.TestCase):
self.box1 = builders.Box("word1", ((15, 22), (23, 42)))
self.box1_bis = builders.Box("word1_bis", ((15, 22), (23, 42)))
self.box2 = builders.Box("word2", ((30, 5), (40, 15)), 95)
self.box_unicode = builders.Box(u"\xe9", ((1, 2), (3, 4)))
self.box_unicode = builders.Box("\xe9", ((1, 2), (3, 4)))
def test_init(self):
self.assertEqual(self.box1.content, "word1")
......@@ -31,20 +30,10 @@ class TestBox(unittest.TestCase):
"bbox 15 22 23 42; x_wconf 0")
self.assertEqual(tag.firstChild.data, "word1")
def test_get_unicode_string(self):
self.assertEqual(self.box_unicode.get_unicode_string(),
u"\xe9 1 2 3 4")
def test_str_method(self):
self.assertEqual(str(self.box1), "word1 15 22 23 42")
@unittest.skipUnless(sys.version_info < (3, 0), "python2 box str")
def test_str_python2(self):
self.assertEqual(str(self.box_unicode),
u"\xe9 1 2 3 4".encode("utf-8"))
@unittest.skipIf(sys.version_info < (3, 0), "python3 box str")
def test_str_python3(self):
def test_str_unicode(self):
self.assertEqual(str(self.box_unicode), "\xe9 1 2 3 4")
def test_box_not_equal_none(self):
......@@ -80,7 +69,7 @@ class TestLineBox(unittest.TestCase):
box2 = builders.Box("word2", ((25, 23), (30, 32)))
box3 = builders.Box("word3", ((32, 25), (40, 32)), 95)
box4 = builders.Box("word4", ((41, 18), (44, 33)), 98)
box_unicode = builders.Box(u"\xe9", ((1, 2), (3, 4)), 98)
box_unicode = builders.Box("\xe9", ((1, 2), (3, 4)), 98)
self.line1 = builders.LineBox(
[box1, box2, box3, box4],
((14, 15), (45, 33))
......@@ -118,10 +107,6 @@ class TestLineBox(unittest.TestCase):
self.assertEqual(tag.firstChild.firstChild.data, "word1")
self.assertEqual(tag.lastChild.firstChild.data, "word4")
def test_get_unicode_string(self):
self.assertEqual(self.line_unicode.get_unicode_string(),
u"[\n word1 15 22 23 30\n \xe9 1 2 3 4\n] 1 2 3 4")
def test_line_str(self):
expected = "[\n"
for box in self.line1.word_boxes:
......@@ -129,16 +114,7 @@ class TestLineBox(unittest.TestCase):
expected += "] 14 15 45 33"
self.assertEqual(str(self.line1), expected)
@unittest.skipUnless(sys.version_info < (3, 0), "python2 line str")
def test_str_python2(self):
self.assertEqual(
str(self.line_unicode),
(u"[\n word1 15 22 23 30"
u"\n \xe9 1 2 3 4\n] 1 2 3 4").encode("utf-8")
)
@unittest.skipIf(sys.version_info < (3, 0), "python3 line str")
def test_str_python3(self):
def test_str_unicode(self):
self.assertEqual(
str(self.line_unicode),
"[\n word1 15 22 23 30\n \xe9 1 2 3 4\n] 1 2 3 4"
......
......@@ -3,10 +3,7 @@ import unittest
from io import StringIO
from itertools import product
from random import randint
try:
from unittest.mock import patch
except ImportError:
from mock import patch
from unittest.mock import patch
from pyocr import builders
......@@ -51,14 +48,14 @@ class TestTextBuilder(unittest.TestCase):
self.assertNotIn("--singlecolumn", builder.cuneiform_args)
def test_read_file(self):
txt = u"first line\nsecond line\n0123456789\n\U0001f5a8 "
txt = "first line\nsecond line\n0123456789\n🖨 "
input_fh = StringIO(txt)
output = self.builder.read_file(input_fh)
self.assertEqual(output, txt.strip())
def test_write_file(self):
output = StringIO()
txt = u"first line\nsecond line\n0123456789\n\U0001f5a8 "
txt = "first line\nsecond line\n0123456789\n🖨 "
self.builder.write_file(output, txt)
output.seek(0)
self.assertEqual(output.read(), txt)
......
import subprocess
from io import StringIO
try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock
from unittest.mock import patch, MagicMock
from PIL import Image
......@@ -19,28 +16,28 @@ class TestCuneiform(BaseTest):
These tests make sure the requirements for the tests are met.
"""
@patch("pyocr.util.is_on_path")
def test_available(self, is_on_path):
@patch("shutil.which")
def test_available(self, which):
# XXX is it useful?
is_on_path.return_value = True
which.return_value = True
self.assertTrue(cuneiform.is_available())
is_on_path.assert_called_once_with("cuneiform")
which.assert_called_once_with("cuneiform")
@patch("subprocess.Popen")
def test_version(self, popen):
stdout = MagicMock()
stdout.stdout.read.return_value = (
"Cuneiform for Linux 1.1.0\n"
"Usage: cuneiform [-l languagename -f format --dotmatrix --fax"
" --singlecolumn -o result_file] imagefile"
).encode()
b"Cuneiform for Linux 1.1.0\n"
b"Usage: cuneiform [-l languagename -f format --dotmatrix --fax"
b" --singlecolumn -o result_file] imagefile"
)
popen.return_value = stdout
self.assertSequenceEqual(cuneiform.get_version(), (1, 1, 0))
@patch("subprocess.Popen")
def test_version_error(self, popen):
stdout = MagicMock()
stdout.stdout.read.return_value = "\n".encode()
stdout.stdout.read.return_value = b"\n"
popen.return_value = stdout
self.assertIsNone(cuneiform.get_version())
......@@ -48,10 +45,10 @@ class TestCuneiform(BaseTest):
def test_langs(self, popen):
stdout = MagicMock()
stdout.stdout.read.return_value = (
"Cuneiform for Linux 1.1.0\n"
"Supported languages: eng ger fra rus swe spa ita ruseng ukr srp "
"hrv pol dan por dut cze rum hun bul slv lav lit est tur."
).encode()
b"Cuneiform for Linux 1.1.0\n"
b"Supported languages: eng ger fra rus swe spa ita ruseng ukr srp "
b"hrv pol dan por dut cze rum hun bul slv lav lit est tur."
)
popen.return_value = stdout
langs = cuneiform.get_available_languages()
self.assertIn("eng", langs)
......@@ -90,9 +87,7 @@ class TestCuneiformTxt(BaseTest):
self.image = Image.new(mode="RGB", size=(1, 1))
self.text_file = StringIO(self._get_file_content("text"))
self.stdout = MagicMock()
self.stdout.stdout.read.return_value = (
"Cuneiform for Linux 1.1.0\n".encode()
)
self.stdout.stdout.read.return_value = b"Cuneiform for Linux 1.1.0\n"
self.stdout.wait.return_value = 0
self.tmp_filename = "/tmp/cuneiform_n0qfk87otxt"
self.enter = MagicMock()
......@@ -213,9 +208,7 @@ class TestCuneiformWordBox(BaseTest):
self.image = Image.new(mode="RGB", size=(1, 1))
self.text_file = StringIO(self._get_file_content("cuneiform.words"))
self.stdout = MagicMock()
self.stdout.stdout.read.return_value = (
"Cuneiform for Linux 1.1.0\n".encode()
)
self.stdout.stdout.read.return_value = b"Cuneiform for Linux 1.1.0\n"
self.stdout.wait.return_value = 0
self.tmp_filename = "/tmp/cuneiform_n0qfk87otxt"
self.enter = MagicMock()
......@@ -268,9 +261,7 @@ class TestCuneiformLineBox(BaseTest):
self.image = Image.new(mode="RGB", size=(1, 1))
self.text_file = StringIO(self._get_file_content("cuneiform.lines"))
self.stdout = MagicMock()
self.stdout.stdout.read.return_value = (
"Cuneiform for Linux 1.1.0\n".encode()
)
self.stdout.stdout.read.return_value = b"Cuneiform for Linux 1.1.0\n"
self.stdout.wait.return_value = 0
self.tmp_filename = "/tmp/cuneiform_n0qfk87otxt"
self.enter = MagicMock()
......
......@@ -3,10 +3,7 @@ import os
from ctypes import POINTER, cast, c_char_p, c_int
from random import randint
try:
from unittest.mock import patch, call
except ImportError:
from mock import patch, call
from unittest.mock import patch, call
from PIL import Image
......@@ -266,7 +263,7 @@ class TestLibTesseractRaw(BaseTest):
@patch("pyocr.libtesseract.tesseract_raw.g_libtesseract")
def test_set_debug_file(self, libtess):
for filename in (u"file", b"file"):
for filename in ("file", b"file"):
tesseract_raw.set_debug_file(self.handle, filename)
self.assertEqual(
libtess.TessBaseAPISetVariable.call_count,
......
This diff is collapsed.
import unittest
import sys
try:
from unittest.mock import patch
except ImportError:
from mock import patch
from unittest.mock import patch
import pyocr
from pyocr.util import (
digits_only,
is_on_path,
to_unicode,
)
......@@ -19,10 +13,9 @@ class TestPyOCR(unittest.TestCase):
@patch("pyocr.libtesseract.tesseract_raw.g_libtesseract")
@patch("pyocr.libtesseract.tesseract_raw.is_available")
@patch("pyocr.util.is_on_path")
def test_available_tools_tesseract4(self, is_on_path,
is_available, libtess):
is_on_path.return_value = True
@patch("shutil.which")
def test_available_tools_tesseract4(self, which, is_available, libtess):
which.return_value = True
is_available.return_value = True
libtess.TessVersion.return_value = b"4.0.0"
self.assertListEqual(
......@@ -36,10 +29,9 @@ class TestPyOCR(unittest.TestCase):
@patch("pyocr.libtesseract.tesseract_raw.g_libtesseract")
@patch("pyocr.libtesseract.tesseract_raw.is_available")
@patch("pyocr.util.is_on_path")
def test_available_tools_tesseract3(self, is_on_path,
is_available, libtess):
is_on_path.return_value = True
@patch("shutil.which")
def test_available_tools_tesseract3(self, which, is_available, libtess):
which.return_value = True
is_available.return_value = True
libtess.TessVersion.return_value = b"3.5.0"
self.assertListEqual(
......@@ -53,10 +45,9 @@ class TestPyOCR(unittest.TestCase):
@patch("pyocr.libtesseract.tesseract_raw.g_libtesseract")
@patch("pyocr.libtesseract.tesseract_raw.is_available")
@patch("pyocr.util.is_on_path")
def test_available_tools_tesseract3_0(self, is_on_path,
is_available, libtess):
is_on_path.return_value = True
@patch("shutil.which")
def test_available_tools_tesseract3_0(self, which, is_available, libtess):
which.return_value = True
is_available.return_value = True
libtess.TessVersi