From 17c304066a1aebd248ca1e5e9afa135ffd0b1803 Mon Sep 17 00:00:00 2001 From: wave1martian Date: Sun, 8 Mar 2026 20:24:24 +0100 Subject: [PATCH 1/4] Replace pkg_resources with importlib.resources for Python 3.14 compatibility --- ebook_converter/__init__.py | 5 ++--- ebook_converter/ebooks/conversion/cli.py | 5 ++--- .../ebooks/conversion/plugins/fb2_input.py | 6 +++--- .../ebooks/conversion/plugins/html_output.py | 17 +++++++---------- .../ebooks/conversion/plugins/lrf_input.py | 7 +++---- .../ebooks/conversion/plugins/rtf_input.py | 6 +++--- ebook_converter/ebooks/oeb/polish/toc.py | 10 ++++------ ebook_converter/ebooks/oeb/stylizer.py | 5 ++--- ebook_converter/spell/__init__.py | 5 ++--- ebook_converter/utils/config_base.py | 5 ++--- ebook_converter/utils/localization.py | 5 ++--- 11 files changed, 32 insertions(+), 44 deletions(-) diff --git a/ebook_converter/__init__.py b/ebook_converter/__init__.py index 2c712a9..fd4a062 100644 --- a/ebook_converter/__init__.py +++ b/ebook_converter/__init__.py @@ -1,6 +1,5 @@ import mimetypes -import pkg_resources +from importlib.resources import files -mimetypes.init([pkg_resources. - resource_filename('ebook_converter', 'data/mime.types')]) +mimetypes.init([str(files('ebook_converter').joinpath('data/mime.types'))]) diff --git a/ebook_converter/ebooks/conversion/cli.py b/ebook_converter/ebooks/conversion/cli.py index cc7276a..cbc9842 100644 --- a/ebook_converter/ebooks/conversion/cli.py +++ b/ebook_converter/ebooks/conversion/cli.py @@ -7,7 +7,7 @@ import mimetypes import numbers import optparse import os -import pkg_resources +from importlib.resources import files import re import sys @@ -351,8 +351,7 @@ def read_sr_patterns(path, log=None): def main(args=sys.argv): log = logging.default_log - mimetypes.init([pkg_resources.resource_filename('ebook_converter', - 'data/mime.types')]) + mimetypes.init([str(files('ebook_converter').joinpath('data/mime.types'))]) parser, plumber = create_option_parser(args, log) opts, leftover_args = parser.parse_args(args) if len(leftover_args) > 3: diff --git a/ebook_converter/ebooks/conversion/plugins/fb2_input.py b/ebook_converter/ebooks/conversion/plugins/fb2_input.py index 93ab23a..0487ef8 100644 --- a/ebook_converter/ebooks/conversion/plugins/fb2_input.py +++ b/ebook_converter/ebooks/conversion/plugins/fb2_input.py @@ -3,7 +3,7 @@ Convert .fb2 files to .lrf """ import mimetypes import os -import pkg_resources +from importlib.resources import files import re from lxml import etree @@ -85,8 +85,8 @@ class FB2Input(InputFormatPlugin): css = re.sub(r'name\s*=\s*', 'class=', css) self.extract_embedded_content(doc) log.debug('Converting XML to HTML...') - with open(pkg_resources.resource_filename('ebook_converter', - 'data/fb2.xsl')) as f: + with open(str(files('ebook_converter') + .joinpath('data/fb2.xsl'))) as f: ss = f.read() ss = ss.replace("__FB_NS__", fb_ns) if options.no_inline_fb2_toc: diff --git a/ebook_converter/ebooks/conversion/plugins/html_output.py b/ebook_converter/ebooks/conversion/plugins/html_output.py index 76311ef..9569f59 100644 --- a/ebook_converter/ebooks/conversion/plugins/html_output.py +++ b/ebook_converter/ebooks/conversion/plugins/html_output.py @@ -1,5 +1,5 @@ import os -import pkg_resources +from importlib.resources import files import re import shutil @@ -92,9 +92,8 @@ class HTMLOutput(OutputFormatPlugin): with open(opts.template_html_index, 'rb') as f: template_html_index_data = f.read() else: - with open(pkg_resources. - resource_filename('ebook_converter', - 'data/html_export_default_index.tmpl') + with open(str(files('ebook_converter') + .joinpath('data/html_export_default_index.tmpl')) ) as fobj: template_html_index_data = fobj.read().decode() @@ -102,9 +101,8 @@ class HTMLOutput(OutputFormatPlugin): with open(opts.template_html, 'rb') as f: template_html_data = f.read() else: - with open(pkg_resources. - resource_filename('ebook_converter', - 'data/html_export_default.tmpl') + with open(str(files('ebook_converter') + .joinpath('data/html_export_default.tmpl')) ) as fobj: template_html_data = fobj.read().decode() @@ -112,9 +110,8 @@ class HTMLOutput(OutputFormatPlugin): with open(opts.template_css, 'rb') as f: template_css_data = f.read() else: - with open(pkg_resources. - resource_filename('ebook_converter', - 'data/html_export_default.css') + with open(str(files('ebook_converter') + .joinpath('data/html_export_default.css')) ) as fobj: template_css_data = fobj.read().decode() diff --git a/ebook_converter/ebooks/conversion/plugins/lrf_input.py b/ebook_converter/ebooks/conversion/plugins/lrf_input.py index b5154b0..36e9582 100644 --- a/ebook_converter/ebooks/conversion/plugins/lrf_input.py +++ b/ebook_converter/ebooks/conversion/plugins/lrf_input.py @@ -1,6 +1,6 @@ import os import sys -import pkg_resources +from importlib.resources import files from lxml import etree @@ -52,9 +52,8 @@ class LRFInput(InputFormatPlugin): self.log.info('Converting XML to HTML...') - with open(pkg_resources. - resource_filename('ebook_converter', - 'data/lrf.xsl')) as fobj: + with open(str(files('ebook_converter') + .joinpath('data/lrf.xsl'))) as fobj: # TODO(gryf): change this nonsense to etree.parse() instead. styledoc = etree.fromstring(fobj.read()) media_type = MediaType() diff --git a/ebook_converter/ebooks/conversion/plugins/rtf_input.py b/ebook_converter/ebooks/conversion/plugins/rtf_input.py index f6e0753..2bf45a5 100644 --- a/ebook_converter/ebooks/conversion/plugins/rtf_input.py +++ b/ebook_converter/ebooks/conversion/plugins/rtf_input.py @@ -1,6 +1,6 @@ import glob import os -import pkg_resources +from importlib.resources import files import re import textwrap @@ -288,8 +288,8 @@ class RTFInput(InputFormatPlugin): self.log.info('Converting XML to HTML...') inline_class = InlineClass(self.log) - with open(pkg_resources.resource_filename('ebook_converter', - 'data/rtf.xsl')) as fobj: + with open(str(files('ebook_converter') + .joinpath('data/rtf.xsl'))) as fobj: styledoc = etree.fromstring(fobj.read()) extensions = {('calibre', 'inline-class'): inline_class} transform = etree.XSLT(styledoc, extensions=extensions) diff --git a/ebook_converter/ebooks/oeb/polish/toc.py b/ebook_converter/ebooks/oeb/polish/toc.py index 6bc05fe..f4c54ea 100644 --- a/ebook_converter/ebooks/oeb/polish/toc.py +++ b/ebook_converter/ebooks/oeb/polish/toc.py @@ -1,7 +1,7 @@ import collections import functools import operator -import pkg_resources +from importlib.resources import files import re import urllib.parse @@ -719,9 +719,8 @@ def commit_nav_toc(container, toc, lang=None, landmarks=None, if previous_nav is not None: root = previous_nav[1] else: - with open(pkg_resources. - resource_filename('ebook_converter', - 'data/new_nav.html')) as fobj: + with open(str(files('ebook_converter') + .joinpath('data/new_nav.html'))) as fobj: root = container.parse_xhtml(fobj.read()) container.replace(tocname, root) 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}) # TODO(gryf): revisit lack of css. - css_f = pkg_resources.resource_filename('ebook_converter', - 'data/inline_toc_styles.css') + css_f = str(files('ebook_converter').joinpath('data/inline_toc_styles.css')) html = E.html(E.head(E.title(title), E.style(css_f, type='text/css')), E.body(E.h2(title), E.ul(), diff --git a/ebook_converter/ebooks/oeb/stylizer.py b/ebook_converter/ebooks/oeb/stylizer.py index 0b9cebd..63b8006 100644 --- a/ebook_converter/ebooks/oeb/stylizer.py +++ b/ebook_converter/ebooks/oeb/stylizer.py @@ -2,7 +2,7 @@ CSS property propagation class. """ import os, re, logging, copy, unicodedata, numbers -import pkg_resources +from importlib.resources import files from operator import itemgetter from weakref import WeakKeyDictionary from xml.dom import SyntaxErr as CSSSyntaxError @@ -28,8 +28,7 @@ _html_css_stylesheet = None def html_css_stylesheet(): global _html_css_stylesheet if _html_css_stylesheet is None: - with open(pkg_resources.resource_filename('ebook_converter', - 'data/html.css'), 'rb') as f: + with open(str(files('ebook_converter').joinpath('data/html.css')), 'rb') as f: html_css = f.read().decode('utf-8') _html_css_stylesheet = parseString(html_css, validate=False) return _html_css_stylesheet diff --git a/ebook_converter/spell/__init__.py b/ebook_converter/spell/__init__.py index dbcdfef..b123c36 100644 --- a/ebook_converter/spell/__init__.py +++ b/ebook_converter/spell/__init__.py @@ -1,6 +1,6 @@ from collections import namedtuple import json -import pkg_resources +from importlib.resources import files from ebook_converter.utils.localization import canonicalize_lang @@ -16,8 +16,7 @@ ccodes, ccodemap, country_names = None, None, None def get_codes(): global ccodes, ccodemap, country_names if ccodes is None: - src = pkg_resources.resource_filename('ebook_converter', - 'data/iso_3166-1.json') + src = str(files('ebook_converter').joinpath('data/iso_3166-1.json')) with open(src, 'rb') as f: db = json.load(f) codes = set() diff --git a/ebook_converter/utils/config_base.py b/ebook_converter/utils/config_base.py index e8f3ab5..15fb13e 100644 --- a/ebook_converter/utils/config_base.py +++ b/ebook_converter/utils/config_base.py @@ -10,7 +10,7 @@ import pickle import re 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 filesystem_encoding @@ -591,8 +591,7 @@ def exec_tweaks(path): def default_tweaks_raw(): - return pkg_resources.resource_filename('ebook_converter', - 'data/default_tweaks.py') + return str(_resource_files('ebook_converter').joinpath('data/default_tweaks.py')) def read_tweaks(): diff --git a/ebook_converter/utils/localization.py b/ebook_converter/utils/localization.py index d5ac8b2..02c9dea 100644 --- a/ebook_converter/utils/localization.py +++ b/ebook_converter/utils/localization.py @@ -1,5 +1,5 @@ import json -import pkg_resources +from importlib.resources import files def get_lang(): @@ -39,8 +39,7 @@ def _load_iso639(): # excerpt form Calibre transform code which is executed during Calibre # build). if _iso639 is None: - src = pkg_resources.resource_filename('ebook_converter', - 'data/iso_639-3.json') + src = str(files('ebook_converter').joinpath('data/iso_639-3.json')) with open(src, 'rb') as f: root = json.load(f) From f72f6ff3b9039dbb7ff35f0ea8c0ee0253c1b958 Mon Sep 17 00:00:00 2001 From: wave1martian Date: Sun, 8 Mar 2026 21:13:13 +0100 Subject: [PATCH 2/4] fix self.XPATH() calls to OPF.XPath() instead since 3.14 made functools.partial a descriptor and thus injected self as the first argument --- ebook_converter/ebooks/metadata/opf2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ebook_converter/ebooks/metadata/opf2.py b/ebook_converter/ebooks/metadata/opf2.py index 2ff150d..68b8182 100644 --- a/ebook_converter/ebooks/metadata/opf2.py +++ b/ebook_converter/ebooks/metadata/opf2.py @@ -697,7 +697,7 @@ class OPF(object): # {{{ def find_toc(self): self.toc = None try: - spine = self.XPath('descendant::*[re:match(name(), "spine", ' + spine = OPF.XPath('descendant::*[re:match(name(), "spine", ' '"i")]')(self.root) toc = None if spine: @@ -801,7 +801,7 @@ class OPF(object): # {{{ def replace_spine_items_by_idref(self, idref, 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] old = [i for i in self.iterspine() if i.get('idref', None) == idref] for x in old: @@ -1005,7 +1005,7 @@ class OPF(object): # {{{ def get_identifiers(self): identifiers = {} - for x in self.XPath( + for x in OPF.XPath( 'descendant::*[local-name() = "identifier" and text()]')( self.metadata): found_scheme = False @@ -1039,7 +1039,7 @@ class OPF(object): # {{{ uuid_id = self.root.attrib[attr] break - for x in self.XPath( + for x in OPF.XPath( 'descendant::*[local-name() = "identifier"]')( self.metadata): xid = x.get('id', None) @@ -1175,7 +1175,7 @@ class OPF(object): # {{{ @property 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) if spine: for k, v in spine[0].attrib.items(): @@ -1185,7 +1185,7 @@ class OPF(object): # {{{ @property 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): return m.get('content') From 7afb71db366d2eaefdcfb4dbb47a0c0ead2072e1 Mon Sep 17 00:00:00 2001 From: wave1martian Date: Mon, 9 Mar 2026 09:01:47 +0100 Subject: [PATCH 3/4] updated install instruction to include lxml to avoid using non matching system version --- README.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d7e2675..d30f58a 100644 --- a/README.rst +++ b/README.rst @@ -123,7 +123,12 @@ managers), i.e: $ . venv/bin/activate (venv) $ git clone https://github.com/gryf/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: From 89340f4908ba347c5894020c2fe06fc56c66328d Mon Sep 17 00:00:00 2001 From: wave1martian Date: Mon, 9 Mar 2026 09:46:49 +0100 Subject: [PATCH 4/4] Fix regex in LRF output that stripped CThumbnail metadata. .*? matched until >\n and cut off all children on the same line --- ebook_converter/ebooks/lrf/pylrs/pylrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebook_converter/ebooks/lrf/pylrs/pylrs.py b/ebook_converter/ebooks/lrf/pylrs/pylrs.py index 745ef6a..9dd4bfe 100644 --- a/ebook_converter/ebooks/lrf/pylrs/pylrs.py +++ b/ebook_converter/ebooks/lrf/pylrs/pylrs.py @@ -670,7 +670,7 @@ class Info(Delegator): f = io.BytesIO() tree.write(f, encoding='utf-8', xml_declaration=True) xmlInfo = f.getvalue().decode('utf-8') - xmlInfo = re.sub(r"\n", "", xmlInfo) + xmlInfo = re.sub(r"]*/>\s*", "", xmlInfo) xmlInfo = xmlInfo.replace("SumPage>", "Page>") lrfWriter.docInfoXml = xmlInfo