diff --git a/setup.py b/setup.py index e6b6f43e2..7b05f6697 100755 --- a/setup.py +++ b/setup.py @@ -631,7 +631,7 @@ def build(): ) log( f'build(): mupdf_build_dir={mupdf_build_dir!r}') - # Build rebased `extra` module. + # Build `extra` module. # if 'p' in PYMUPDF_SETUP_FLAVOUR: path_so_leaf = _build_extension( @@ -687,9 +687,8 @@ def add(flavour, from_, to_): # Add Windows .lib files. mupdf_build_dir2 = _windows_lib_directory(mupdf_local, build_type) add('d', f'{mupdf_build_dir2}/mupdfcpp{wp.cpu.windows_suffix}.lib', f'{to_dir_d}/lib/') - if mupdf_version_tuple >= (1, 26): - # MuPDF-1.25+ language bindings build also builds libmuthreads. - add('d', f'{mupdf_build_dir2}/libmuthreads.lib', f'{to_dir_d}/lib/') + # MuPDF-1.25+ language bindings build also builds libmuthreads. + add('d', f'{mupdf_build_dir2}/libmuthreads.lib', f'{to_dir_d}/lib/') elif darwin: add('p', f'{mupdf_build_dir}/_mupdf.so', to_dir) add('b', f'{mupdf_build_dir}/libmupdfcpp.so', to_dir) @@ -983,10 +982,8 @@ def build_mupdf_unix( # a system limit, not the actual limit of the current shell, and there # doesn't seem to be a way to find the current shell's limit. # - build_prefix = f'PyMuPDF-' - if mupdf_version_tuple >= (1, 26): - # Avoid link command length problems seen on musllinux. - build_prefix = '' + # Avoid link command length problems seen on musllinux. + build_prefix = '' if pyodide: build_prefix += 'pyodide-' else: @@ -1094,8 +1091,7 @@ def _build_extension( mupdf_local, mupdf_build_dir, build_type, g_py_limited_api f'{mupdf_local}/include', ) - # Build rebased extension module. - log('Building PyMuPDF rebased.') + log('Building PyMuPDF extension.') compile_extra_cpp = '' if darwin: # Avoids `error: cannot pass object of non-POD type diff --git a/src/__init__.py b/src/__init__.py index 874e59cc4..938291b15 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -6784,12 +6784,9 @@ def select(self, pyliste): pdf = _as_pdf_document(self) # create page sub-pdf via pdf_rearrange_pages2(). # - if mupdf_version_tuple >= (1, 25, 3): - # We use PDF_CLEAN_STRUCTURE_KEEP otherwise we lose structure tree - # which, for example, breaks test_3705. - mupdf.pdf_rearrange_pages2(pdf, pyliste, mupdf.PDF_CLEAN_STRUCTURE_KEEP) - else: - mupdf.pdf_rearrange_pages2(pdf, pyliste) + # We use PDF_CLEAN_STRUCTURE_KEEP otherwise we lose structure tree + # which, for example, breaks test_3705. + mupdf.pdf_rearrange_pages2(pdf, pyliste, mupdf.PDF_CLEAN_STRUCTURE_KEEP) # remove any existing pages with their kids self._reset_page_refs() @@ -17600,14 +17597,13 @@ def width(self): if mupdf_version_tuple >= (1, 27, 1): TEXT_LAZY_VECTORS = mupdf.FZ_STEXT_LAZY_VECTORS -if mupdf_version_tuple >= (1, 26): - TEXT_PARAGRAPH_BREAK = mupdf.FZ_STEXT_PARAGRAPH_BREAK - TEXT_TABLE_HUNT = mupdf.FZ_STEXT_TABLE_HUNT - TEXT_COLLECT_STYLES = mupdf.FZ_STEXT_COLLECT_STYLES - TEXT_USE_GID_FOR_UNKNOWN_UNICODE = mupdf.FZ_STEXT_USE_GID_FOR_UNKNOWN_UNICODE - TEXT_CLIP_RECT = mupdf.FZ_STEXT_CLIP_RECT - TEXT_ACCURATE_ASCENDERS = mupdf.FZ_STEXT_ACCURATE_ASCENDERS - TEXT_ACCURATE_SIDE_BEARINGS = mupdf.FZ_STEXT_ACCURATE_SIDE_BEARINGS +TEXT_PARAGRAPH_BREAK = mupdf.FZ_STEXT_PARAGRAPH_BREAK +TEXT_TABLE_HUNT = mupdf.FZ_STEXT_TABLE_HUNT +TEXT_COLLECT_STYLES = mupdf.FZ_STEXT_COLLECT_STYLES +TEXT_USE_GID_FOR_UNKNOWN_UNICODE = mupdf.FZ_STEXT_USE_GID_FOR_UNKNOWN_UNICODE +TEXT_CLIP_RECT = mupdf.FZ_STEXT_CLIP_RECT +TEXT_ACCURATE_ASCENDERS = mupdf.FZ_STEXT_ACCURATE_ASCENDERS +TEXT_ACCURATE_SIDE_BEARINGS = mupdf.FZ_STEXT_ACCURATE_SIDE_BEARINGS # 2025-05-07: Non-standard names preserved for backwards compatibility. TEXT_STEXT_SEGMENT = TEXT_SEGMENT @@ -20532,8 +20528,7 @@ def __init__(self, rhs=None): if rhs: self.size = rhs.size self.flags = rhs.flags - if mupdf_version_tuple >= (1, 25, 2): - self.char_flags = rhs.char_flags + self.char_flags = rhs.char_flags self.font = rhs.font self.argb = rhs.argb self.asc = rhs.asc @@ -20542,8 +20537,7 @@ def __init__(self, rhs=None): else: self.size = -1 self.flags = -1 - if mupdf_version_tuple >= (1, 25, 2): - self.char_flags = -1 + self.char_flags = -1 self.font = '' self.argb = -1 self.asc = 0 @@ -20551,8 +20545,7 @@ def __init__(self, rhs=None): self.bidi = 0 def __str__(self): ret = f'{self.size} {self.flags}' - if mupdf_version_tuple >= (1, 25, 2): - ret += f' {self.char_flags}' + ret += f' {self.char_flags}' ret += f' {self.font} {self.color} {self.asc} {self.desc}' return ret @@ -20580,9 +20573,8 @@ def __str__(self): origin = mupdf.FzPoint(ch.m_internal.origin) style.size = ch.m_internal.size style.flags = flags - if mupdf_version_tuple >= (1, 25, 2): - # FZ_STEXT_SYNTHETIC is per-char, not per-span. - style.char_flags = ch.m_internal.flags & ~mupdf.FZ_STEXT_SYNTHETIC + # FZ_STEXT_SYNTHETIC is per-char, not per-span. + style.char_flags = ch.m_internal.flags & ~mupdf.FZ_STEXT_SYNTHETIC style.font = JM_font_name(mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font))) style.argb = ch.m_internal.argb style.asc = JM_font_ascender(mupdf.FzFont(mupdf.ll_fz_keep_font(ch.m_internal.font))) @@ -20591,9 +20583,7 @@ def __str__(self): if (style.size != old_style.size or style.flags != old_style.flags - or (mupdf_version_tuple >= (1, 25, 2) - and (style.char_flags != old_style.char_flags) - ) + or (style.char_flags != old_style.char_flags) or style.argb != old_style.argb or style.font != old_style.font or style.bidi != old_style.bidi @@ -20625,12 +20615,10 @@ def __str__(self): span[dictkey_size] = style.size span[dictkey_flags] = style.flags span[dictkey_bidi] = style.bidi - if mupdf_version_tuple >= (1, 25, 2): - span[dictkey_char_flags] = style.char_flags + span[dictkey_char_flags] = style.char_flags span[dictkey_font] = JM_EscapeStrFromStr(style.font) span[dictkey_color] = style.argb & 0xffffff - if mupdf_version_tuple >= (1, 25, 0): - span['alpha'] = style.argb >> 24 + span['alpha'] = style.argb >> 24 span["ascender"] = asc span["descender"] = desc @@ -25896,6 +25884,6 @@ def deprecated_function( *args, **kwargs): __version__ = VersionBind __doc__ = ( - f'PyMuPDF {VersionBind}: Python bindings for the MuPDF {VersionFitz} library (rebased implementation).\n' + f'PyMuPDF {VersionBind}: Python bindings for the MuPDF {VersionFitz} library.\n' f'Python {sys.version_info[0]}.{sys.version_info[1]} running on {sys.platform} ({64 if sys.maxsize > 2**32 else 32}-bit).\n' ) diff --git a/src/extra.i b/src/extra.i index 8e4e75efb..8f4e79c63 100644 --- a/src/extra.i +++ b/src/extra.i @@ -1337,9 +1337,9 @@ static PyObject *lll_JM_get_annot_xref_list(pdf_obj *page_obj) //------------------------------------------------------------------------ static PyObject* JM_get_annot_xref_list(const mupdf::PdfObj& page_obj) { - PyObject* names = PyList_New(0); if (!page_obj.m_internal) { + PyObject* names = PyList_New(0); return names; } return lll_JM_get_annot_xref_list( page_obj.m_internal); @@ -3081,7 +3081,6 @@ mupdf::FzRect JM_make_spanlist( float size = -1; unsigned flags = 0; - #if MUPDF_VERSION_GE(1, 25, 2) /* From mupdf:include/mupdf/fitz/structured-text.h:fz_stext_char::flags, which uses anonymous enum values: FZ_STEXT_STRIKEOUT = 1, @@ -3092,7 +3091,6 @@ mupdf::FzRect JM_make_spanlist( FZ_STEXT_CLIPPED = 64 */ unsigned char_flags = 0; - #endif const char *font = ""; unsigned argb = 0; @@ -3121,25 +3119,17 @@ mupdf::FzRect JM_make_spanlist( fz_point origin = ch.m_internal->origin; style.size = ch.m_internal->size; style.flags = flags; - #if MUPDF_VERSION_GE(1, 25, 2) /* FZ_STEXT_SYNTHETIC is per-char, not per-span. */ style.char_flags = ch.m_internal->flags & ~FZ_STEXT_SYNTHETIC; - #endif style.font = JM_font_name(ch.m_internal->font); - #if MUPDF_VERSION_GE(1, 25, 0) - style.argb = ch.m_internal->argb; - #else - style.argb = ch.m_internal->color; - #endif + style.argb = ch.m_internal->argb; style.asc = JM_font_ascender(ch.m_internal->font); style.desc = JM_font_descender(ch.m_internal->font); if (0 || style.size != old_style.size || style.flags != old_style.flags - #if MUPDF_VERSION_GE(1, 25, 2) || style.char_flags != old_style.char_flags - #endif || style.argb != old_style.argb || strcmp(style.font, old_style.font) != 0 || style.bidi != old_style.bidi @@ -3179,14 +3169,10 @@ mupdf::FzRect JM_make_spanlist( DICT_SETITEM_DROP(span, dictkey_size, Py_BuildValue("f", style.size)); DICT_SETITEM_DROP(span, dictkey_flags, Py_BuildValue("I", style.flags)); DICT_SETITEM_DROP(span, dictkey_bidi, Py_BuildValue("I", style.bidi)); - #if MUPDF_VERSION_GE(1, 25, 2) DICT_SETITEM_DROP(span, dictkey_char_flags, Py_BuildValue("I", style.char_flags)); - #endif DICT_SETITEM_DROP(span, dictkey_font, JM_EscapeStrFromStr(style.font)); DICT_SETITEM_DROP(span, dictkey_color, Py_BuildValue("I", style.argb & 0xffffff)); - #if MUPDF_VERSION_GE(1, 25, 0) DICT_SETITEMSTR_DROP(span, "alpha", Py_BuildValue("I", style.argb >> 24)); - #endif DICT_SETITEMSTR_DROP(span, "ascender", Py_BuildValue("f", asc)); DICT_SETITEMSTR_DROP(span, "descender", Py_BuildValue("f", desc)); diff --git a/tests/conftest.py b/tests/conftest.py index 92079b063..44c5e4d02 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ def install_required_packages(): # We can't run child processes, so rely on required test packages # already being installed, e.g. in our wheel's . return - packages = 'pytest fontTools pymupdf-fonts flake8 pylint codespell' + packages = 'pytest fontTools pymupdf-fonts flake8 pylint codespell mypy' if platform.system() == 'Windows' and int.bit_length(sys.maxsize+1) == 32: # No pillow wheel available, and doesn't build easily. pass diff --git a/tests/resources/test_4751.pdf b/tests/resources/test_4751.pdf new file mode 100644 index 000000000..4b196b6e0 Binary files /dev/null and b/tests/resources/test_4751.pdf differ diff --git a/tests/test_2548.py b/tests/test_2548.py index 24261fc28..cc7ca8594 100644 --- a/tests/test_2548.py +++ b/tests/test_2548.py @@ -18,12 +18,7 @@ def test_2548(): _ = page.get_text() except Exception as ee: print(f'test_2548: {ee=}') - if hasattr(pymupdf, 'mupdf'): - # Rebased. - expected = "RuntimeError('code=2: cycle in structure tree')" - else: - # Classic. - expected = "RuntimeError('cycle in structure tree')" + expected = "RuntimeError('code=2: cycle in structure tree')" assert repr(ee) == expected, f'Expected {expected=} but got {repr(ee)=}.' e = True wt = pymupdf.TOOLS.mupdf_warnings() @@ -31,7 +26,6 @@ def test_2548(): # This checks that PyMuPDF 1.23.7 fixes this bug, and also that earlier # versions with updated MuPDF also fix the bug. - rebased = hasattr(pymupdf, 'mupdf') if pymupdf.mupdf_version_tuple >= (1, 27, 1): expected = '' elif pymupdf.mupdf_version_tuple >= (1, 27): @@ -39,6 +33,5 @@ def test_2548(): expected = '\n'.join([expected] * 5) else: expected = 'format error: cycle in structure tree\nstructure tree broken, assume tree is missing' - if rebased: - assert wt == expected, f'expected:\n {expected!r}\nwt:\n {wt!r}\n' + assert wt == expected, f'expected:\n {expected!r}\nwt:\n {wt!r}\n' assert not e diff --git a/tests/test_2904.py b/tests/test_2904.py index 21b55fb18..6749edd5f 100644 --- a/tests/test_2904.py +++ b/tests/test_2904.py @@ -27,12 +27,7 @@ def test_2904(): print(f'{pymupdf.mupdf_version_tuple=}: {page_id=} {i=} {e=} {img=}:') if page_id == 5 and i==3: assert e - if hasattr(pymupdf, 'mupdf'): - # rebased. - assert str(e) == 'code=8: Failed to read JPX header' - else: - # classic - assert str(e) == 'Failed to read JPX header' + assert str(e) == 'code=8: Failed to read JPX header' else: assert not e diff --git a/tests/test_annots.py b/tests/test_annots.py index 834d85cdc..b6df366f2 100644 --- a/tests/test_annots.py +++ b/tests/test_annots.py @@ -308,16 +308,13 @@ def test_2270(): print(f'{text=}') print(f'{getattr(textpage, "parent")=}') - if pymupdf.mupdf_version_tuple >= (1, 26): - # Check Annotation.get_textpage()'s arg. - clip = textBox.rect - clip.x1 = clip.x0 + (clip.x1 - clip.x0) / 3 - textpage2 = textBox.get_textpage(clip=clip) - text = textpage2.extractText() - print(f'With {clip=}: {text=}') - assert text == 'ab\n' - else: - assert not hasattr(pymupdf.mupdf, 'FZ_STEXT_CLIP_RECT') + # Check Annotation.get_textpage()'s arg. + clip = textBox.rect + clip.x1 = clip.x0 + (clip.x1 - clip.x0) / 3 + textpage2 = textBox.get_textpage(clip=clip) + text = textpage2.extractText() + print(f'With {clip=}: {text=}') + assert text == 'ab\n' def test_2934_add_redact_annot(): @@ -492,11 +489,7 @@ def test_4047(): def test_4079(): path = os.path.normpath(f'{__file__}/../../tests/resources/test_4079.pdf') - if pymupdf.mupdf_version_tuple >= (1, 25, 5): - path_after = os.path.normpath(f'{__file__}/../../tests/resources/test_4079_after.pdf') - else: - # 2024-11-27 Expect incorrect behaviour. - path_after = os.path.normpath(f'{__file__}/../../tests/resources/test_4079_after_1.25.pdf') + path_after = os.path.normpath(f'{__file__}/../../tests/resources/test_4079_after.pdf') path_out = os.path.normpath(f'{__file__}/../../tests/test_4079_out') with pymupdf.open(path_after) as document_after: diff --git a/tests/test_barcode.py b/tests/test_barcode.py index 99682514a..f660f751a 100644 --- a/tests/test_barcode.py +++ b/tests/test_barcode.py @@ -4,9 +4,6 @@ def test_barcode(): - if pymupdf.mupdf_version_tuple < (1, 26): - print(f'Not testing barcode because {pymupdf.mupdf_version=} < 1.26') - return path = os.path.normpath(f'{__file__}/../../tests/test_barcode_out.pdf') url = 'http://artifex.com' diff --git a/tests/test_codespell.py b/tests/test_codespell.py index cdcb5f022..9b837ea37 100644 --- a/tests/test_codespell.py +++ b/tests/test_codespell.py @@ -10,16 +10,12 @@ def test_codespell(): ''' - Check rebased Python code with codespell. + Check Python code with codespell. ''' if os.environ.get('PYODIDE_ROOT'): print('test_codespell(): not running on Pyodide - cannot run child processes.') return - if not hasattr(pymupdf, 'mupdf'): - print('Not running codespell with classic implementation.') - return - if platform.system() == 'Windows': # Git commands seem to fail on Github Windows runners. print(f'test_codespell(): Not running on Windows') diff --git a/tests/test_flake8.py b/tests/test_flake8.py index 8001cc77a..b35c34f2a 100644 --- a/tests/test_flake8.py +++ b/tests/test_flake8.py @@ -7,15 +7,12 @@ def test_flake8(): ''' - Check rebased Python code with flake8. + Check Python code with flake8. ''' if os.environ.get('PYODIDE_ROOT'): print('test_flake8(): not running on Pyodide - cannot run child processes.') return - if not hasattr(pymupdf, 'mupdf'): - print(f'Not running flake8 with classic implementation.') - return ignores = ( 'E123', # closing bracket does not match indentation of opening bracket's line 'E124', # closing bracket does not match visual indentation diff --git a/tests/test_font.py b/tests/test_font.py index 834db349d..1228def07 100644 --- a/tests/test_font.py +++ b/tests/test_font.py @@ -34,9 +34,8 @@ def test_font1(): # Also check we can get font's bbox. bbox1 = font.bbox print(f'{bbox1=}') - if hasattr(pymupdf, 'mupdf'): - bbox2 = font.this.fz_font_bbox() - assert bbox2 == bbox1 + bbox2 = font.this.fz_font_bbox() + assert bbox2 == bbox1 def test_font2(): @@ -105,9 +104,6 @@ def test_fontarchive(): ] def test_load_system_font(): - if not hasattr(pymupdf, 'mupdf'): - print(f'test_load_system_font(): Not running on classic.') - return trace = list() def font_f(name, bold, italic, needs_exact_metrics): trace.append((name, bold, italic, needs_exact_metrics)) @@ -130,9 +126,6 @@ def f_fallback(script, language, serif, bold, italic): def test_mupdf_subset_fonts2(): - if not hasattr(pymupdf, 'mupdf'): - print('Not running on rebased.') - return path = os.path.abspath(f'{__file__}/../../tests/resources/2.pdf') with pymupdf.open(path) as doc: n = len(doc) diff --git a/tests/test_general.py b/tests/test_general.py index 0b1635c16..05f6db8c6 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -86,13 +86,11 @@ def test_wrapcontents(): page.set_contents(xref) assert len(page.get_contents()) == 1 page.clean_contents() - rebased = hasattr(pymupdf, 'mupdf') - if rebased: - wt = pymupdf.TOOLS.mupdf_warnings() - if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27): - assert wt == 'bogus font ascent/descent values (0 / 0)\nPDF stream Length incorrect' - else: - assert wt == 'PDF stream Length incorrect' + wt = pymupdf.TOOLS.mupdf_warnings() + if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27): + assert wt == 'bogus font ascent/descent values (0 / 0)\nPDF stream Length incorrect' + else: + assert wt == 'PDF stream Length incorrect' def test_page_clean_contents(): @@ -300,8 +298,8 @@ def test_2533(): Search for a unique char on page and confirm that page.get_texttrace() returns the same bbox as the search method. """ - if hasattr(pymupdf, 'mupdf') and not pymupdf.g_use_extra: - print('Not running test_2533() because rebased with use_extra=0 known to fail') + if not pymupdf.g_use_extra: + print('Not running test_2533() because use_extra=0 known to fail') return pymupdf.TOOLS.set_small_glyph_heights(True) try: @@ -401,29 +399,20 @@ def test_2108(): print(f'') print(f'{pymupdf.mupdf_version_tuple=}') - if pymupdf.mupdf_version_tuple >= (1, 21, 2): - print('Asserting text==text_expected') - assert text == text_expected - else: - print('Asserting text!=text_expected') - assert text != text_expected + print('Asserting text==text_expected') + assert text == text_expected def test_2238(): filepath = f'{scriptdir}/resources/test2238.pdf' doc = pymupdf.open(filepath) - rebased = hasattr(pymupdf, 'mupdf') - if rebased: - wt = pymupdf.TOOLS.mupdf_warnings() - wt_expected = '' - if pymupdf.mupdf_version_tuple >= (1, 26): - wt_expected += 'garbage bytes before version marker\n' - wt_expected += 'syntax error: expected \'obj\' keyword (6 0 ?)\n' - else: - wt_expected += 'format error: cannot recognize version marker\n' - wt_expected += 'trying to repair broken xref\n' - wt_expected += 'repairing PDF document' - assert wt == wt_expected, f'{wt=}' + wt = pymupdf.TOOLS.mupdf_warnings() + wt_expected = '' + wt_expected += 'garbage bytes before version marker\n' + wt_expected += 'syntax error: expected \'obj\' keyword (6 0 ?)\n' + wt_expected += 'trying to repair broken xref\n' + wt_expected += 'repairing PDF document' + assert wt == wt_expected, f'{wt=}' first_page = doc.load_page(0).get_text('text', clip=pymupdf.INFINITE_RECT()) last_page = doc.load_page(-1).get_text('text', clip=pymupdf.INFINITE_RECT()) @@ -619,7 +608,6 @@ def test_2596(): page = doc.reload_page(page) pix1 = page.get_pixmap() assert pix1.samples == pix0.samples - rebased = hasattr(pymupdf, 'mupdf') if pymupdf.mupdf_version_tuple < (1, 26, 6): wt = pymupdf.TOOLS.mupdf_warnings() assert wt == 'too many indirections (possible indirection cycle involving 24 0 R)' @@ -747,14 +735,12 @@ def assert_rects_approx_eq(a, b): print(f'test_2710(): {pymupdf.mupdf_version_tuple=}') # 2023-11-05: Currently broken in mupdf master. print(f'test_2710(): Not Checking page.rect and rect.') - rebased = hasattr(pymupdf, 'mupdf') - if rebased: - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == ( - "syntax error: cannot find ExtGState resource 'GS7'\n" - "syntax error: cannot find ExtGState resource 'GS8'\n" - "encountered syntax errors; page may not be correct" - ) + wt = pymupdf.TOOLS.mupdf_warnings() + assert wt == ( + "syntax error: cannot find ExtGState resource 'GS7'\n" + "syntax error: cannot find ExtGState resource 'GS8'\n" + "encountered syntax errors; page may not be correct" + ) def test_2736(): @@ -930,8 +916,6 @@ def test_3081(): path1 = os.path.abspath(f'{__file__}/../../tests/resources/1.pdf') path2 = os.path.abspath(f'{__file__}/../../tests/test_3081-2.pdf') - rebased = hasattr(pymupdf, 'mupdf') - import shutil import sys import traceback @@ -953,9 +937,8 @@ def next_fd(): page = document[0] fd2 = next_fd() document.close() - if rebased: - assert document.this is None - assert page.this is None + assert document.this is None + assert page.this is None try: document.page_count() except Exception as e: @@ -970,10 +953,7 @@ def next_fd(): except Exception as e: print(f'Received expected exception: {e}') #traceback.print_exc(file=sys.stdout) - if rebased: - assert str(e) == 'page is None' - else: - assert str(e) == 'orphaned object: parent is None' + assert str(e) == 'page is None' else: assert 0, 'Did not receive expected exception.' page = None @@ -996,17 +976,11 @@ def test_3112_set_xml_metadata(): document.set_xml_metadata('hello world') def test_archive_3126(): - if not hasattr(pymupdf, 'mupdf'): - print(f'Not running because known to fail with classic.') - return p = os.path.abspath(f'{__file__}/../../tests/resources') p = pathlib.Path(p) archive = pymupdf.Archive(p) def test_3140(): - if not hasattr(pymupdf, 'mupdf'): - print(f'Not running test_3140 on classic, because Page.insert_htmlbox() not available.') - return css2 = '' path = os.path.abspath(f'{__file__}/../../tests/resources/2.pdf') oldfile = os.path.abspath(f'{__file__}/../../tests/test_3140_old.pdf') @@ -1043,9 +1017,6 @@ def test_cli(): print('test_cli(): not running on Pyodide - cannot run child processes.') return - if not hasattr(pymupdf, 'mupdf'): - print('test_cli(): Not running on classic because of fitz_old.') - return import subprocess subprocess.run(f'pymupdf -h', shell=1, check=1) @@ -1108,9 +1079,6 @@ def test_cli_out(): print('test_cli_out(): not running on Pyodide - cannot run child processes.') return - if not hasattr(pymupdf, 'mupdf'): - print('test_cli(): Not running on classic because of fitz_old.') - return import platform import re import subprocess @@ -1395,10 +1363,6 @@ def relpath(path, start=None): def test_open(): - if not hasattr(pymupdf, 'mupdf'): - print('test_open(): not running on classic.') - return - import re import textwrap import traceback @@ -1635,24 +1599,20 @@ def dict_set_path(dict_, *items): with open(path_out, 'w') as f: json.dump(results, f, indent=4, sort_keys=1) - if pymupdf.mupdf_version_tuple >= (1, 26): - with open(os.path.normpath(f'{__file__}/../../tests/resources/test_open2_expected.json')) as f: - results_expected = json.load(f) - if results != results_expected: - print(f'results != results_expected:') - def show(r, name): - text = json.dumps(r, indent=4, sort_keys=1) - print(f'{name}:') - print(textwrap.indent(text, ' ')) - show(results_expected, 'results_expected') - show(results, 'results') - assert 0 + with open(os.path.normpath(f'{__file__}/../../tests/resources/test_open2_expected.json')) as f: + results_expected = json.load(f) + if results != results_expected: + print(f'results != results_expected:') + def show(r, name): + text = json.dumps(r, indent=4, sort_keys=1) + print(f'{name}:') + print(textwrap.indent(text, ' ')) + show(results_expected, 'results_expected') + show(results, 'results') + assert 0 def test_533(): - if not hasattr(pymupdf, 'mupdf'): - print('test_533(): Not running on classic.') - return path = os.path.abspath(f'{__file__}/../../tests/resources/2.pdf') doc = pymupdf.open(path) print() @@ -1805,10 +1765,7 @@ def test_3905(): else: assert 0 wt = pymupdf.TOOLS.mupdf_warnings() - if pymupdf.mupdf_version_tuple >= (1, 26): - assert wt == 'format error: cannot find version marker\ntrying to repair broken xref\nrepairing PDF document' - else: - assert wt == 'format error: cannot recognize version marker\ntrying to repair broken xref\nrepairing PDF document' + assert wt == 'format error: cannot find version marker\ntrying to repair broken xref\nrepairing PDF document' def test_3624(): path = os.path.normpath(f'{__file__}/../../tests/resources/test_3624.pdf') @@ -1854,10 +1811,7 @@ def test_4034(): pixmap2 = document[0].get_pixmap() rms = gentle_compare.pixmaps_rms(pixmap1, pixmap2) print(f'test_4034(): Comparison of original/cleaned page 0 pixmaps: {rms=}.') - if pymupdf.mupdf_version_tuple < (1, 25, 2): - assert 30 < rms < 50 - else: - assert rms == 0 + assert rms == 0 def test_4309(): document = pymupdf.open() @@ -1874,11 +1828,8 @@ def test_4263(): command = f'pymupdf clean -linear {path} {path_out}' print(f'Running: {command}') cp = subprocess.run(command, shell=1, check=0) - if pymupdf.mupdf_version_tuple < (1, 26): - assert cp.returncode == 0 - else: - # Support for linerarisation dropped in MuPDF-1.26. - assert cp.returncode + # Support for linerarisation dropped in MuPDF-1.26. + assert cp.returncode def test_4224(): path = os.path.normpath(f'{__file__}/../../tests/resources/test_4224.pdf') @@ -1888,9 +1839,6 @@ def test_4224(): path_pixmap = f'{path}.{page.number}.png' pixmap.save(path_pixmap) print(f'Have created: {path_pixmap}') - if pymupdf.mupdf_version_tuple < (1, 25, 5): - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == 'format error: negative code in 1d faxd\npadding truncated image' def test_4319(): # Have not seen this test reproduce issue #4319, but keeping it anyway. diff --git a/tests/test_memory.py b/tests/test_memory.py index cfaccbd92..7584e4438 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -225,9 +225,6 @@ def get_stat(): if pv < (3, 11): # Python < 3.11 has less reliable memory usage so we exclude. print(f'test_4125(): Not checking on {platform.python_version()=} because < 3.11.') - elif pymupdf.mupdf_version_tuple < (1, 25, 2): - rss_delta_expected = 4915200 * (len(state.rsss) - 3) - assert abs(1 - rss_delta / rss_delta_expected) < 0.15, f'{rss_delta_expected=}' else: # Before the fix, each iteration would leak 4.9MB. rss_delta_max = 100*1000 * (len(state.rsss) - 3) @@ -238,3 +235,116 @@ def get_stat(): # we don't actually check. # print(f'Not checking results because non-Linux behaviour is too variable.') + + +def _test_4751(): + import gc + import tracemalloc + + def analysis(stream_data, do_iter=True): + pdf_info = pymupdf.Document(stream=stream_data, filetype='pdf') + tmp_list = range(len(pdf_info)) + for page_num in tmp_list: + page = pdf_info[page_num] + raw_info = page.get_text('rawdict')['blocks'] + page_widgets_list = page.widgets() + if do_iter: + for widget_info in page_widgets_list: + print(widget_info) + del page_widgets_list + pdf_info.close() + pdf_info = None + pymupdf.TOOLS.store_shrink(100) + + file_path = os.path.normpath(f'{__file__}/../../tests/resources/test_4751.pdf') + + def log(text): + print(text, flush=1) + + # We filter out all allocations where leaf-most frame is in tracemalloc + # itself, or in test_memory.py itself, because these are not relevant + # to finding leaks in pymupdf. + # + tm_filters = [ + tracemalloc.Filter(inclusive=False, filename_pattern=tracemalloc.__file__, all_frames=True), + tracemalloc.Filter(inclusive=False, filename_pattern=__file__), + ] + + def get_snapshot(): + ''' + Wrapper for tracemalloc.take_snapshot() that filters out blocks with + backtraces that we are not interested in. + ''' + ret = tracemalloc.take_snapshot() + ret2 = ret.filter_traces(tm_filters) + #log(f' {len(ret.traces)=} => {len(ret2.traces)=}') + return ret2 + + # Check that `analysis()` does not leak. + # + num_leaks = 0 + with open(file_path,'rb') as f: + bytes_data = f.read() + + tracemalloc.start(30) + snapshot_prev = get_snapshot() + + for it in range(2): + log('') + log(f'{it=}') + + current, peak = tracemalloc.get_traced_memory() + log(f' {current=} {peak=}') + + analysis(bytes_data) + gc.collect() + snapshot = get_snapshot() + + top_stats = snapshot.compare_to(snapshot_prev, 'traceback') + snapshot_prev = snapshot + + top_stats = sorted(top_stats, key=lambda x: -x.size_diff) + for block_num, stat in enumerate(top_stats[0:10]): + if stat.size_diff > 0: + log(f' Leak detected') + log(f' {block_num=} {stat.size_diff=}: {stat}') + bt = '' + for frame in stat.traceback: + bt += f' {frame.filename}:{frame.lineno}\n' + log(bt) + # We ignore extra allocations in the first iteration. + if it != 0: + num_leaks += 1 + + assert not num_leaks, f'{num_leaks=}' + + +def test_4751(): + # We run the actual test in a child process, because otherwise previous + # tests seem to effect the leak detection causing false positives. It's + # possible that these could be real leaks, but they are not the ones + # we are testing for here. + # + if os.path.basename(__file__).startswith(f'test_fitz_'): + # Don't test the `fitz` alias, because we assume our leafname. + print(f'test_4751(): Not testing with fitz alias.') + return + + if os.environ.get('PYODIDE_ROOT'): + print('test_4751(): not running on Pyodide - cannot run child processes.') + return + + python_version = [int(i) for i in platform.python_version_tuple()[:2]] + python_version_tuple = tuple(python_version) + if python_version_tuple < (3, 13): + # We see additional leaks with python-3.12. + print(f'test_4751(): not running because known to fail on python < 3.13: {platform.python_version_tuple()=}') + return + + import subprocess + env_extra = dict(PYTHONPATH = os.path.abspath(f'{__file__}/..')) + command = f'{sys.executable} -c "import test_memory; test_memory._test_4751()"' + print('', flush=1) + print(f'test_4751(): Running: {command!r}', flush=1) + print(f'test_4751(): With: {env_extra=}', flush=1) + subprocess.run(command, shell=1, check=1, env=os.environ | env_extra) diff --git a/tests/test_mupdf_regressions.py b/tests/test_mupdf_regressions.py index 8816260f7..316ec7c81 100644 --- a/tests/test_mupdf_regressions.py +++ b/tests/test_mupdf_regressions.py @@ -50,11 +50,8 @@ def test_707727(): print(f'{rms=}', flush=1) pix0.save(os.path.normpath(f'{__file__}/../../tests/test_707727_pix0.png')) pix1.save(os.path.normpath(f'{__file__}/../../tests/test_707727_pix1.png')) - if pymupdf.mupdf_version_tuple >= (1, 25, 2): - # New sanitising gives small fp rounding errors. - assert rms < 0.05 - else: - assert rms == 0 + # New sanitising gives small fp rounding errors. + assert rms < 0.05 def test_707721(): diff --git a/tests/test_objectstreams.py b/tests/test_objectstreams.py index 257318681..2a792c855 100644 --- a/tests/test_objectstreams.py +++ b/tests/test_objectstreams.py @@ -6,10 +6,6 @@ def test_objectstream1(): This option compresses PDF object definitions into a special object type "ObjStm". We test its presence by searching for that /Type. """ - if not hasattr(pymupdf, "mupdf"): - # only implemented for rebased - return - # make some arbitrary page with content text = "Hello, World! Hallo, Welt!" doc = pymupdf.open() @@ -32,10 +28,6 @@ def test_objectstream2(): This option compresses PDF object definitions into a special object type "ObjStm". We test its presence by searching for that /Type. """ - if not hasattr(pymupdf, "mupdf"): - # only implemented for rebased - return - # make some arbitrary page with content text = "Hello, World! Hallo, Welt!" doc = pymupdf.open() @@ -58,9 +50,6 @@ def test_objectstream3(): """Test ez_save(). Should automatically use object streams """ - if not hasattr(pymupdf, "mupdf"): - # only implemented for rebased - return import io fp = io.BytesIO() diff --git a/tests/test_pixmap.py b/tests/test_pixmap.py index de9f8b937..76e564c04 100644 --- a/tests/test_pixmap.py +++ b/tests/test_pixmap.py @@ -184,7 +184,7 @@ def product(x, y): for xx in x: yield (xx, yy) n = 0 - # We use a small subset of the image because non-optimised rebase gets + # We use a small subset of the image because non-optimised build gets # very slow. for pos in product(range(100), range(100)): if sum(pix.pixel(pos[0], pos[1])) >= 600: @@ -195,11 +195,7 @@ def product(x, y): path_expected = os.path.normpath(f'{__file__}/../../tests/resources/test_3050_expected.png') rms = gentle_compare.pixmaps_rms(path_expected, path_out2) print(f'{rms=}') - if pymupdf.mupdf_version_tuple < (1, 26): - # Slight differences in rendering from fix for mupdf bug 708274. - assert rms < 0.2 - else: - assert rms == 0 + assert rms == 0 wt = pymupdf.TOOLS.mupdf_warnings() if (1, 26, 0) <= pymupdf.mupdf_version_tuple < (1, 27): assert wt == 'bogus font ascent/descent values (0 / 0)\nPDF stream Length incorrect' @@ -247,15 +243,13 @@ def test_3072(): pix = page_49.get_pixmap(clip=rect, matrix=zoom) image_save_path = f'{out}/2.jpg' pix.save(image_save_path, jpg_quality=95) - rebase = hasattr(pymupdf, 'mupdf') - if rebase: - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == ( - "syntax error: cannot find ExtGState resource 'BlendMode0'\n" - "encountered syntax errors; page may not be correct\n" - "syntax error: cannot find ExtGState resource 'BlendMode0'\n" - "encountered syntax errors; page may not be correct" - ) + wt = pymupdf.TOOLS.mupdf_warnings() + assert wt == ( + "syntax error: cannot find ExtGState resource 'BlendMode0'\n" + "encountered syntax errors; page may not be correct\n" + "syntax error: cannot find ExtGState resource 'BlendMode0'\n" + "encountered syntax errors; page may not be correct" + ) def test_3134(): doc = pymupdf.Document() @@ -418,11 +412,7 @@ def test_3448(): path_diff = os.path.normpath(f'{__file__}/../../tests/test_3448-diff.png') diff.save(path_diff) print(f'{rms=}') - if pymupdf.mupdf_version_tuple < (1, 25, 5): - # Prior to fix for mupdf bug 708274. - assert 1 < rms < 2 - else: - assert rms == 0 + assert rms == 0 def test_3854(): @@ -444,9 +434,6 @@ def test_3854(): # MuPDF using external third-party libs gives slightly different # behaviour. assert rms < 2 - elif pymupdf.mupdf_version_tuple < (1, 25, 5): - # # Prior to fix for mupdf bug 708274. - assert 0.5 < rms < 2 else: assert rms == 0 @@ -560,14 +547,9 @@ def test_4423(): print(f'Exception: {e}') ee = e - if (1, 25, 5) <= pymupdf.mupdf_version_tuple < (1, 26): - assert ee, f'Did not receive the expected exception.' - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == 'dropping unclosed output' - else: - assert not ee, f'Received unexpected exception: {e}' - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == 'format error: cannot find object in xref (56 0 R)\nformat error: cannot find object in xref (68 0 R)' + assert not ee, f'Received unexpected exception: {e}' + wt = pymupdf.TOOLS.mupdf_warnings() + assert wt == 'format error: cannot find object in xref (56 0 R)\nformat error: cannot find object in xref (68 0 R)' def test_4445(): @@ -589,10 +571,7 @@ def test_4445(): pixmap = page.get_pixmap() print(f'{pixmap.width=}') print(f'{pixmap.height=}') - if pymupdf.mupdf_version_tuple >= (1, 26): - assert (pixmap.width, pixmap.height) == (792, 612) - else: - assert (pixmap.width, pixmap.height) == (612, 792) + assert (pixmap.width, pixmap.height) == (792, 612) if 0: path_pixmap = f'{path}.png' pixmap.save(path_pixmap) diff --git a/tests/test_showpdfpage.py b/tests/test_showpdfpage.py index 2e6b27a18..ea5551f62 100644 --- a/tests/test_showpdfpage.py +++ b/tests/test_showpdfpage.py @@ -46,10 +46,8 @@ def test_2742(): dest.save(os.path.abspath(f'{__file__}/../../tests/test_2742-out.pdf')) print("The end!") - rebased = hasattr(pymupdf, 'mupdf') - if rebased: - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == ( - 'Circular dependencies! Consider page cleaning.\n' - '... repeated 3 times...' - ), f'{wt=}' + wt = pymupdf.TOOLS.mupdf_warnings() + assert wt == ( + 'Circular dependencies! Consider page cleaning.\n' + '... repeated 3 times...' + ), f'{wt=}' diff --git a/tests/test_tables.py b/tests/test_tables.py index 2c537de52..9e4fdb869 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -184,13 +184,10 @@ def test_2979(): ), f"{pymupdf.TOOLS.set_small_glyph_heights()=}" wt = pymupdf.TOOLS.mupdf_warnings() - if pymupdf.mupdf_version_tuple >= (1, 26, 0): - assert ( - wt - == "bogus font ascent/descent values (3117 / -2463)\n... repeated 2 times..." - ) - else: - assert not wt + assert ( + wt + == "bogus font ascent/descent values (3117 / -2463)\n... repeated 2 times..." + ) def test_3062(): diff --git a/tests/test_tesseract.py b/tests/test_tesseract.py index 7650e8381..11c383526 100644 --- a/tests/test_tesseract.py +++ b/tests/test_tesseract.py @@ -18,28 +18,19 @@ def test_tesseract(): path = os.path.abspath( f'{__file__}/../resources/2.pdf') doc = pymupdf.open( path) page = doc[5] - if hasattr(pymupdf, 'mupdf'): - # rebased. - if pymupdf.mupdf_version_tuple < (1, 25, 4): - tail = 'OCR initialisation failed' - else: - tail = 'Tesseract language initialisation failed' - if os.environ.get('PYODIDE_ROOT'): - e_expected = 'code=6: No OCR support in this build' - e_expected_type = pymupdf.mupdf.FzErrorUnsupported - else: - e_expected = f'code=3: {tail}' - if platform.system() == 'OpenBSD': - # 2023-12-12: For some reason the SWIG catch code only catches - # the exception as FzErrorBase. - e_expected_type = pymupdf.mupdf.FzErrorBase - print(f'OpenBSD workaround - expecting FzErrorBase, not FzErrorLibrary.') - else: - e_expected_type = pymupdf.mupdf.FzErrorLibrary + tail = 'Tesseract language initialisation failed' + if os.environ.get('PYODIDE_ROOT'): + e_expected = 'code=6: No OCR support in this build' + e_expected_type = pymupdf.mupdf.FzErrorUnsupported else: - # classic. - e_expected = 'OCR initialisation failed' - e_expected_type = None + e_expected = f'code=3: {tail}' + if platform.system() == 'OpenBSD': + # 2023-12-12: For some reason the SWIG catch code only catches + # the exception as FzErrorBase. + e_expected_type = pymupdf.mupdf.FzErrorBase + print(f'OpenBSD workaround - expecting FzErrorBase, not FzErrorLibrary.') + else: + e_expected_type = pymupdf.mupdf.FzErrorLibrary tessdata_prefix = os.environ.get('TESSDATA_PREFIX') if tessdata_prefix: tp = page.get_textpage_ocr(full=True) @@ -58,16 +49,6 @@ def test_tesseract(): assert type(e) == e_expected_type, f'{type(e)=} != {e_expected_type=}.' else: assert 0, f'Expected exception {e_expected!r}' - rebased = hasattr(pymupdf, 'mupdf') - if rebased: - wt = pymupdf.TOOLS.mupdf_warnings() - if pymupdf.mupdf_version_tuple < (1, 25, 4): - assert wt == ( - 'UNHANDLED EXCEPTION!\n' - 'library error: Tesseract initialisation failed' - ) - else: - assert not wt def test_3842b(): @@ -89,13 +70,7 @@ def test_3842b(): if 'No tessdata specified and Tesseract is not installed' in str(e): pass else: - if pymupdf.mupdf_version_tuple < (1, 25, 4): - assert 'OCR initialisation failed' in str(e) - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == 'UNHANDLED EXCEPTION!\nlibrary error: Tesseract initialisation failed\nUNHANDLED EXCEPTION!\nlibrary error: Tesseract initialisation failed', \ - f'Unexpected {wt=}' - else: - assert 'Tesseract language initialisation failed' in str(e) + assert 'Tesseract language initialisation failed' in str(e) def test_3842(): diff --git a/tests/test_textextract.py b/tests/test_textextract.py index b5ae3e901..cdb6e4bc7 100644 --- a/tests/test_textextract.py +++ b/tests/test_textextract.py @@ -446,16 +446,12 @@ def test_4147(): #print(f' line') for span in line['spans']: #print(f' span') - if pymupdf.mupdf_version_tuple >= (1, 25, 2): - #print(f' span: {span["flags"]=:#x} {span["char_flags"]=:#x}') - if expect_visible: - assert span['char_flags'] & pymupdf.mupdf.FZ_STEXT_FILLED - else: - assert not (span['char_flags'] & pymupdf.mupdf.FZ_STEXT_FILLED) - assert not (span['char_flags'] & pymupdf.mupdf.FZ_STEXT_STROKED) + #print(f' span: {span["flags"]=:#x} {span["char_flags"]=:#x}') + if expect_visible: + assert span['char_flags'] & pymupdf.mupdf.FZ_STEXT_FILLED else: - #print(f' span: {span["flags"]=:#x}') - assert 'char_flags' not in span + assert not (span['char_flags'] & pymupdf.mupdf.FZ_STEXT_FILLED) + assert not (span['char_flags'] & pymupdf.mupdf.FZ_STEXT_STROKED) # Check commit `add 'bidi' to span dict, add 'synthetic' to char dict.` assert span['bidi'] == 0 for ch in span['chars']: @@ -507,11 +503,7 @@ def test_4245(): path_diff = os.path.normpath(f'{__file__}/../../tests/resources/test_4245_diff.png') pixmap_diff.save(path_diff) print(f'{rms=}') - if pymupdf.mupdf_version_tuple < (1, 25, 5): - # Prior to fix for mupdf bug 708274. - assert 0.1 < rms < 0.2 - else: - assert rms < 0.01 + assert rms < 0.01 def test_4180(): @@ -532,11 +524,7 @@ def test_4180(): path_diff = os.path.normpath(f'{__file__}/../../tests/resources/test_4180_diff.png') pixmap_diff.save(path_diff) print(f'{rms=}') - if pymupdf.mupdf_version_tuple < (1, 25, 5): - # Prior to fix for mupdf bug 708274. - assert 0.2 < rms < 0.3 - else: - assert rms < 0.01 + assert rms < 0.01 def test_4182(): @@ -566,11 +554,7 @@ def test_4182(): pixmap_diff.save(path_diff) rms = gentle_compare.pixmaps_rms(path_expected, pixmap) print(f'{rms=}') - if pymupdf.mupdf_version_tuple < (1, 25, 5): - # Prior to fix for mupdf bug 708274. - assert 3 < rms < 3.5 - else: - assert rms < 0.01 + assert rms < 0.01 def test_4179(): @@ -655,11 +639,7 @@ def test_4179(): pixmap_diff.save(path_out_diff) print(f'Have saved to: {path_out_diff=}') print(f'{rms=}') - if pymupdf.mupdf_version_tuple < (1, 25, 5): - # Prior to fix for mupdf bug 708274, our rects are rendered slightly incorrectly. - assert 3.5 < rms < 4.5 - else: - assert rms < 0.01 + assert rms < 0.01 finally: pymupdf.TOOLS.set_aa_level(aa) diff --git a/tests/test_toc.py b/tests/test_toc.py index 92ab81894..14e500caf 100644 --- a/tests/test_toc.py +++ b/tests/test_toc.py @@ -128,14 +128,12 @@ def test_2788(): # Also test Page.get_links() bugfix from #2817. for page in document: page.get_links() - rebased = hasattr(pymupdf, 'mupdf') - if rebased: - wt = pymupdf.TOOLS.mupdf_warnings() - assert wt == ( - "syntax error: expected 'obj' keyword (0 3 ?)\n" - "trying to repair broken xref\n" - "repairing PDF document" - ), f'{wt=}' + wt = pymupdf.TOOLS.mupdf_warnings() + assert wt == ( + "syntax error: expected 'obj' keyword (0 3 ?)\n" + "trying to repair broken xref\n" + "repairing PDF document" + ), f'{wt=}' def test_toc_count(): diff --git a/tests/test_typing.py b/tests/test_typing.py index f8f907c88..7add1a5f3 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -20,8 +20,6 @@ def test_py_typed(): return print(f'test_py_typed(): {pymupdf.__path__=}') - run('pip uninstall -y mypy') - run('pip install mypy') root = os.path.abspath(f'{__file__}/../..') # Run mypy on this .py file; it will fail at `import pymypdf` if the