From b51a1ea7f548142c229c4331a2186651101419aa Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Wed, 5 Nov 2025 16:43:53 +0000 Subject: [PATCH 1/6] Update to 1.26.7 after release of 1.26.6. --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + changes.txt | 10 +++++++++- setup.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7fdb600ce..5d3eed2f7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,6 +46,7 @@ body: attributes: label: PyMuPDF version options: + - 1.26.7 - 1.26.6 - 1.26.5 - 1.26.4 diff --git a/changes.txt b/changes.txt index 970a75baf..c70720625 100644 --- a/changes.txt +++ b/changes.txt @@ -2,7 +2,14 @@ Change Log ========== -**Changes in version 1.26.6** +**Changes in version 1.26.7** + +Other: + + * Retrospectively mark `4756 `_ as fixed in 1.26.6. + + +**Changes in version 1.26.6** (2025-11-05) * Use MuPDF-1.26.11. @@ -15,6 +22,7 @@ Change Log * **Fixed** `4720 `_: Memory leaking in rewrite_images? * **Fixed** `4742 `_: 'Rect' object has no attribute 'get_area' * **Fixed** `4746 `_: Document.__init__() got an unexpected keyword argument 'encoding' + * **Fixed** `4756 `_: swig --version doesn't work in all versions of swig; -version should be used instead **Changes in version 1.26.5** (2025-10-10) diff --git a/setup.py b/setup.py index 676f0e1db..121c10c3f 100755 --- a/setup.py +++ b/setup.py @@ -1268,7 +1268,7 @@ def sdist(): # # PyMuPDF version. -version_p = '1.26.6' +version_p = '1.26.7' version_mupdf = '1.26.11' From 56e469e83628f456de51217937e404c099990a45 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 6 Nov 2025 15:20:51 +0000 Subject: [PATCH 2/6] tests/README.md: minor update. --- tests/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/README.md b/tests/README.md index b793d2489..3c0fb16fa 100644 --- a/tests/README.md +++ b/tests/README.md @@ -79,3 +79,11 @@ To skip tests before a particular test, set PYMUPDF_PYTEST_RESUME to the name of the function. For example PYMUPDF_PYTEST_RESUME=test_haslinks. + + +## Checks on all tests + +All tests are wrapped by tests/conftest.py:wrap(), which does additional checking. + +* Checks that pymupdf.TOOLS.mupdf_warnings() is empty. +* Checks tests do not modify globals such as small_glyph_heights. From 988323ee12814152c5ce11c972165af499a6f085 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 6 Nov 2025 15:21:08 +0000 Subject: [PATCH 3/6] tests/conftest.py: Don't attempt to install packages on pyodide - not supported. --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 8b9bd31b8..773cc80b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,11 @@ # before tests start to run. # def install_required_packages(): + PYODIDE_ROOT = os.environ.get('PYODIDE_ROOT') + if PYODIDE_ROOT: + # 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' if platform.system() == 'Windows' and int.bit_length(sys.maxsize+1) == 32: # No pillow wheel available, and doesn't build easily. From 81fa641f60661c21f6e7695dac5f2ed269f5e87d Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Thu, 6 Nov 2025 16:55:23 +0000 Subject: [PATCH 4/6] pipcl.py: run_if(): also detect changes to argv[0]. We compare hashes of argv[0]. This will help if we change swig between runs - in this case argv[0] itself may be unchanged, but the contents of the file `which argv[0]` will differ. --- pipcl.py | 120 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 15 deletions(-) diff --git a/pipcl.py b/pipcl.py index 64348d8b2..b82a50456 100644 --- a/pipcl.py +++ b/pipcl.py @@ -38,6 +38,7 @@ import inspect import io import os +import pickle import platform import re import shlex @@ -2690,15 +2691,17 @@ def run_if( command, out, *prerequisites, caller=1): Args: command: - The command to run. We write this into a file .cmd so that we - know to run a command if the command itself has changed. + The command to run. We write this and a hash of argv[0] into a file + .cmd so that we know to run a command if the command itself + has changed. out: Path of the output file. prerequisites: List of prerequisite paths or true/false/None items. If an item is None it is ignored, otherwise if an item is not a string we - immediately return it cast to a bool. + immediately return it cast to a bool. We recurse into directories, + effectively using the newest file in the directory. Returns: True if we ran the command, otherwise None. @@ -2757,25 +2760,75 @@ def run_if( command, out, *prerequisites, caller=1): >>> run_if( f'touch {out}', out, prerequisite, caller=0) pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out' + + We detect changes to the contents of argv[0]: + + Create a shell script and run it: + + >>> _ = subprocess.run('rm run_if_test_argv0.* 1>/dev/null 2>/dev/null || true', shell=1) + >>> with open('run_if_test_argv0.sh', 'w') as f: + ... print('#! /bin/sh', file=f) + ... print('echo hello world > run_if_test_argv0.out', file=f) + >>> _ = subprocess.run(f'chmod u+x run_if_test_argv0.sh', shell=1) + >>> run_if( f'./run_if_test_argv0.sh', f'run_if_test_argv0.out', caller=0) + pipcl.py:run_if(): Running command because: File does not exist: 'run_if_test_argv0.out' + pipcl.py:run_if(): Running: ./run_if_test_argv0.sh + True + + Running it a second time does nothing: + + >>> run_if( f'./run_if_test_argv0.sh', f'run_if_test_argv0.out', caller=0) + pipcl.py:run_if(): Not running command because up to date: 'run_if_test_argv0.out' + + Modify the script. + + >>> with open('run_if_test_argv0.sh', 'a') as f: + ... print('\\necho hello >> run_if_test_argv0.out', file=f) + + And now it is run because the hash of argv[0] has changed: + + >>> run_if( f'./run_if_test_argv0.sh', f'run_if_test_argv0.out', caller=0) + pipcl.py:run_if(): Running command because: arg0 hash has changed. + pipcl.py:run_if(): Running: ./run_if_test_argv0.sh + True ''' doit = False + + # Path of file containing pickle data for command and hash of command's + # first arg. cmd_path = f'{out}.cmd' + + def hash_get(path): + try: + with open(path, 'rb') as f: + return hashlib.md5(f.read()).hexdigest() + except Exception as e: + #log(f'Failed to get hash of {path=}: {e}') + return None + + command_args = shlex.split(command or '') + command_arg0_path = fs_find_in_paths(command_args[0]) + command_arg0_hash = hash_get(command_arg0_path) + + cmd_args, cmd_arg0_hash = (None, None) + if os.path.isfile(cmd_path): + with open(cmd_path, 'rb') as f: + try: + cmd_args, cmd_arg0_hash = pickle.load(f) + except Exception as e: + #log(f'pickle.load() failed with {cmd_path=}: {e}') + pass if not doit: + # Set doit if outfile does not exist. out_mtime = _fs_mtime( out) if out_mtime == 0: doit = f'File does not exist: {out!r}' if not doit: - if os.path.isfile( cmd_path): - with open( cmd_path) as f: - cmd = f.read() - else: - cmd = None - cmd_args = shlex.split(cmd or '') - command_args = shlex.split(command or '') + # Set doit if command has changed. if command_args != cmd_args: - if cmd is None: + if cmd_args is None: doit = 'No previous command stored' else: doit = f'Command has changed' @@ -2791,8 +2844,8 @@ def run_if( command, out, *prerequisites, caller=1): # shlex.split(). doit += ':\n' lines = difflib.unified_diff( - cmd.split(), - command.split(), + cmd_args, + command_args, lineterm='', ) # Skip initial lines. @@ -2801,6 +2854,13 @@ def run_if( command, out, *prerequisites, caller=1): for line in lines: doit += f' {line}\n' + if not doit: + # Set doit if argv[0] hash has changed. + #print(f'{cmd_arg0_hash=} {command_arg0_hash=}', file=sys.stderr) + if command_arg0_hash != cmd_arg0_hash: + doit = f'arg0 hash has changed.' + #doit = f'arg0 hash has changed from {cmd_arg0_hash=} to {command_arg0_hash=}..' + if not doit: # See whether any prerequisites are newer than target. def _make_prerequisites(p): @@ -2845,8 +2905,9 @@ def _make_prerequisites(p): run( command, caller=caller+1) # Write the command we ran, into `cmd_path`. - with open( cmd_path, 'w') as f: - f.write( command) + + with open(cmd_path, 'wb') as f: + pickle.dump((command_args, command_arg0_hash), f) return True else: log1( f'Not running command because up to date: {out!r}', caller=caller+1) @@ -2857,6 +2918,35 @@ def _make_prerequisites(p): ) +def fs_find_in_paths( name, paths=None, verbose=False): + ''' + Looks for `name` in paths and returns complete path. `paths` is list/tuple + or `os.pathsep`-separated string; if `None` we use `$PATH`. If `name` + contains `/`, we return `name` itself if it is a file or None, regardless + of . + ''' + if '/' in name: + return name if os.path.isfile( name) else None + if paths is None: + paths = os.environ.get( 'PATH', '') + if verbose: + log('From os.environ["PATH"]: {paths=}') + if isinstance( paths, str): + paths = paths.split( os.pathsep) + if verbose: + log('After split: {paths=}') + for path in paths: + p = os.path.join( path, name) + if verbose: + log('Checking {p=}') + if os.path.isfile( p): + if verbose: + log('Returning because is file: {p!r}') + return p + if verbose: + log('Returning None because not found: {name!r}') + + def _get_prerequisites(path): ''' Returns list of prerequisites from Makefile-style dependency file, e.g. From b91ca71492a72e1d2765023e22861c074a129a04 Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Fri, 7 Nov 2025 00:36:27 +0000 Subject: [PATCH 5/6] setup.py: also use swig-4.3.1 with pyodide. swig-4.4.0 breaks pyodide similarly to macos. --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 121c10c3f..46c27da9b 100755 --- a/setup.py +++ b/setup.py @@ -1412,8 +1412,9 @@ def platform_release_tuple(): print(f'OpenBSD: pip install of swig does not build; assuming `pkg_add swig`.') elif PYMUPDF_SETUP_SWIG: pass - elif darwin: - # 2025-10-27: new swig-4.4.0 fails badly at runtime. + elif darwin or os.environ.get('PYODIDE_ROOT'): + # 2025-10-27: new swig-4.4.0 fails badly at runtime on macos. + # 2025-11-06: similar for pyodide. ret.append('swig==4.3.1') else: ret.append('swig') From 86154561df7b1612f696618dbd02061878a0a69c Mon Sep 17 00:00:00 2001 From: Julian Smith Date: Fri, 7 Nov 2025 15:39:33 +0000 Subject: [PATCH 6/6] tests/test_pixmap.py:test_4435(): expect alloc failure on pyodide. --- tests/test_pixmap.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_pixmap.py b/tests/test_pixmap.py index 5e55b931f..3aa174da3 100644 --- a/tests/test_pixmap.py +++ b/tests/test_pixmap.py @@ -10,6 +10,7 @@ import os import platform +import re import subprocess import sys import tempfile @@ -521,7 +522,21 @@ def test_4435(): with pymupdf.open(path) as document: page = document[2] print(f'Calling page.get_pixmap().', flush=1) - pixmap = page.get_pixmap(alpha=False, dpi=120) + if os.environ.get('PYODIDE_ROOT'): + # 2025-11-07: Expect alloc failure. + try: + pixmap = page.get_pixmap(alpha=False, dpi=120) + except Exception as e: + print(f'Received exception: {type(e)=}: {e}') + assert isinstance(e, pymupdf.mupdf.FzErrorSystem), f'Unrecognised {type(e)=}' + m = re.match('code=2: malloc [(][0-9]+ bytes[)] failed', str(e)) + assert m, f'Unrecognised exception text: {e}' + return + else: + # Hopefully this means that mupdf has been fixed. + assert 0, 'Expected alloc failure on pyodide' + else: + pixmap = page.get_pixmap(alpha=False, dpi=120) print(f'Called page.get_pixmap().', flush=1) if pymupdf.mupdf_version_tuple < (1, 27): assert pymupdf.TOOLS.mupdf_warnings() == 'bogus font ascent/descent values (0 / 0)\n... repeated 9 times...'