Fixes for python 3.14 (#17)

* Replace pkg_resources with importlib.resources for Python 3.14 compatibility

* fix self.XPATH() calls to OPF.XPath() instead since 3.14 made functools.partial a descriptor and thus injected self as the first argument

* updated install instruction to include lxml to avoid using non matching system version

* Fix regex in LRF output that stripped CThumbnail metadata. .*? matched until >\n and cut off all children on the same line

---------

Co-authored-by: wave1martian <wave1martian@buildsleeprepeat.com>
This commit is contained in:
Roman Dobosz
2026-05-13 21:18:33 +02:00
committed by GitHub
14 changed files with 45 additions and 52 deletions
+6 -1
View File
@@ -123,7 +123,12 @@ managers), i.e:
$ . venv/bin/activate $ . venv/bin/activate
(venv) $ git clone https://github.com/gryf/ebook-converter (venv) $ git clone https://github.com/gryf/ebook-converter
(venv) $ cd ebook-converter (venv) $ cd ebook-converter
(venv) $ pip install . (venv) $ pip install --no-binary lxml .
Note: the ``--no-binary lxml`` flag is required to ensure ``lxml`` is compiled
against the same system ``libxml2`` as ``html5-parser``. Without it, pip may
install a pre-built ``lxml`` wheel bundling a different ``libxml2`` version,
which causes a ``RuntimeError``.
Simple as that. And from now on, you can issue converter: Simple as that. And from now on, you can issue converter:
+2 -3
View File
@@ -1,6 +1,5 @@
import mimetypes import mimetypes
import pkg_resources from importlib.resources import files
mimetypes.init([pkg_resources. mimetypes.init([str(files('ebook_converter').joinpath('data/mime.types'))])
resource_filename('ebook_converter', 'data/mime.types')])
+2 -3
View File
@@ -7,7 +7,7 @@ import mimetypes
import numbers import numbers
import optparse import optparse
import os import os
import pkg_resources from importlib.resources import files
import re import re
import sys import sys
@@ -351,8 +351,7 @@ def read_sr_patterns(path, log=None):
def main(args=sys.argv): def main(args=sys.argv):
log = logging.default_log log = logging.default_log
mimetypes.init([pkg_resources.resource_filename('ebook_converter', mimetypes.init([str(files('ebook_converter').joinpath('data/mime.types'))])
'data/mime.types')])
parser, plumber = create_option_parser(args, log) parser, plumber = create_option_parser(args, log)
opts, leftover_args = parser.parse_args(args) opts, leftover_args = parser.parse_args(args)
if len(leftover_args) > 3: if len(leftover_args) > 3:
@@ -3,7 +3,7 @@ Convert .fb2 files to .lrf
""" """
import mimetypes import mimetypes
import os import os
import pkg_resources from importlib.resources import files
import re import re
from lxml import etree from lxml import etree
@@ -85,8 +85,8 @@ class FB2Input(InputFormatPlugin):
css = re.sub(r'name\s*=\s*', 'class=', css) css = re.sub(r'name\s*=\s*', 'class=', css)
self.extract_embedded_content(doc) self.extract_embedded_content(doc)
log.debug('Converting XML to HTML...') log.debug('Converting XML to HTML...')
with open(pkg_resources.resource_filename('ebook_converter', with open(str(files('ebook_converter')
'data/fb2.xsl')) as f: .joinpath('data/fb2.xsl'))) as f:
ss = f.read() ss = f.read()
ss = ss.replace("__FB_NS__", fb_ns) ss = ss.replace("__FB_NS__", fb_ns)
if options.no_inline_fb2_toc: if options.no_inline_fb2_toc:
@@ -1,5 +1,5 @@
import os import os
import pkg_resources from importlib.resources import files
import re import re
import shutil import shutil
@@ -92,9 +92,8 @@ class HTMLOutput(OutputFormatPlugin):
with open(opts.template_html_index, 'rb') as f: with open(opts.template_html_index, 'rb') as f:
template_html_index_data = f.read() template_html_index_data = f.read()
else: else:
with open(pkg_resources. with open(str(files('ebook_converter')
resource_filename('ebook_converter', .joinpath('data/html_export_default_index.tmpl'))
'data/html_export_default_index.tmpl')
) as fobj: ) as fobj:
template_html_index_data = fobj.read().decode() template_html_index_data = fobj.read().decode()
@@ -102,9 +101,8 @@ class HTMLOutput(OutputFormatPlugin):
with open(opts.template_html, 'rb') as f: with open(opts.template_html, 'rb') as f:
template_html_data = f.read() template_html_data = f.read()
else: else:
with open(pkg_resources. with open(str(files('ebook_converter')
resource_filename('ebook_converter', .joinpath('data/html_export_default.tmpl'))
'data/html_export_default.tmpl')
) as fobj: ) as fobj:
template_html_data = fobj.read().decode() template_html_data = fobj.read().decode()
@@ -112,9 +110,8 @@ class HTMLOutput(OutputFormatPlugin):
with open(opts.template_css, 'rb') as f: with open(opts.template_css, 'rb') as f:
template_css_data = f.read() template_css_data = f.read()
else: else:
with open(pkg_resources. with open(str(files('ebook_converter')
resource_filename('ebook_converter', .joinpath('data/html_export_default.css'))
'data/html_export_default.css')
) as fobj: ) as fobj:
template_css_data = fobj.read().decode() template_css_data = fobj.read().decode()
@@ -1,6 +1,6 @@
import os import os
import sys import sys
import pkg_resources from importlib.resources import files
from lxml import etree from lxml import etree
@@ -52,9 +52,8 @@ class LRFInput(InputFormatPlugin):
self.log.info('Converting XML to HTML...') self.log.info('Converting XML to HTML...')
with open(pkg_resources. with open(str(files('ebook_converter')
resource_filename('ebook_converter', .joinpath('data/lrf.xsl'))) as fobj:
'data/lrf.xsl')) as fobj:
# TODO(gryf): change this nonsense to etree.parse() instead. # TODO(gryf): change this nonsense to etree.parse() instead.
styledoc = etree.fromstring(fobj.read()) styledoc = etree.fromstring(fobj.read())
media_type = MediaType() media_type = MediaType()
@@ -1,6 +1,6 @@
import glob import glob
import os import os
import pkg_resources from importlib.resources import files
import re import re
import textwrap import textwrap
@@ -288,8 +288,8 @@ class RTFInput(InputFormatPlugin):
self.log.info('Converting XML to HTML...') self.log.info('Converting XML to HTML...')
inline_class = InlineClass(self.log) inline_class = InlineClass(self.log)
with open(pkg_resources.resource_filename('ebook_converter', with open(str(files('ebook_converter')
'data/rtf.xsl')) as fobj: .joinpath('data/rtf.xsl'))) as fobj:
styledoc = etree.fromstring(fobj.read()) styledoc = etree.fromstring(fobj.read())
extensions = {('calibre', 'inline-class'): inline_class} extensions = {('calibre', 'inline-class'): inline_class}
transform = etree.XSLT(styledoc, extensions=extensions) transform = etree.XSLT(styledoc, extensions=extensions)
+1 -1
View File
@@ -670,7 +670,7 @@ class Info(Delegator):
f = io.BytesIO() f = io.BytesIO()
tree.write(f, encoding='utf-8', xml_declaration=True) tree.write(f, encoding='utf-8', xml_declaration=True)
xmlInfo = f.getvalue().decode('utf-8') xmlInfo = f.getvalue().decode('utf-8')
xmlInfo = re.sub(r"<CThumbnail.*?>\n", "", xmlInfo) xmlInfo = re.sub(r"<CThumbnail[^>]*/>\s*", "", xmlInfo)
xmlInfo = xmlInfo.replace("SumPage>", "Page>") xmlInfo = xmlInfo.replace("SumPage>", "Page>")
lrfWriter.docInfoXml = xmlInfo lrfWriter.docInfoXml = xmlInfo
+6 -6
View File
@@ -697,7 +697,7 @@ class OPF(object): # {{{
def find_toc(self): def find_toc(self):
self.toc = None self.toc = None
try: try:
spine = self.XPath('descendant::*[re:match(name(), "spine", ' spine = OPF.XPath('descendant::*[re:match(name(), "spine", '
'"i")]')(self.root) '"i")]')(self.root)
toc = None toc = None
if spine: if spine:
@@ -801,7 +801,7 @@ class OPF(object): # {{{
def replace_spine_items_by_idref(self, idref, new_idrefs): def replace_spine_items_by_idref(self, idref, new_idrefs):
items = list(map(self.create_spine_item, new_idrefs)) items = list(map(self.create_spine_item, new_idrefs))
spine = self.XPath('/opf:package/*[re:match(name(), "spine", ' spine = OPF.XPath('/opf:package/*[re:match(name(), "spine", '
'"i")]')(self.root)[0] '"i")]')(self.root)[0]
old = [i for i in self.iterspine() if i.get('idref', None) == idref] old = [i for i in self.iterspine() if i.get('idref', None) == idref]
for x in old: for x in old:
@@ -1005,7 +1005,7 @@ class OPF(object): # {{{
def get_identifiers(self): def get_identifiers(self):
identifiers = {} identifiers = {}
for x in self.XPath( for x in OPF.XPath(
'descendant::*[local-name() = "identifier" and text()]')( 'descendant::*[local-name() = "identifier" and text()]')(
self.metadata): self.metadata):
found_scheme = False found_scheme = False
@@ -1039,7 +1039,7 @@ class OPF(object): # {{{
uuid_id = self.root.attrib[attr] uuid_id = self.root.attrib[attr]
break break
for x in self.XPath( for x in OPF.XPath(
'descendant::*[local-name() = "identifier"]')( 'descendant::*[local-name() = "identifier"]')(
self.metadata): self.metadata):
xid = x.get('id', None) xid = x.get('id', None)
@@ -1175,7 +1175,7 @@ class OPF(object): # {{{
@property @property
def page_progression_direction(self): def page_progression_direction(self):
spine = self.XPath('descendant::*[re:match(name(), "spine", ' spine = OPF.XPath('descendant::*[re:match(name(), "spine", '
'"i")][1]')(self.root) '"i")][1]')(self.root)
if spine: if spine:
for k, v in spine[0].attrib.items(): for k, v in spine[0].attrib.items():
@@ -1185,7 +1185,7 @@ class OPF(object): # {{{
@property @property
def primary_writing_mode(self): def primary_writing_mode(self):
for m in self.XPath('//*[local-name()="meta" and @name="primary-' for m in OPF.XPath('//*[local-name()="meta" and @name="primary-'
'writing-mode" and @content]')(self.root): 'writing-mode" and @content]')(self.root):
return m.get('content') return m.get('content')
+4 -6
View File
@@ -1,7 +1,7 @@
import collections import collections
import functools import functools
import operator import operator
import pkg_resources from importlib.resources import files
import re import re
import urllib.parse import urllib.parse
@@ -719,9 +719,8 @@ def commit_nav_toc(container, toc, lang=None, landmarks=None,
if previous_nav is not None: if previous_nav is not None:
root = previous_nav[1] root = previous_nav[1]
else: else:
with open(pkg_resources. with open(str(files('ebook_converter')
resource_filename('ebook_converter', .joinpath('data/new_nav.html'))) as fobj:
'data/new_nav.html')) as fobj:
root = container.parse_xhtml(fobj.read()) root = container.parse_xhtml(fobj.read())
container.replace(tocname, root) container.replace(tocname, root)
else: else:
@@ -874,8 +873,7 @@ def toc_to_html(toc, container, toc_name, title, lang=None):
E = ElementMaker(namespace=const.XHTML_NS, nsmap={None: const.XHTML_NS}) E = ElementMaker(namespace=const.XHTML_NS, nsmap={None: const.XHTML_NS})
# TODO(gryf): revisit lack of css. # TODO(gryf): revisit lack of css.
css_f = pkg_resources.resource_filename('ebook_converter', css_f = str(files('ebook_converter').joinpath('data/inline_toc_styles.css'))
'data/inline_toc_styles.css')
html = E.html(E.head(E.title(title), html = E.html(E.head(E.title(title),
E.style(css_f, type='text/css')), E.style(css_f, type='text/css')),
E.body(E.h2(title), E.ul(), E.body(E.h2(title), E.ul(),
+2 -3
View File
@@ -2,7 +2,7 @@
CSS property propagation class. CSS property propagation class.
""" """
import os, re, logging, copy, unicodedata, numbers import os, re, logging, copy, unicodedata, numbers
import pkg_resources from importlib.resources import files
from operator import itemgetter from operator import itemgetter
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
from xml.dom import SyntaxErr as CSSSyntaxError from xml.dom import SyntaxErr as CSSSyntaxError
@@ -28,8 +28,7 @@ _html_css_stylesheet = None
def html_css_stylesheet(): def html_css_stylesheet():
global _html_css_stylesheet global _html_css_stylesheet
if _html_css_stylesheet is None: if _html_css_stylesheet is None:
with open(pkg_resources.resource_filename('ebook_converter', with open(str(files('ebook_converter').joinpath('data/html.css')), 'rb') as f:
'data/html.css'), 'rb') as f:
html_css = f.read().decode('utf-8') html_css = f.read().decode('utf-8')
_html_css_stylesheet = parseString(html_css, validate=False) _html_css_stylesheet = parseString(html_css, validate=False)
return _html_css_stylesheet return _html_css_stylesheet
+2 -3
View File
@@ -1,6 +1,6 @@
from collections import namedtuple from collections import namedtuple
import json import json
import pkg_resources from importlib.resources import files
from ebook_converter.utils.localization import canonicalize_lang from ebook_converter.utils.localization import canonicalize_lang
@@ -16,8 +16,7 @@ ccodes, ccodemap, country_names = None, None, None
def get_codes(): def get_codes():
global ccodes, ccodemap, country_names global ccodes, ccodemap, country_names
if ccodes is None: if ccodes is None:
src = pkg_resources.resource_filename('ebook_converter', src = str(files('ebook_converter').joinpath('data/iso_3166-1.json'))
'data/iso_3166-1.json')
with open(src, 'rb') as f: with open(src, 'rb') as f:
db = json.load(f) db = json.load(f)
codes = set() codes = set()
+2 -3
View File
@@ -10,7 +10,7 @@ import pickle
import re import re
import traceback import traceback
import pkg_resources from importlib.resources import files as _resource_files
from ebook_converter.constants_old import config_dir from ebook_converter.constants_old import config_dir
from ebook_converter.constants_old import filesystem_encoding from ebook_converter.constants_old import filesystem_encoding
@@ -591,8 +591,7 @@ def exec_tweaks(path):
def default_tweaks_raw(): def default_tweaks_raw():
return pkg_resources.resource_filename('ebook_converter', return str(_resource_files('ebook_converter').joinpath('data/default_tweaks.py'))
'data/default_tweaks.py')
def read_tweaks(): def read_tweaks():
+2 -3
View File
@@ -1,5 +1,5 @@
import json import json
import pkg_resources from importlib.resources import files
def get_lang(): def get_lang():
@@ -39,8 +39,7 @@ def _load_iso639():
# excerpt form Calibre transform code which is executed during Calibre # excerpt form Calibre transform code which is executed during Calibre
# build). # build).
if _iso639 is None: if _iso639 is None:
src = pkg_resources.resource_filename('ebook_converter', src = str(files('ebook_converter').joinpath('data/iso_639-3.json'))
'data/iso_639-3.json')
with open(src, 'rb') as f: with open(src, 'rb') as f:
root = json.load(f) root = json.load(f)