epub2text 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {epub2text-0.2.3 → epub2text-0.2.4}/.ruff.toml +1 -1
  2. {epub2text-0.2.3 → epub2text-0.2.4}/PKG-INFO +2 -2
  3. {epub2text-0.2.3 → epub2text-0.2.4}/docs/make.py +1 -1
  4. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/__init__.py +2 -2
  5. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/_version.py +3 -3
  6. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/bookmarks.py +9 -5
  7. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/cleaner.py +2 -2
  8. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/cli.py +19 -20
  9. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/models.py +11 -12
  10. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/parser.py +28 -28
  11. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/reader.py +6 -7
  12. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/PKG-INFO +2 -2
  13. {epub2text-0.2.3 → epub2text-0.2.4}/examples/demo.py +2 -3
  14. {epub2text-0.2.3 → epub2text-0.2.4}/pyproject.toml +15 -2
  15. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_epub_download.py +4 -2
  16. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_epub_generation.py +1 -1
  17. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_page_extraction_improvements.py +1 -1
  18. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_title_deduplication.py +1 -1
  19. {epub2text-0.2.3 → epub2text-0.2.4}/.codecrate.toml +0 -0
  20. {epub2text-0.2.3 → epub2text-0.2.4}/.coveragerc +0 -0
  21. {epub2text-0.2.3 → epub2text-0.2.4}/.github/pytest.ini +0 -0
  22. {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/codecov.yml +0 -0
  23. {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/pre-commit.yml +0 -0
  24. {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/python-publish.yml +0 -0
  25. {epub2text-0.2.3 → epub2text-0.2.4}/.github/workflows/tests.yml +0 -0
  26. {epub2text-0.2.3 → epub2text-0.2.4}/.gitignore +0 -0
  27. {epub2text-0.2.3 → epub2text-0.2.4}/.pre-commit-config.yaml +0 -0
  28. {epub2text-0.2.3 → epub2text-0.2.4}/.prettierrc.yml +0 -0
  29. {epub2text-0.2.3 → epub2text-0.2.4}/.readthedocs.yaml +0 -0
  30. {epub2text-0.2.3 → epub2text-0.2.4}/LICENSE +0 -0
  31. {epub2text-0.2.3 → epub2text-0.2.4}/README.md +0 -0
  32. {epub2text-0.2.3 → epub2text-0.2.4}/docs/api.rst +0 -0
  33. {epub2text-0.2.3 → epub2text-0.2.4}/docs/changelog.rst +0 -0
  34. {epub2text-0.2.3 → epub2text-0.2.4}/docs/conf.py +0 -0
  35. {epub2text-0.2.3 → epub2text-0.2.4}/docs/index.rst +0 -0
  36. {epub2text-0.2.3 → epub2text-0.2.4}/docs/installation.rst +0 -0
  37. {epub2text-0.2.3 → epub2text-0.2.4}/docs/make.bat +0 -0
  38. {epub2text-0.2.3 → epub2text-0.2.4}/docs/requirements.txt +0 -0
  39. {epub2text-0.2.3 → epub2text-0.2.4}/docs/usage.rst +0 -0
  40. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/__main__.py +0 -0
  41. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/formatters.py +0 -0
  42. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text/py.typed +0 -0
  43. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/SOURCES.txt +0 -0
  44. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/dependency_links.txt +0 -0
  45. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/entry_points.txt +0 -0
  46. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/requires.txt +0 -0
  47. {epub2text-0.2.3 → epub2text-0.2.4}/epub2text.egg-info/top_level.txt +0 -0
  48. {epub2text-0.2.3 → epub2text-0.2.4}/examples/__init__.py +0 -0
  49. {epub2text-0.2.3 → epub2text-0.2.4}/examples/__main__.py +0 -0
  50. {epub2text-0.2.3 → epub2text-0.2.4}/requirements-test.txt +0 -0
  51. {epub2text-0.2.3 → epub2text-0.2.4}/setup.cfg +0 -0
  52. {epub2text-0.2.3 → epub2text-0.2.4}/setup.py +0 -0
  53. {epub2text-0.2.3 → epub2text-0.2.4}/tests/__init__.py +0 -0
  54. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_cleaner.py +0 -0
  55. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_cli_ranges.py +0 -0
  56. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_parser_page_list.py +0 -0
  57. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_parser_spine_slice.py +0 -0
  58. {epub2text-0.2.3 → epub2text-0.2.4}/tests/test_reader_bookmarks.py +0 -0
@@ -1,5 +1,5 @@
1
1
 
2
- target-version = "py39"
2
+ target-version = "py310"
3
3
  fix = true
4
4
 
5
5
  [lint]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epub2text
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A simple tool to extract text from EPUB files.
5
5
  Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
6
6
  License: MIT License
@@ -37,7 +37,7 @@ Classifier: Programming Language :: Python :: 3.12
37
37
  Classifier: Programming Language :: Python :: 3.13
38
38
  Classifier: Programming Language :: Python :: 3.14
39
39
  Classifier: Topic :: Software Development :: Libraries
40
- Requires-Python: >=3.9
40
+ Requires-Python: >=3.10
41
41
  Description-Content-Type: text/markdown
42
42
  License-File: LICENSE
43
43
  Requires-Dist: click>=8.0.0
@@ -20,7 +20,7 @@ import subprocess
20
20
  import sys
21
21
 
22
22
 
23
- def main():
23
+ def main() -> int:
24
24
  """Run the script."""
25
25
  sphinx_build = "sphinx-build"
26
26
  build_dir = os.path.join("docs", "build")
@@ -136,7 +136,7 @@ def _download_epub(
136
136
  except Exception as exc:
137
137
  if tmp_path:
138
138
  Path(tmp_path).unlink(missing_ok=True)
139
- if isinstance(exc, (urllib.error.URLError, socket.timeout)):
139
+ if isinstance(exc, urllib.error.URLError | socket.timeout):
140
140
  if isinstance(exc, socket.timeout) or isinstance(
141
141
  getattr(exc, "reason", None), socket.timeout
142
142
  ):
@@ -152,7 +152,7 @@ def epub2txt(
152
152
  max_bytes: int = DEFAULT_MAX_DOWNLOAD_BYTES,
153
153
  user_agent: str | None = None,
154
154
  allowed_schemes: tuple[str, ...] = ("https", "http"),
155
- ) -> Union[str, list[str]]:
155
+ ) -> str | list[str]:
156
156
  """
157
157
  Extract text from EPUB file (compatibility function for old epub2txt API).
158
158
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.2.3'
32
- __version_tuple__ = version_tuple = (0, 2, 3)
31
+ __version__ = version = '0.2.4'
32
+ __version_tuple__ = version_tuple = (0, 2, 4)
33
33
 
34
- __commit_id__ = commit_id = 'g82e1d316b'
34
+ __commit_id__ = commit_id = 'ge900e4fef'
@@ -7,7 +7,7 @@ import tempfile
7
7
  from dataclasses import asdict, dataclass
8
8
  from datetime import datetime, timezone
9
9
  from pathlib import Path
10
- from typing import Any, Optional
10
+ from typing import Any, cast
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -54,7 +54,7 @@ class Bookmark:
54
54
  class BookmarkManager:
55
55
  """Manages bookmarks for EPUB files."""
56
56
 
57
- def __init__(self, bookmark_file: Optional[Path] = None) -> None:
57
+ def __init__(self, bookmark_file: Path | None = None) -> None:
58
58
  """
59
59
  Initialize bookmark manager.
60
60
 
@@ -119,11 +119,15 @@ class BookmarkManager:
119
119
  """Return the path for the bookmark backup file."""
120
120
  return self.bookmark_file.with_suffix(self.bookmark_file.suffix + ".bak")
121
121
 
122
- def _read_bookmarks_file(self, path: Path) -> Optional[dict[str, Any]]:
122
+ def _read_bookmarks_file(self, path: Path) -> dict[str, Any] | None:
123
123
  """Read bookmarks JSON from disk."""
124
124
  try:
125
125
  with open(path, encoding="utf-8") as f:
126
- return json.load(f)
126
+ data = json.load(f)
127
+ if isinstance(data, dict):
128
+ return cast(dict[str, Any], data)
129
+ logger.warning("Unexpected bookmark data structure in %s", path)
130
+ return None
127
131
  except (json.JSONDecodeError, OSError) as exc:
128
132
  logger.warning("Failed to load bookmarks from %s: %s", path, exc)
129
133
  return None
@@ -144,7 +148,7 @@ class BookmarkManager:
144
148
  self._bookmarks[key] = asdict(bookmark)
145
149
  self._save()
146
150
 
147
- def load(self, epub_path: str) -> Optional[Bookmark]:
151
+ def load(self, epub_path: str) -> Bookmark | None:
148
152
  """
149
153
  Load bookmark for a specific EPUB file.
150
154
 
@@ -96,7 +96,7 @@ class TextCleaner:
96
96
  markers = {}
97
97
  marker_count = [0]
98
98
 
99
- def save_marker(match):
99
+ def save_marker(match: re.Match[str]) -> str:
100
100
  placeholder = f"___MARKER_{marker_count[0]}___"
101
101
  markers[placeholder] = match.group(0)
102
102
  marker_count[0] += 1
@@ -196,7 +196,7 @@ class TextCleaner:
196
196
  abbrev_map = {}
197
197
  abbrev_counter = [0]
198
198
 
199
- def protect_abbrev(match):
199
+ def protect_abbrev(match: re.Match[str]) -> str:
200
200
  placeholder = f"__ABBREV_{abbrev_counter[0]}__"
201
201
  abbrev_map[placeholder] = match.group(0)
202
202
  abbrev_counter[0] += 1
@@ -7,7 +7,6 @@ import sys
7
7
  from collections.abc import Iterator
8
8
  from contextlib import contextmanager
9
9
  from pathlib import Path
10
- from typing import Optional
11
10
 
12
11
  import click
13
12
  from rich.console import Console
@@ -46,7 +45,7 @@ def spinner(task_description: str) -> Iterator[None]:
46
45
  yield
47
46
 
48
47
 
49
- def parse_range_tokens(range_str: str) -> list[tuple[str, Optional[str]]]:
48
+ def parse_range_tokens(range_str: str) -> list[tuple[str, str | None]]:
50
49
  """
51
50
  Parse a range string into ordered tokens.
52
51
 
@@ -59,7 +58,7 @@ def parse_range_tokens(range_str: str) -> list[tuple[str, Optional[str]]]:
59
58
  if not range_str:
60
59
  raise ValueError("Range string cannot be empty")
61
60
 
62
- tokens: list[tuple[str, Optional[str]]] = []
61
+ tokens: list[tuple[str, str | None]] = []
63
62
  parts = range_str.split(",")
64
63
 
65
64
  for part in parts:
@@ -205,7 +204,7 @@ def parse_page_range(range_str: str, pages: list[Page]) -> list[str]:
205
204
  result: list[str] = []
206
205
  seen: set[str] = set()
207
206
 
208
- def parse_int(value: str) -> Optional[int]:
207
+ def parse_int(value: str) -> int | None:
209
208
  try:
210
209
  return int(value)
211
210
  except ValueError:
@@ -590,8 +589,8 @@ def list_pages(filepath: Path, page_size: int, use_words: bool) -> None:
590
589
  )
591
590
  def extract_pages_cmd(
592
591
  filepath: Path,
593
- output: Optional[Path],
594
- pages: Optional[str],
592
+ output: Path | None,
593
+ pages: str | None,
595
594
  page_size: int,
596
595
  use_words: bool,
597
596
  raw: bool,
@@ -786,13 +785,13 @@ def extract_pages_cmd(
786
785
  )
787
786
  def extract(
788
787
  filepath: Path,
789
- output: Optional[Path],
790
- chapters: Optional[str],
788
+ output: Path | None,
789
+ chapters: str | None,
791
790
  interactive: bool,
792
791
  paragraphs: bool,
793
792
  sentences: bool,
794
793
  comma: bool,
795
- max_length: Optional[int],
794
+ max_length: int | None,
796
795
  empty_lines: bool,
797
796
  separator: str,
798
797
  raw: bool,
@@ -801,7 +800,7 @@ def extract(
801
800
  no_markers: bool,
802
801
  language_model: str,
803
802
  offset: int,
804
- limit: Optional[int],
803
+ limit: int | None,
805
804
  line_numbers: bool,
806
805
  ) -> None:
807
806
  """
@@ -1012,8 +1011,8 @@ def extract(
1012
1011
  )
1013
1012
  def extract_gutenberg(
1014
1013
  filepath: Path,
1015
- output: Optional[Path],
1016
- chapters: Optional[str],
1014
+ output: Path | None,
1015
+ chapters: str | None,
1017
1016
  interactive: bool,
1018
1017
  ) -> None:
1019
1018
  """
@@ -1124,9 +1123,9 @@ def extract_gutenberg(
1124
1123
  # First, normalize the chapter text to help with splitting
1125
1124
  chapter_lines = chapter_text.split("\n")
1126
1125
 
1127
- current_title = None
1128
- current_content = []
1129
- chapters_list = []
1126
+ current_title: str | None = None
1127
+ current_content: list[str] = []
1128
+ chapters_list: list[tuple[str, str]] = []
1130
1129
 
1131
1130
  i = 0
1132
1131
  while i < len(chapter_lines):
@@ -1418,11 +1417,11 @@ def info(filepath: Path, format: str) -> None:
1418
1417
  )
1419
1418
  def read(
1420
1419
  filepath: Path,
1421
- chapter: Optional[int],
1422
- line: Optional[int],
1420
+ chapter: int | None,
1421
+ line: int | None,
1423
1422
  resume: bool,
1424
- bookmark_file: Optional[Path],
1425
- page_size: Optional[int],
1423
+ bookmark_file: Path | None,
1424
+ page_size: int | None,
1426
1425
  no_header: bool,
1427
1426
  no_footer: bool,
1428
1427
  sentences: bool,
@@ -1432,7 +1431,7 @@ def read(
1432
1431
  raw: bool,
1433
1432
  keep_footnotes: bool,
1434
1433
  keep_page_numbers: bool,
1435
- width: Optional[int],
1434
+ width: int | None,
1436
1435
  ) -> None:
1437
1436
  """
1438
1437
  Interactively read an EPUB file in the terminal.
@@ -2,7 +2,6 @@
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from enum import Enum
5
- from typing import Optional
6
5
 
7
6
 
8
7
  class PageSource(Enum):
@@ -25,8 +24,8 @@ class Page:
25
24
  text: str
26
25
  char_count: int
27
26
  source: PageSource
28
- chapter_id: Optional[str] = None # Which chapter this page belongs to
29
- chapter_title: Optional[str] = None # Title of the chapter
27
+ chapter_id: str | None = None # Which chapter this page belongs to
28
+ chapter_title: str | None = None # Title of the chapter
30
29
 
31
30
  def __str__(self) -> str:
32
31
  source_str = "print" if self.source == PageSource.EPUB_PAGE_LIST else "syn"
@@ -41,7 +40,7 @@ class Chapter:
41
40
  title: str
42
41
  text: str
43
42
  char_count: int
44
- parent_id: Optional[str] = None
43
+ parent_id: str | None = None
45
44
  level: int = 0
46
45
 
47
46
  def __str__(self) -> str:
@@ -52,18 +51,18 @@ class Chapter:
52
51
  class Metadata:
53
52
  """EPUB metadata."""
54
53
 
55
- title: Optional[str] = None
54
+ title: str | None = None
56
55
  authors: list[str] = field(default_factory=list)
57
- publisher: Optional[str] = None
58
- publication_year: Optional[str] = None
59
- description: Optional[str] = None
56
+ publisher: str | None = None
57
+ publication_year: str | None = None
58
+ description: str | None = None
60
59
  # Required EPUB3 metadata
61
- identifier: Optional[str] = None
62
- language: Optional[str] = None
60
+ identifier: str | None = None
61
+ language: str | None = None
63
62
  # Optional Dublin Core metadata
64
63
  contributors: list[str] = field(default_factory=list)
65
- rights: Optional[str] = None
66
- coverage: Optional[str] = None
64
+ rights: str | None = None
65
+ coverage: str | None = None
67
66
 
68
67
  def __str__(self) -> str:
69
68
  lines = []
@@ -7,7 +7,7 @@ import logging
7
7
  import re
8
8
  import urllib.parse
9
9
  from pathlib import Path
10
- from typing import Any, Optional
10
+ from typing import Any
11
11
 
12
12
  import ebooklib # type: ignore[import-untyped]
13
13
  from bs4 import BeautifulSoup, NavigableString # type: ignore[import-untyped]
@@ -54,11 +54,11 @@ class EPUBParser:
54
54
  self.content_texts: dict[str, str] = {}
55
55
  self.content_lengths: dict[str, int] = {}
56
56
  self.processed_nav_structure: list[dict[str, Any]] = []
57
- self._metadata: Optional[Metadata] = None
58
- self._page_list_nav: Optional[tuple[Any, str]] = None
57
+ self._metadata: Metadata | None = None
58
+ self._page_list_nav: tuple[Any, str] | None = None
59
59
  self._page_list_nav_checked = False
60
60
  self._nav_processed = False
61
- self._upper_title_cache: Optional[set[str]] = None
61
+ self._upper_title_cache: set[str] | None = None
62
62
 
63
63
  self._load_epub()
64
64
 
@@ -84,7 +84,7 @@ class EPUBParser:
84
84
  except Exception as e:
85
85
  raise ValueError(f"Failed to read EPUB file: {e}") from e
86
86
 
87
- def _get_single_metadata(self, field: str) -> Optional[str]:
87
+ def _get_single_metadata(self, field: str) -> str | None:
88
88
  """Extract a single metadata value from Dublin Core field."""
89
89
  try:
90
90
  items = self.book.get_metadata("DC", field)
@@ -163,7 +163,7 @@ class EPUBParser:
163
163
  self,
164
164
  nav_structure: list[dict[str, Any]],
165
165
  chapters: list[Chapter],
166
- parent_id: Optional[str] = None,
166
+ parent_id: str | None = None,
167
167
  level: int = 1,
168
168
  include_text: bool = True,
169
169
  ) -> None:
@@ -178,7 +178,7 @@ class EPUBParser:
178
178
  include_text: If True, include full text; if False, only metadata
179
179
  """
180
180
  for entry in nav_structure:
181
- src: Optional[str] = entry.get("src")
181
+ src: str | None = entry.get("src")
182
182
  title: str = entry.get("title", "Untitled")
183
183
  children: list[dict[str, Any]] = entry.get("children", [])
184
184
 
@@ -438,12 +438,12 @@ class EPUBParser:
438
438
 
439
439
  # Handle ordered lists
440
440
  for ol in slice_soup.find_all("ol"):
441
- start_attr = ol.get("start") # type: ignore[union-attr]
442
- start_num = int(start_attr) if start_attr else 1 # type: ignore[arg-type]
441
+ start_attr = ol.get("start")
442
+ start_num = int(start_attr) if start_attr else 1
443
443
  for li_idx, li in enumerate(ol.find_all("li", recursive=False)):
444
444
  number_text = f"{start_num + li_idx}) "
445
445
  if li.string:
446
- li.string.replace_with(number_text + str(li.string)) # type: ignore[union-attr]
446
+ li.string.replace_with(number_text + str(li.string))
447
447
  else:
448
448
  li.insert(0, NavigableString(number_text))
449
449
 
@@ -510,7 +510,7 @@ class EPUBParser:
510
510
  base_href: str,
511
511
  doc_order: dict[str, int],
512
512
  doc_order_decoded: dict[str, int],
513
- ) -> tuple[Optional[str], Optional[int]]:
513
+ ) -> tuple[str | None, int | None]:
514
514
  """Find the best matching doc_key for a given base_href."""
515
515
  import os
516
516
 
@@ -606,7 +606,7 @@ class EPUBParser:
606
606
  link = li_element.find("a", recursive=False)
607
607
  span_text = li_element.find("span", recursive=False)
608
608
  title = "Untitled Section"
609
- src: Optional[str] = None
609
+ src: str | None = None
610
610
  current_entry_node: dict[str, Any] = {"children": []}
611
611
 
612
612
  if link and "href" in link.attrs:
@@ -673,7 +673,7 @@ class EPUBParser:
673
673
  )
674
674
  tree_structure_list.append(current_entry_node)
675
675
 
676
- def _find_position_robust(self, doc_href: str, fragment_id: Optional[str]) -> int:
676
+ def _find_position_robust(self, doc_href: str, fragment_id: str | None) -> int:
677
677
  """
678
678
  Find the position of a fragment ID within a document.
679
679
 
@@ -835,7 +835,7 @@ class EPUBParser:
835
835
 
836
836
  def extract_chapters(
837
837
  self,
838
- chapter_ids: Optional[list[str]] = None,
838
+ chapter_ids: list[str] | None = None,
839
839
  deduplicate_chapter_titles: bool = True,
840
840
  skip_toc: bool = False,
841
841
  include_chapter_title: bool = False,
@@ -881,7 +881,7 @@ class EPUBParser:
881
881
  selected = filtered
882
882
 
883
883
  # Combine text with chapter titles
884
- parts = []
884
+ parts: list[str] = []
885
885
  for i, chapter in enumerate(selected):
886
886
  if chapter.text:
887
887
  # Remove duplicate title if requested
@@ -944,7 +944,7 @@ class EPUBParser:
944
944
  """
945
945
  return bool(self._find_page_list_nav())
946
946
 
947
- def _find_page_list_nav(self) -> Optional[tuple[Any, str]]:
947
+ def _find_page_list_nav(self) -> tuple[Any, str] | None:
948
948
  """
949
949
  Find the page-list navigation element in the EPUB.
950
950
 
@@ -1155,8 +1155,8 @@ class EPUBParser:
1155
1155
  page_num = 1
1156
1156
  current_sentences: list[str] = []
1157
1157
  current_size = 0 # Current size in chars or words
1158
- current_chapter_id: Optional[str] = None
1159
- current_chapter_title: Optional[str] = None
1158
+ current_chapter_id: str | None = None
1159
+ current_chapter_title: str | None = None
1160
1160
 
1161
1161
  def get_size(text: str) -> int:
1162
1162
  """Get size of text in chars or words."""
@@ -1362,15 +1362,15 @@ class EPUBParser:
1362
1362
  doc_href: str,
1363
1363
  position: int,
1364
1364
  chapter_map: list[tuple[str, int, str, str]],
1365
- ) -> tuple[Optional[str], Optional[str]]:
1365
+ ) -> tuple[str | None, str | None]:
1366
1366
  """
1367
1367
  Find which chapter a given position belongs to.
1368
1368
 
1369
1369
  Returns:
1370
1370
  Tuple of (chapter_id, chapter_title) or (None, None)
1371
1371
  """
1372
- result_id: Optional[str] = None
1373
- result_title: Optional[str] = None
1372
+ result_id: str | None = None
1373
+ result_title: str | None = None
1374
1374
 
1375
1375
  for ch_doc, ch_pos, ch_id, ch_title in chapter_map:
1376
1376
  if ch_doc == doc_href and ch_pos <= position:
@@ -1381,7 +1381,7 @@ class EPUBParser:
1381
1381
 
1382
1382
  return result_id, result_title
1383
1383
 
1384
- def _get_spine_index(self, doc_href: str, spine_docs: list[str]) -> Optional[int]:
1384
+ def _get_spine_index(self, doc_href: str, spine_docs: list[str]) -> int | None:
1385
1385
  """Get the index of a document in the spine, logging if missing."""
1386
1386
  try:
1387
1387
  return spine_docs.index(doc_href)
@@ -1393,8 +1393,8 @@ class EPUBParser:
1393
1393
  self,
1394
1394
  current_doc: str,
1395
1395
  current_pos: int,
1396
- next_doc: Optional[str],
1397
- next_pos: Optional[int],
1396
+ next_doc: str | None,
1397
+ next_pos: int | None,
1398
1398
  spine_docs: list[str],
1399
1399
  *,
1400
1400
  allow_wraparound: bool,
@@ -1439,7 +1439,7 @@ class EPUBParser:
1439
1439
  def _extract_text_between_positions(
1440
1440
  self,
1441
1441
  current: dict[str, Any],
1442
- next_entry: Optional[dict[str, Any]],
1442
+ next_entry: dict[str, Any] | None,
1443
1443
  spine_docs: list[str],
1444
1444
  ) -> str:
1445
1445
  """
@@ -1488,7 +1488,7 @@ class EPUBParser:
1488
1488
 
1489
1489
  def extract_pages(
1490
1490
  self,
1491
- page_numbers: Optional[list[str]] = None,
1491
+ page_numbers: list[str] | None = None,
1492
1492
  deduplicate_chapter_titles: bool = True,
1493
1493
  skip_toc: bool = False,
1494
1494
  ) -> str:
@@ -1518,8 +1518,8 @@ class EPUBParser:
1518
1518
  # Note: We don't filter out entire pages here anymore
1519
1519
  # Instead, we strip TOC content from pages below
1520
1520
 
1521
- parts = []
1522
- current_chapter: Optional[str] = None
1521
+ parts: list[str] = []
1522
+ current_chapter: str | None = None
1523
1523
 
1524
1524
  for page in selected:
1525
1525
  if not page.text:
@@ -3,7 +3,6 @@
3
3
  import signal
4
4
  import sys
5
5
  from dataclasses import dataclass
6
- from typing import Optional
7
6
 
8
7
  import click
9
8
  from rich.align import Align
@@ -71,13 +70,13 @@ class EpubReader:
71
70
  chapters: list[Chapter],
72
71
  title: str,
73
72
  epub_path: str,
74
- page_size: Optional[int] = None,
73
+ page_size: int | None = None,
75
74
  show_header: bool = True,
76
75
  show_footer: bool = True,
77
76
  start_line: int = 0,
78
- start_chapter: Optional[int] = None,
79
- bookmark_manager: Optional[BookmarkManager] = None,
80
- width: Optional[int] = None,
77
+ start_chapter: int | None = None,
78
+ bookmark_manager: BookmarkManager | None = None,
79
+ width: int | None = None,
81
80
  ) -> None:
82
81
  """
83
82
  Initialize the EPUB reader.
@@ -120,7 +119,7 @@ class EpubReader:
120
119
 
121
120
  # State
122
121
  self._show_help = False
123
- self._message: Optional[str] = None
122
+ self._message: str | None = None
124
123
  self._message_style: str = "green"
125
124
  self._running = False
126
125
 
@@ -402,7 +401,7 @@ class EpubReader:
402
401
 
403
402
  return True
404
403
 
405
- def _get_effective_width(self) -> Optional[int]:
404
+ def _get_effective_width(self) -> int | None:
406
405
  """Get effective content width, capped by terminal width."""
407
406
  if self._width is None:
408
407
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epub2text
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A simple tool to extract text from EPUB files.
5
5
  Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
6
6
  License: MIT License
@@ -37,7 +37,7 @@ Classifier: Programming Language :: Python :: 3.12
37
37
  Classifier: Programming Language :: Python :: 3.13
38
38
  Classifier: Programming Language :: Python :: 3.14
39
39
  Classifier: Topic :: Software Development :: Libraries
40
- Requires-Python: >=3.9
40
+ Requires-Python: >=3.10
41
41
  Description-Content-Type: text/markdown
42
42
  License-File: LICENSE
43
43
  Requires-Dist: click>=8.0.0
@@ -16,9 +16,8 @@ Run with: python -m examples.demo
16
16
  import tempfile
17
17
  from datetime import datetime
18
18
  from pathlib import Path
19
- from typing import Optional
20
19
 
21
- import pypub # type: ignore[import-untyped]
20
+ import pypub
22
21
 
23
22
  from epub2text import (
24
23
  Bookmark,
@@ -43,7 +42,7 @@ from epub2text.formatters import (
43
42
  # =============================================================================
44
43
 
45
44
 
46
- def create_sample_epub(output_path: Optional[Path] = None) -> Path:
45
+ def create_sample_epub(output_path: Path | None = None) -> Path:
47
46
  """
48
47
  Create a sample EPUB file for demonstration purposes.
49
48
 
@@ -10,7 +10,7 @@ include = ["epub2text*"]
10
10
  name = "epub2text"
11
11
  description = "A simple tool to extract text from EPUB files."
12
12
  readme = "README.md"
13
- requires-python = ">=3.9"
13
+ requires-python = ">=3.10"
14
14
  license = {file = "LICENSE"}
15
15
  authors = [
16
16
  {name = "Holger Nahrstaedt", email = "nahrstaedt@gmail.com"}
@@ -67,7 +67,7 @@ commands =
67
67
  """
68
68
 
69
69
  [tool.mypy]
70
- python_version = "3.9"
70
+ python_version = "3.10"
71
71
  warn_return_any = true
72
72
  warn_unused_configs = true
73
73
  disallow_untyped_defs = true
@@ -80,8 +80,21 @@ warn_no_return = true
80
80
  warn_unreachable = true
81
81
  strict_equality = true
82
82
  exclude = [
83
+ "^build/",
84
+ "^epub2text\\.egg-info/",
83
85
  ]
84
86
 
87
+ [[tool.mypy.overrides]]
88
+ module = [
89
+ "defusedxml",
90
+ "defusedxml.*",
91
+ "phrasplit",
92
+ "pypub",
93
+ "pypub.*",
94
+ "setuptools",
95
+ ]
96
+ ignore_missing_imports = true
97
+
85
98
  [tool.git-cliff.changelog]
86
99
  # A Tera template to be rendered as the changelog's header.
87
100
  # See https://keats.github.io/tera/docs/#introduction
@@ -11,7 +11,7 @@ from functools import partial
11
11
  from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
12
12
  from pathlib import Path
13
13
 
14
- import pypub # type: ignore[import-untyped]
14
+ import pypub
15
15
  import pytest
16
16
 
17
17
  from epub2text import epub2txt
@@ -42,7 +42,8 @@ def serve_directory(
42
42
  thread.start()
43
43
  try:
44
44
  host, port = server.server_address[:2]
45
- yield f"http://{host}:{port}"
45
+ host_value = host.decode() if isinstance(host, bytes) else host
46
+ yield f"http://{host_value}:{port}"
46
47
  finally:
47
48
  # Stop the loop, then close the listening socket to avoid FD/memory growth
48
49
  with contextlib.suppress(Exception):
@@ -68,6 +69,7 @@ def test_epub2txt_downloads_from_url(tmp_path: Path) -> None:
68
69
 
69
70
  with serve_directory(tmp_path) as base_url:
70
71
  text = epub2txt(f"{base_url}/{epub_path.name}")
72
+ assert isinstance(text, str)
71
73
 
72
74
  assert "downloadable epub" in text.lower()
73
75
 
@@ -9,7 +9,7 @@ from datetime import datetime
9
9
  from pathlib import Path
10
10
  from typing import cast
11
11
 
12
- import pypub # type: ignore[import-untyped]
12
+ import pypub
13
13
  import pytest
14
14
 
15
15
  from epub2text import EPUBParser, epub2txt
@@ -9,7 +9,7 @@ Tests the new features:
9
9
 
10
10
  from pathlib import Path
11
11
 
12
- import pypub # type: ignore[import-untyped]
12
+ import pypub
13
13
  import pytest
14
14
 
15
15
  from epub2text import EPUBParser
@@ -7,7 +7,7 @@ parameter in extract_chapters().
7
7
 
8
8
  from pathlib import Path
9
9
 
10
- import pypub # type: ignore[import-untyped]
10
+ import pypub
11
11
  import pytest
12
12
 
13
13
  from epub2text import EPUBParser
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes