Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ Change Log

**Changes in version 1.26.5**

* Use MuPDF-1.26.10.

* Fixed issues:

* **Fixed** `2883 <https://github.com/pymupdf/PyMuPDF/issues/2883>`_: Improve the Python type annotations for fitz_new
* **Fixed** `4507 <https://github.com/pymupdf/PyMuPDF/issues/4507>`_: Bugs in pyodide
* **Fixed** `4613 <https://github.com/pymupdf/PyMuPDF/issues/4613>`_: Thai and number blocks are not auto-scaled and get wrong hyphen when using in insert_htmlbox
* **Fixed** `4700 <https://github.com/pymupdf/PyMuPDF/issues/4700>`_: pymupdf.open() processes .zip file without raising
* **Fixed** `4716 <https://github.com/pymupdf/PyMuPDF/issues/4716>`_: Problems with unreadable characters

* Other:

* Partially address `2883 <https://github.com/pymupdf/PyMuPDF/issues/2883>`_: Improve the Python type annotations for fitz_new

We now define all class methods explicitly instead of with dynamic assignment.
* We now define all class methods explicitly instead of with dynamic assignment; this improves type hints.
* Removed `pymupdf.utils.Shape` class, was duplicate of `pymupdf.Shape`.
* Allow use of cibuildwheel to build and test on Pyodide.
* Fixed various Pyodide bugs.
* In documentation, added section about Linux wheels and glibc compatibility.
* Improved documentation of pymupdf.open()'s <filetype> arg.
* Retrospectively mark `4544 <https://github.com/pymupdf/PyMuPDF/issues/4544>`_ as fixed in 1.26.4.


Expand Down
134 changes: 96 additions & 38 deletions pipcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,32 @@
Python packaging operations, including PEP-517 support, for use by a `setup.py`
script.

The intention is to take care of as many packaging details as possible so that
setup.py contains only project-specific information, while also giving as much
flexibility as possible.
Overview:

For example we provide a function `build_extension()` that can be used to build
a SWIG extension, but we also give access to the located compiler/linker so
that a `setup.py` script can take over the details itself.
The intention is to take care of as many packaging details as possible so
that setup.py contains only project-specific information, while also giving
as much flexibility as possible.

Run doctests with: `python -m doctest pipcl.py`
For example we provide a function `build_extension()` that can be used
to build a SWIG extension, but we also give access to the located
compiler/linker so that a `setup.py` script can take over the details
itself.

For Graal we require that PIPCL_GRAAL_PYTHON is set to non-graal Python (we
build for non-graal except with Graal Python's include paths and library
directory).
Doctests:
Doctest strings are provided in some comments.

Test in the usual way with:
python -m doctest pipcl.py

Test specific functions/classes with:
python pipcl.py --doctest run_if ...

If no functions or classes are specified, this tests everything.

Graal:
For Graal we require that PIPCL_GRAAL_PYTHON is set to non-graal Python (we
build for non-graal except with Graal Python's include paths and library
directory).
'''

import base64
Expand Down Expand Up @@ -532,6 +545,12 @@ def assert_str_or_multi( v):
assert_str_or_multi( requires_external)
assert_str_or_multi( project_url)
assert_str_or_multi( provides_extra)

assert re.match('^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])\\Z', name, re.IGNORECASE), (
f'Invalid package name'
f' (https://packaging.python.org/en/latest/specifications/name-normalization/)'
f': {name!r}'
)

# https://packaging.python.org/en/latest/specifications/core-metadata/.
assert re.match('([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$', name, re.IGNORECASE), \
Expand Down Expand Up @@ -761,7 +780,7 @@ def build_sdist(self,
else:
items = self.fn_sdist()

prefix = f'{_normalise(self.name)}-{self.version}'
prefix = f'{_normalise2(self.name)}-{self.version}'
os.makedirs(sdist_directory, exist_ok=True)
tarpath = f'{sdist_directory}/{prefix}.tar.gz'
log2(f'Creating sdist: {tarpath}')
Expand Down Expand Up @@ -833,9 +852,11 @@ def tag_python(self):
Get two-digit python version, e.g. 'cp3.8' for python-3.8.6.
'''
if self.tag_python_:
return self.tag_python_
ret = self.tag_python_
else:
return 'cp' + ''.join(platform.python_version().split('.')[:2])
ret = 'cp' + ''.join(platform.python_version().split('.')[:2])
assert '-' not in ret
return ret

def tag_abi(self):
'''
Expand Down Expand Up @@ -891,10 +912,13 @@ def tag_platform(self):
ret = ret2

log0( f'tag_platform(): returning {ret=}.')
assert '-' not in ret
return ret

def wheel_name(self):
return f'{_normalise(self.name)}-{self.version}-{self.tag_python()}-{self.tag_abi()}-{self.tag_platform()}.whl'
ret = f'{_normalise2(self.name)}-{self.version}-{self.tag_python()}-{self.tag_abi()}-{self.tag_platform()}.whl'
assert ret.count('-') == 4, f'Expected 4 dash characters in {ret=}.'
return ret

def wheel_name_match(self, wheel):
'''
Expand Down Expand Up @@ -923,7 +947,7 @@ def wheel_name_match(self, wheel):
log2(f'py_limited_api; {tag_python=} compatible with {self.tag_python()=}.')
py_limited_api_compatible = True

log2(f'{_normalise(self.name) == name=}')
log2(f'{_normalise2(self.name) == name=}')
log2(f'{self.version == version=}')
log2(f'{self.tag_python() == tag_python=} {self.tag_python()=} {tag_python=}')
log2(f'{py_limited_api_compatible=}')
Expand All @@ -932,7 +956,7 @@ def wheel_name_match(self, wheel):
log2(f'{self.tag_platform()=}')
log2(f'{tag_platform.split(".")=}')
ret = (1
and _normalise(self.name) == name
and _normalise2(self.name) == name
and self.version == version
and (self.tag_python() == tag_python or py_limited_api_compatible)
and self.tag_abi() == tag_abi
Expand Down Expand Up @@ -1059,7 +1083,7 @@ def _argv_dist_info(self, root):
it writes to a slightly different directory.
'''
if root is None:
root = f'{self.name}-{self.version}.dist-info'
root = f'{normalise2(self.name)}-{self.version}.dist-info'
self._write_info(f'{root}/METADATA')
if self.license:
with open( f'{root}/COPYING', 'w') as f:
Expand Down Expand Up @@ -1347,7 +1371,7 @@ def __str__(self):
)

def _dist_info_dir( self):
return f'{_normalise(self.name)}-{self.version}.dist-info'
return f'{_normalise2(self.name)}-{self.version}.dist-info'

def _metainfo(self):
'''
Expand Down Expand Up @@ -1487,7 +1511,7 @@ def _fromto(self, p):
to_ = f'{self._dist_info_dir()}/{to_[ len(prefix):]}'
prefix = '$data/'
if to_.startswith( prefix):
to_ = f'{self.name}-{self.version}.data/{to_[ len(prefix):]}'
to_ = f'{_normalise2(self.name)}-{self.version}.data/{to_[ len(prefix):]}'
if isinstance(from_, str):
from_, _ = self._path_relative_to_root( from_, assert_within_root=False)
to_ = self._path_relative_to_root(to_)
Expand Down Expand Up @@ -2569,7 +2593,7 @@ def _cpu_name():
return f'x{32 if sys.maxsize == 2**31 - 1 else 64}'


def run_if( command, out, *prerequisites):
def run_if( command, out, *prerequisites, caller=1):
'''
Runs a command only if the output file is not up to date.

Expand Down Expand Up @@ -2599,21 +2623,26 @@ def run_if( command, out, *prerequisites):
... os.remove( out)
>>> if os.path.exists( f'{out}.cmd'):
... os.remove( f'{out}.cmd')
>>> run_if( f'touch {out}', out)
>>> run_if( f'touch {out}', out, caller=0)
pipcl.py:run_if(): Running command because: File does not exist: 'run_if_test_out'
pipcl.py:run_if(): Running: touch run_if_test_out
True

If we repeat, the output file will be up to date so the command is not run:

>>> run_if( f'touch {out}', out)
>>> run_if( f'touch {out}', out, caller=0)
pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'

If we change the command, the command is run:

>>> run_if( f'touch {out}', out)
pipcl.py:run_if(): Running command because: Command has changed
pipcl.py:run_if(): Running: touch run_if_test_out
>>> run_if( f'touch {out};', out, caller=0)
pipcl.py:run_if(): Running command because: Command has changed:
pipcl.py:run_if(): @@ -1,2 +1,2 @@
pipcl.py:run_if(): touch
pipcl.py:run_if(): -run_if_test_out
pipcl.py:run_if(): +run_if_test_out;
pipcl.py:run_if():
pipcl.py:run_if(): Running: touch run_if_test_out;
True

If we add a prerequisite that is newer than the output, the command is run:
Expand All @@ -2622,15 +2651,20 @@ def run_if( command, out, *prerequisites):
>>> prerequisite = 'run_if_test_prerequisite'
>>> run( f'touch {prerequisite}', caller=0)
pipcl.py:run(): Running: touch run_if_test_prerequisite
>>> run_if( f'touch {out}', out, prerequisite)
pipcl.py:run_if(): Running command because: Prerequisite is new: 'run_if_test_prerequisite'
>>> run_if( f'touch {out}', out, prerequisite, caller=0)
pipcl.py:run_if(): Running command because: Command has changed:
pipcl.py:run_if(): @@ -1,2 +1,2 @@
pipcl.py:run_if(): touch
pipcl.py:run_if(): -run_if_test_out;
pipcl.py:run_if(): +run_if_test_out
pipcl.py:run_if():
pipcl.py:run_if(): Running: touch run_if_test_out
True

If we repeat, the output will be newer than the prerequisite, so the
command is not run:

>>> run_if( f'touch {out}', out, prerequisite)
>>> run_if( f'touch {out}', out, prerequisite, caller=0)
pipcl.py:run_if(): Not running command because up to date: 'run_if_test_out'
'''
doit = False
Expand Down Expand Up @@ -2687,9 +2721,9 @@ def _make_prerequisites(p):
for p in prerequisites:
prerequisites_all += _make_prerequisites( p)
if 0:
log2( 'prerequisites_all:')
log2( 'prerequisites_all:', caller=caller+1)
for i in prerequisites_all:
log2( f' {i!r}')
log2( f' {i!r}', caller=caller+1)
pre_mtime = 0
pre_path = None
for prerequisite in prerequisites_all:
Expand All @@ -2715,16 +2749,16 @@ def _make_prerequisites(p):
os.remove( cmd_path)
except Exception:
pass
log1( f'Running command because: {doit}', caller=2)
log1( f'Running command because: {doit}', caller=caller+1)

run( command, caller=2)
run( command, caller=caller+1)

# Write the command we ran, into `cmd_path`.
with open( cmd_path, 'w') as f:
f.write( command)
return True
else:
log1( f'Not running command because up to date: {out!r}', caller=2)
log1( f'Not running command because up to date: {out!r}', caller=caller+1)

if 0:
log2( f'out_mtime={time.ctime(out_mtime)} pre_mtime={time.ctime(pre_mtime)}.'
Expand Down Expand Up @@ -2796,6 +2830,11 @@ def _normalise(name):
return re.sub(r"[-_.]+", "-", name).lower()


def _normalise2(name):
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/
return _normalise(name).replace('-', '_')


def _assert_version_pep_440(version):
assert re.match(
r'^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$',
Expand Down Expand Up @@ -2848,19 +2887,30 @@ def _log(text, level, caller):
print(f'{filename}:{fr.function}(): {line}', file=sys.stdout, flush=1)


def relpath(path, start=None):
def relpath(path, start=None, allow_up=True):
'''
A safe alternative to os.path.relpath(), avoiding an exception on Windows
if the drive needs to change - in this case we use os.path.abspath().

Args:
path:
Path to be processed.
start:
Start directory or current directory if None.
allow_up:
If false we return absolute path is <path> is not within <start>.
'''
if windows():
try:
return os.path.relpath(path, start)
ret = os.path.relpath(path, start)
except ValueError:
# os.path.relpath() fails if trying to change drives.
return os.path.abspath(path)
ret = os.path.abspath(path)
else:
return os.path.relpath(path, start)
ret = os.path.relpath(path, start)
if not allow_up and ret.startswith('../') or ret.startswith('..\\'):
ret = os.path.abspath(path)
return ret


def _so_suffix(use_so_versioning=True):
Expand Down Expand Up @@ -3218,7 +3268,15 @@ def venv_run(args, path, recreate=True, clean=False):
# graal_legacy_python_config is true.
#
includes, ldflags = sysconfig_python_flags()
if sys.argv[1:] == ['--graal-legacy-python-config', '--includes']:
if sys.argv[1] == '--doctest':
import doctest
if sys.argv[2:]:
for f in sys.argv[2:]:
ff = globals()[f]
doctest.run_docstring_examples(ff, globals())
else:
doctest.testmod(None)
elif sys.argv[1:] == ['--graal-legacy-python-config', '--includes']:
print(includes)
elif sys.argv[1:] == ['--graal-legacy-python-config', '--ldflags']:
print(ldflags)
Expand Down
2 changes: 1 addition & 1 deletion scripts/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
Use specified prefix when running pytest, must be one of:
gdb
helgrind
vagrind
valgrind

-v <venv>
venv is:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,7 @@ def sdist():
# PyMuPDF version.
version_p = '1.26.5'

version_mupdf = '1.26.7'
version_mupdf = '1.26.10'

# PyMuPDFb version. This is the PyMuPDF version whose PyMuPDFb wheels we will
# (re)use if generating separate PyMuPDFb wheels. Though as of PyMuPDF-1.24.11
Expand Down
Binary file added tests/resources/test_4712_a.pdf
Binary file not shown.
Binary file added tests/resources/test_4712_b.pdf
Binary file not shown.
Loading