reader 3.13__tar.gz → 3.15__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 (231) hide show
  1. {reader-3.13 → reader-3.15}/CHANGES.rst +74 -2
  2. {reader-3.13/src/reader.egg-info → reader-3.15}/PKG-INFO +5 -4
  3. {reader-3.13 → reader-3.15}/docs/api.rst +3 -1
  4. {reader-3.13 → reader-3.15}/docs/dev.rst +21 -9
  5. {reader-3.13 → reader-3.15}/docs/guide.rst +28 -20
  6. {reader-3.13 → reader-3.15}/docs/install.rst +14 -0
  7. {reader-3.13 → reader-3.15}/docs/internal.rst +42 -21
  8. {reader-3.13 → reader-3.15}/docs/plugins.rst +18 -1
  9. {reader-3.13 → reader-3.15}/docs/tutorial.rst +1 -1
  10. reader-3.15/examples/custom_headers.py +50 -0
  11. reader-3.15/examples/feed_slugs.py +95 -0
  12. {reader-3.13 → reader-3.15}/examples/parser_only.py +1 -1
  13. {reader-3.13 → reader-3.15}/pyproject.toml +6 -4
  14. {reader-3.13 → reader-3.15}/scripts/backup.sh +9 -6
  15. {reader-3.13 → reader-3.15}/src/reader/__init__.py +1 -1
  16. {reader-3.13 → reader-3.15}/src/reader/_app/__init__.py +19 -3
  17. {reader-3.13 → reader-3.15}/src/reader/_cli.py +13 -20
  18. {reader-3.13 → reader-3.15}/src/reader/_parser/__init__.py +187 -83
  19. {reader-3.13 → reader-3.15}/src/reader/_parser/_http_utils.py +1 -0
  20. {reader-3.13 → reader-3.15}/src/reader/_parser/_lazy.py +183 -154
  21. {reader-3.13 → reader-3.15}/src/reader/_parser/_url_utils.py +15 -4
  22. {reader-3.13 → reader-3.15}/src/reader/_parser/feedparser.py +10 -4
  23. {reader-3.13 → reader-3.15}/src/reader/_parser/file.py +2 -6
  24. reader-3.15/src/reader/_parser/http.py +107 -0
  25. {reader-3.13 → reader-3.15}/src/reader/_parser/jsonfeed.py +1 -1
  26. {reader-3.13 → reader-3.15}/src/reader/_parser/requests/__init__.py +2 -0
  27. {reader-3.13 → reader-3.15}/src/reader/_parser/requests/_lazy.py +26 -6
  28. reader-3.15/src/reader/_plugins/enclosure_tags.py +166 -0
  29. {reader-3.13 → reader-3.15}/src/reader/_storage/_base.py +7 -2
  30. {reader-3.13 → reader-3.15}/src/reader/_storage/_entries.py +3 -4
  31. {reader-3.13 → reader-3.15}/src/reader/_storage/_feeds.py +10 -9
  32. {reader-3.13 → reader-3.15}/src/reader/_storage/_schema.py +50 -3
  33. {reader-3.13 → reader-3.15}/src/reader/_storage/_search.py +16 -4
  34. {reader-3.13 → reader-3.15}/src/reader/_storage/_sqlite_utils.py +2 -1
  35. {reader-3.13 → reader-3.15}/src/reader/_storage/_tags.py +158 -52
  36. {reader-3.13 → reader-3.15}/src/reader/_types.py +52 -84
  37. {reader-3.13 → reader-3.15}/src/reader/_update.py +58 -59
  38. {reader-3.13 → reader-3.15}/src/reader/_utils.py +12 -4
  39. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/__init__.py +29 -10
  40. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/api.py +106 -135
  41. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/__init__.py +3 -2
  42. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/asctime.py +26 -17
  43. reader-3.15/src/reader/_vendor/feedparser/datetimes/greek.py +90 -0
  44. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/hungarian.py +19 -25
  45. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/iso8601.py +62 -52
  46. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/korean.py +35 -24
  47. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/perforce.py +21 -4
  48. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/rfc822.py +52 -23
  49. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +36 -22
  50. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/encodings.py +215 -147
  51. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/exceptions.py +6 -6
  52. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/html.py +93 -84
  53. reader-3.15/src/reader/_vendor/feedparser/http.py +78 -0
  54. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/mixin.py +330 -275
  55. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/_base.py +210 -166
  56. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/admin.py +10 -10
  57. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/cc.py +20 -19
  58. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/dc.py +21 -17
  59. reader-3.15/src/reader/_vendor/feedparser/namespaces/georss.py +683 -0
  60. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/itunes.py +25 -21
  61. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/mediarss.py +37 -36
  62. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/psc.py +13 -13
  63. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/parsers/json.py +54 -52
  64. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/parsers/loose.py +27 -29
  65. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/parsers/strict.py +34 -28
  66. reader-3.15/src/reader/_vendor/feedparser/sanitizer.py +974 -0
  67. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/sgml.py +17 -17
  68. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/urls.py +91 -72
  69. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/util.py +40 -40
  70. {reader-3.13 → reader-3.15}/src/reader/core.py +20 -32
  71. {reader-3.13 → reader-3.15}/src/reader/exceptions.py +6 -10
  72. {reader-3.13 → reader-3.15}/src/reader/plugins/__init__.py +1 -1
  73. {reader-3.13 → reader-3.15}/src/reader/plugins/entry_dedupe.py +42 -9
  74. {reader-3.13 → reader-3.15}/src/reader/plugins/mark_as_read.py +1 -1
  75. {reader-3.13 → reader-3.15}/src/reader/plugins/readtime.py +1 -1
  76. {reader-3.13 → reader-3.15}/src/reader/types.py +14 -4
  77. {reader-3.13 → reader-3.15/src/reader.egg-info}/PKG-INFO +5 -4
  78. {reader-3.13 → reader-3.15}/src/reader.egg-info/SOURCES.txt +2 -0
  79. {reader-3.13 → reader-3.15}/src/reader.egg-info/requires.txt +2 -2
  80. {reader-3.13 → reader-3.15}/tests/conftest.py +4 -5
  81. {reader-3.13 → reader-3.15}/tests/fakeparser.py +15 -16
  82. {reader-3.13 → reader-3.15}/tests/test_bench.py +3 -6
  83. {reader-3.13 → reader-3.15}/tests/test_cli.py +14 -0
  84. {reader-3.13 → reader-3.15}/tests/test_lazy_imports.py +11 -1
  85. {reader-3.13 → reader-3.15}/tests/test_parser.py +220 -89
  86. {reader-3.13 → reader-3.15}/tests/test_plugins_entry_dedupe.py +39 -5
  87. {reader-3.13 → reader-3.15}/tests/test_plugins_sqlite_releases.py +4 -2
  88. {reader-3.13 → reader-3.15}/tests/test_reader.py +43 -7
  89. {reader-3.13 → reader-3.15}/tests/test_reader_counts.py +41 -3
  90. {reader-3.13 → reader-3.15}/tests/test_reader_integration.py +21 -1
  91. {reader-3.13 → reader-3.15}/tests/test_reader_private.py +26 -20
  92. {reader-3.13 → reader-3.15}/tests/test_reader_search.py +3 -3
  93. {reader-3.13 → reader-3.15}/tests/utils.py +9 -4
  94. {reader-3.13 → reader-3.15}/tox.ini +3 -3
  95. reader-3.13/src/reader/_parser/http.py +0 -108
  96. reader-3.13/src/reader/_plugins/enclosure_tags.py +0 -119
  97. reader-3.13/src/reader/_vendor/feedparser/datetimes/greek.py +0 -86
  98. reader-3.13/src/reader/_vendor/feedparser/http.py +0 -227
  99. reader-3.13/src/reader/_vendor/feedparser/namespaces/georss.py +0 -276
  100. reader-3.13/src/reader/_vendor/feedparser/sanitizer.py +0 -950
  101. {reader-3.13 → reader-3.15}/LICENSE +0 -0
  102. {reader-3.13 → reader-3.15}/MANIFEST.in +0 -0
  103. {reader-3.13 → reader-3.15}/README.rst +0 -0
  104. {reader-3.13 → reader-3.15}/docs/Makefile +0 -0
  105. {reader-3.13 → reader-3.15}/docs/app.rst +0 -0
  106. {reader-3.13 → reader-3.15}/docs/changelog.rst +0 -0
  107. {reader-3.13 → reader-3.15}/docs/cli.rst +0 -0
  108. {reader-3.13 → reader-3.15}/docs/conf.py +0 -0
  109. {reader-3.13 → reader-3.15}/docs/config.rst +0 -0
  110. {reader-3.13 → reader-3.15}/docs/contributing.rst +0 -0
  111. {reader-3.13 → reader-3.15}/docs/dev-app.rst +0 -0
  112. {reader-3.13 → reader-3.15}/docs/images/redesign-01.png +0 -0
  113. {reader-3.13 → reader-3.15}/docs/index.rst +0 -0
  114. {reader-3.13 → reader-3.15}/docs/make.bat +0 -0
  115. {reader-3.13 → reader-3.15}/docs/screenshots/dillo.png +0 -0
  116. {reader-3.13 → reader-3.15}/docs/screenshots/entries-feed.png +0 -0
  117. {reader-3.13 → reader-3.15}/docs/screenshots/entries.png +0 -0
  118. {reader-3.13 → reader-3.15}/docs/screenshots/entry-one.png +0 -0
  119. {reader-3.13 → reader-3.15}/docs/screenshots/entry-two.png +0 -0
  120. {reader-3.13 → reader-3.15}/docs/screenshots/feeds.png +0 -0
  121. {reader-3.13 → reader-3.15}/docs/screenshots/lynx.png +0 -0
  122. {reader-3.13 → reader-3.15}/docs/screenshots/search.png +0 -0
  123. {reader-3.13 → reader-3.15}/docs/why.rst +0 -0
  124. {reader-3.13 → reader-3.15}/examples/config.yaml +0 -0
  125. {reader-3.13 → reader-3.15}/examples/podcast.py +0 -0
  126. {reader-3.13 → reader-3.15}/examples/terminal.py +0 -0
  127. {reader-3.13 → reader-3.15}/run.sh +0 -0
  128. {reader-3.13 → reader-3.15}/scripts/.coverage +0 -0
  129. {reader-3.13 → reader-3.15}/scripts/bench.py +0 -0
  130. {reader-3.13 → reader-3.15}/scripts/debug_storage_stats.py +0 -0
  131. {reader-3.13 → reader-3.15}/scripts/generate_import_all.py +0 -0
  132. {reader-3.13 → reader-3.15}/scripts/jscontrols.html +0 -0
  133. {reader-3.13 → reader-3.15}/scripts/jscontrols.py +0 -0
  134. {reader-3.13 → reader-3.15}/scripts/lines.sh +0 -0
  135. {reader-3.13 → reader-3.15}/scripts/release.py +0 -0
  136. {reader-3.13 → reader-3.15}/setup.cfg +0 -0
  137. {reader-3.13 → reader-3.15}/src/reader/__main__.py +0 -0
  138. {reader-3.13 → reader-3.15}/src/reader/_app/api_thing.py +0 -0
  139. {reader-3.13 → reader-3.15}/src/reader/_app/cli.py +0 -0
  140. {reader-3.13 → reader-3.15}/src/reader/_app/static/controls.js +0 -0
  141. {reader-3.13 → reader-3.15}/src/reader/_app/static/style.css +0 -0
  142. {reader-3.13 → reader-3.15}/src/reader/_app/templates/add_entry.html +0 -0
  143. {reader-3.13 → reader-3.15}/src/reader/_app/templates/entries.html +0 -0
  144. {reader-3.13 → reader-3.15}/src/reader/_app/templates/entry.html +0 -0
  145. {reader-3.13 → reader-3.15}/src/reader/_app/templates/feeds.html +0 -0
  146. {reader-3.13 → reader-3.15}/src/reader/_app/templates/layout.html +0 -0
  147. {reader-3.13 → reader-3.15}/src/reader/_app/templates/macros.html +0 -0
  148. {reader-3.13 → reader-3.15}/src/reader/_app/templates/metadata.html +0 -0
  149. {reader-3.13 → reader-3.15}/src/reader/_app/templates/tags.html +0 -0
  150. {reader-3.13 → reader-3.15}/src/reader/_app/wsgi.py +0 -0
  151. {reader-3.13 → reader-3.15}/src/reader/_config.py +0 -0
  152. {reader-3.13 → reader-3.15}/src/reader/_hash_utils.py +0 -0
  153. {reader-3.13 → reader-3.15}/src/reader/_plugins/__init__.py +0 -0
  154. {reader-3.13 → reader-3.15}/src/reader/_plugins/cli_status.py +0 -0
  155. {reader-3.13 → reader-3.15}/src/reader/_plugins/preview_feed_list.py +0 -0
  156. {reader-3.13 → reader-3.15}/src/reader/_plugins/share.py +0 -0
  157. {reader-3.13 → reader-3.15}/src/reader/_plugins/sqlite_releases.py +0 -0
  158. {reader-3.13 → reader-3.15}/src/reader/_plugins/templates/preview_feed_list.html +0 -0
  159. {reader-3.13 → reader-3.15}/src/reader/_plugins/timer.py +0 -0
  160. {reader-3.13 → reader-3.15}/src/reader/_storage/__init__.py +0 -0
  161. {reader-3.13 → reader-3.15}/src/reader/_storage/_changes.py +0 -0
  162. {reader-3.13 → reader-3.15}/src/reader/_storage/_html_utils.py +0 -0
  163. {reader-3.13 → reader-3.15}/src/reader/_storage/_sql_utils.py +0 -0
  164. {reader-3.13 → reader-3.15}/src/reader/_vendor/__init__.py +0 -0
  165. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
  166. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
  167. {reader-3.13 → reader-3.15}/src/reader/_vendor/feedparser/py.typed +0 -0
  168. {reader-3.13 → reader-3.15}/src/reader/plugins/enclosure_dedupe.py +0 -0
  169. {reader-3.13 → reader-3.15}/src/reader/plugins/ua_fallback.py +0 -0
  170. {reader-3.13 → reader-3.15}/src/reader/py.typed +0 -0
  171. {reader-3.13 → reader-3.15}/src/reader.egg-info/dependency_links.txt +0 -0
  172. {reader-3.13 → reader-3.15}/src/reader.egg-info/top_level.txt +0 -0
  173. {reader-3.13 → reader-3.15}/tests/data/10.json +0 -0
  174. {reader-3.13 → reader-3.15}/tests/data/10.json.py +0 -0
  175. {reader-3.13 → reader-3.15}/tests/data/custom +0 -0
  176. {reader-3.13 → reader-3.15}/tests/data/empty.atom +0 -0
  177. {reader-3.13 → reader-3.15}/tests/data/empty.atom.py +0 -0
  178. {reader-3.13 → reader-3.15}/tests/data/empty.json +0 -0
  179. {reader-3.13 → reader-3.15}/tests/data/empty.json.py +0 -0
  180. {reader-3.13 → reader-3.15}/tests/data/empty.rss +0 -0
  181. {reader-3.13 → reader-3.15}/tests/data/empty.rss.py +0 -0
  182. {reader-3.13 → reader-3.15}/tests/data/full.atom +0 -0
  183. {reader-3.13 → reader-3.15}/tests/data/full.atom.py +0 -0
  184. {reader-3.13 → reader-3.15}/tests/data/full.json +0 -0
  185. {reader-3.13 → reader-3.15}/tests/data/full.json.py +0 -0
  186. {reader-3.13 → reader-3.15}/tests/data/full.rss +0 -0
  187. {reader-3.13 → reader-3.15}/tests/data/full.rss.py +0 -0
  188. {reader-3.13 → reader-3.15}/tests/data/invalid.json +0 -0
  189. {reader-3.13 → reader-3.15}/tests/data/invalid.json.py +0 -0
  190. {reader-3.13 → reader-3.15}/tests/data/relative.atom +0 -0
  191. {reader-3.13 → reader-3.15}/tests/data/relative.atom.py +0 -0
  192. {reader-3.13 → reader-3.15}/tests/data/relative.rss +0 -0
  193. {reader-3.13 → reader-3.15}/tests/data/relative.rss.py +0 -0
  194. {reader-3.13 → reader-3.15}/tests/data/sqlite_releases.html +0 -0
  195. {reader-3.13 → reader-3.15}/tests/data/unknown.json +0 -0
  196. {reader-3.13 → reader-3.15}/tests/data/unknown.json.py +0 -0
  197. {reader-3.13 → reader-3.15}/tests/reader_methods.py +0 -0
  198. {reader-3.13 → reader-3.15}/tests/reader_test_plugins/__init__.py +0 -0
  199. {reader-3.13 → reader-3.15}/tests/reader_test_plugins/good.py +0 -0
  200. {reader-3.13 → reader-3.15}/tests/reader_test_plugins/init_error.py +0 -0
  201. {reader-3.13 → reader-3.15}/tests/reader_test_plugins/missing_dependency.py +0 -0
  202. {reader-3.13 → reader-3.15}/tests/reader_test_plugins/missing_entry_point.py +0 -0
  203. {reader-3.13 → reader-3.15}/tests/test__types.py +0 -0
  204. {reader-3.13 → reader-3.15}/tests/test_app.py +0 -0
  205. {reader-3.13 → reader-3.15}/tests/test_app_wsgi.py +0 -0
  206. {reader-3.13 → reader-3.15}/tests/test_changes.py +0 -0
  207. {reader-3.13 → reader-3.15}/tests/test_config.py +0 -0
  208. {reader-3.13 → reader-3.15}/tests/test_exceptions.py +0 -0
  209. {reader-3.13 → reader-3.15}/tests/test_hash_utils.py +0 -0
  210. {reader-3.13 → reader-3.15}/tests/test_html_utils.py +0 -0
  211. {reader-3.13 → reader-3.15}/tests/test_plugins_cli_status.py +0 -0
  212. {reader-3.13 → reader-3.15}/tests/test_plugins_enclosure_dedupe.py +0 -0
  213. {reader-3.13 → reader-3.15}/tests/test_plugins_mark_as_read.py +0 -0
  214. {reader-3.13 → reader-3.15}/tests/test_plugins_preview_feed_list.py +0 -0
  215. {reader-3.13 → reader-3.15}/tests/test_plugins_readtime.py +0 -0
  216. {reader-3.13 → reader-3.15}/tests/test_plugins_ua_fallback.py +0 -0
  217. {reader-3.13 → reader-3.15}/tests/test_reader_context.py +0 -0
  218. {reader-3.13 → reader-3.15}/tests/test_reader_deprecations.py +0 -0
  219. {reader-3.13 → reader-3.15}/tests/test_reader_filter.py +0 -0
  220. {reader-3.13 → reader-3.15}/tests/test_reader_hooks.py +0 -0
  221. {reader-3.13 → reader-3.15}/tests/test_reader_plugins.py +0 -0
  222. {reader-3.13 → reader-3.15}/tests/test_reader_sort.py +0 -0
  223. {reader-3.13 → reader-3.15}/tests/test_reader_update.py +0 -0
  224. {reader-3.13 → reader-3.15}/tests/test_search.py +0 -0
  225. {reader-3.13 → reader-3.15}/tests/test_sql_utils.py +0 -0
  226. {reader-3.13 → reader-3.15}/tests/test_sqlite_utils.py +0 -0
  227. {reader-3.13 → reader-3.15}/tests/test_storage.py +0 -0
  228. {reader-3.13 → reader-3.15}/tests/test_tags.py +0 -0
  229. {reader-3.13 → reader-3.15}/tests/test_test_utils.py +0 -0
  230. {reader-3.13 → reader-3.15}/tests/test_types.py +0 -0
  231. {reader-3.13 → reader-3.15}/tests/test_utils.py +0 -0
@@ -3,7 +3,77 @@ Changelog
3
3
  =========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
+
8
+
9
+ Version 3.15
10
+ ------------
11
+
12
+ Released 2024-11-11
13
+
14
+ * During :ref:`scheduled updates <scheduled>`,
15
+ honor the Retry-After HTTP header sent with
16
+ 429 Too Many Requests or 503 Service Unavailable responses.
17
+ (:issue:`307`)
18
+ * Optimize OR-only :meth:`~Reader.get_feeds()` and :meth:`~Reader.get_entries()`
19
+ tag filters (e.g. ``[['one', ...]]``). (:issue:`359`)
20
+ * Expose :data:`reader.core.DEFAULT_RESERVED_NAME_SCHEME`. (:issue:`287`)
21
+
22
+ * Update the (unstable) parser API to expose HTTP information to the updater.
23
+ (:issue:`307`)
24
+
25
+ * The :class:`.RetrieverType` protocol used by retrievers changed
26
+ (new return type, allow storing arbitrary caching data via
27
+ :attr:`~.RetrievedFeed.caching_info`).
28
+ * The ``HTTPAcceptParserType`` was renamed to :class:`.AcceptParserType`.
29
+
30
+ * Allow :ref:`using the installed feedparser <no-vendored-feedparser>`
31
+ instead of the one vendored by *reader*.
32
+ This is useful for working around issues in the vendored feedparser,
33
+ like the libxml2 <=2.13.3 incompatibility reported in :issue:`350`;
34
+ thanks to `Maks Verver`_ for reporting,
35
+ root causing, and following up with both dependencies.
36
+
37
+ * Fix a number of brittle / broken tests.
38
+ Thanks to `Maks Verver`_ for the issues and fixes.
39
+ (:issue:`348`, :issue:`349`, :issue:`355`)
40
+
41
+ .. _Maks Verver: https://github.com/maksverver
42
+
43
+
44
+ Version 3.14
45
+ ------------
46
+
47
+ Released 2024-07-23
48
+
49
+ * Add :attr:`~EntryCounts.unimportant` attribute to
50
+ :class:`EntryCounts` and :class:`EntrySearchCounts`.
51
+ Thanks to `chenthur`_ for the pull request.
52
+ (:issue:`283`)
53
+ * Fix bug in the :mod:`~reader.plugins.entry_dedupe` plugin causing entries
54
+ to flip-flop if there were multiple *new* duplicates of the same issue
55
+ (on the first update, one entry remains, on the second update, the other);
56
+ related to the bug fixed in `version 3.2 <Version 3.2_>`_.
57
+ (:issue:`340`)
58
+
59
+ * :mod:`~reader._plugins.enclosure_tags` plugin improvements. (:issue:`344`)
60
+
61
+ * Set genre to `Podcast` if the feed has any tag containing "podcast".
62
+ * Rewrite tags on the fly,
63
+ instead of downloading the entire file before sending it to the user;
64
+ allows browsers to display accurate download progress.
65
+ * Prefer feed user title to feed title if available.
66
+ * Use feed title as artist, instead of author.
67
+
68
+ * Support Python 3.13. (:issue:`341`)
69
+ * Update vendored `feedparser`_ to the ``develop`` branch as of 2024-06-26.
70
+ Needed because upstream removed dependency on stdlib module ``cgi``
71
+ (removed in Python 3.13), but the `version 2.9 <Version 2.9_>`_
72
+ memory usage reduction was not released upstream yet.
73
+ (:issue:`341`)
74
+
75
+ .. _chenthur: https://github.com/chenthur
76
+ .. _feedparser: https://feedparser.readthedocs.io/en/latest/
7
77
 
8
78
 
9
79
  Version 3.13
@@ -341,7 +411,7 @@ Released 2022-09-14
341
411
  became optional.
342
412
  (:issue:`96`)
343
413
  * Fix bug in :mod:`~reader.plugins.entry_dedupe` causing updates to fail
344
- if there were multiple *new* duplicates of the same issue.
414
+ if there were multiple *new* duplicates of the same entry.
345
415
  (:issue:`292`)
346
416
  * Fix bug in :mod:`~reader.plugins.readtime`
347
417
  and :mod:`~reader.plugins.mark_as_read` causing updates to fail
@@ -615,6 +685,8 @@ Released 2022-03-12
615
685
  Fix broken error flashing.
616
686
 
617
687
 
688
+ .. _version 2.9:
689
+
618
690
  Version 2.9
619
691
  -----------
620
692
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reader
3
- Version: 3.13
3
+ Version: 3.15
4
4
  Summary: A Python feed reader library.
5
5
  Author: lemon24
6
6
  License: BSD-3-Clause
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
22
22
  Classifier: Programming Language :: Python :: 3.10
23
23
  Classifier: Programming Language :: Python :: 3.11
24
24
  Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
25
26
  Classifier: Programming Language :: Python :: Implementation :: CPython
26
27
  Classifier: Programming Language :: Python :: Implementation :: PyPy
27
28
  Classifier: Topic :: Internet
@@ -65,11 +66,11 @@ Requires-Dist: pytest-rerunfailures; extra == "tests"
65
66
  Requires-Dist: coverage; extra == "tests"
66
67
  Requires-Dist: pytest-cov; extra == "tests"
67
68
  Requires-Dist: requests-mock; extra == "tests"
68
- Requires-Dist: mechanicalsoup; (implementation_name != "pypy" or python_version <= "3.9") and extra == "tests"
69
+ Requires-Dist: mechanicalsoup; (implementation_name != "pypy" and python_version <= "3.12") and extra == "tests"
69
70
  Requires-Dist: requests-wsgi-adapter; extra == "tests"
70
- Requires-Dist: lxml; (implementation_name != "pypy" or python_version <= "3.9") and extra == "tests"
71
+ Requires-Dist: lxml; (implementation_name != "pypy" and python_version <= "3.12") and extra == "tests"
71
72
  Requires-Dist: html5lib; extra == "tests"
72
- Requires-Dist: numpy; (implementation_name != "pypy" and os_name == "posix") and extra == "tests"
73
+ Requires-Dist: numpy; (implementation_name != "pypy" and os_name == "posix" and python_version <= "3.12") and extra == "tests"
73
74
  Requires-Dist: mypy; implementation_name != "pypy" and extra == "tests"
74
75
  Requires-Dist: types-requests; extra == "tests"
75
76
  Requires-Dist: types-beautifulsoup4; extra == "tests"
@@ -16,7 +16,7 @@ Most of *reader*'s functionality can be accessed through a :class:`Reader` insta
16
16
 
17
17
  Split this into sub-sections.
18
18
 
19
- .. autofunction:: make_reader(url, *, feed_root=None, plugins=..., session_timeout=(3.05, 60), reserved_name_scheme=..., search_enabled='auto')
19
+ .. autofunction:: make_reader
20
20
 
21
21
  .. autoclass:: Reader(...)
22
22
  :members:
@@ -184,4 +184,6 @@ Type aliases
184
184
  Constants
185
185
  ---------
186
186
 
187
+ .. autodata:: reader.core.DEFAULT_RESERVED_NAME_SCHEME
188
+
187
189
  .. autodata:: reader.plugins.DEFAULT_PLUGINS
@@ -3,7 +3,7 @@ Development
3
3
  ===========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
7
 
8
8
 
9
9
  Development should follow a problem-solution_ approach.
@@ -378,8 +378,14 @@ Requests session plugins:
378
378
  Retriever / parser split:
379
379
 
380
380
  * :issue:`205#issuecomment-766321855`
381
- * split exception hierarchy (not implemented as of 3.9): :issue:`218#issuecomment-1687094315`
381
+ * split exception hierarchy (not implemented as of 3.15): :issue:`218#issuecomment-1687094315`
382
+ * API overview as of 3.14, meant to show the dataflow: :issue:`307#issuecomment-2266647310`
383
+ * ~ideal API (mostly implemented in 3.15): :issue:`307#issuecomment-2281797898`
382
384
 
385
+ * exposes HTTP information (so it can be used by the updater)
386
+ * introduces ~internal RetrieveError and NotModified :exc:`ParseError` subclasses;
387
+ notably, this doesn't really follow the split exception hierarchy mentioned above,
388
+ and is only meant to surface HTTP information in error cases
383
389
 
384
390
  Alternative feed parsers:
385
391
 
@@ -398,17 +404,13 @@ Lessons learned from the :ref:`Twitter` plugin:
398
404
  :meth:`~reader._parser.ParserType` being generic.
399
405
 
400
406
  * It is useful for a Retriever to store arbitrary caching data;
401
- the plugin (mis)used
402
- :attr:`~reader._parser.RetrieveResult.http_etag`
407
+ the plugin (mis)used ``RetrieveResult.http_etag``
403
408
  to store the (integer) id of the newest tweet in the thread.
404
409
 
405
- It would be nice to formalize this into a single
406
- "arbitrary caching data" attribute;
407
- also see `this comment <https://github.com/lemon24/reader/blob/0ada24bf4e65c69c3028641bada8eaeabbe02754/src/reader/_parser.py#L569>`_.
410
+ Update: This was formalized as :attr:`.RetrievedFeed.caching_info` in 3.15.
408
411
 
409
412
  * It is useful for a Retriever to pass arbitrary data to itself;
410
- the plugin (mis)used
411
- :attr:`~reader._types.FeedForUpdate.http_etag` to pass from
413
+ the plugin (mis)used ``FeedForUpdate.http_etag`` to pass from
412
414
  :meth:`~reader._parser.FeedForUpdateRetrieverType.process_feed_for_update`
413
415
  to :meth:`~reader._parser.RetrieverType.__call__`:
414
416
 
@@ -684,6 +686,14 @@ Update hook error handling:
684
686
  * expected behavior: :issue:`218#issuecomment-1595691410`, :issue:`218#issuecomment-1666823222`
685
687
  * update hook pseudocode + exception hierarchy: :issue:`218#issuecomment-1666869215`
686
688
 
689
+ Considerations on using pluggy: :issue:`329`,
690
+ `how adapting reader use cases to the pluggy model may work <https://github.com/pytest-dev/pluggy/issues/151#issuecomment-2421648901>`_.
691
+ Conclusions:
692
+
693
+ even if using pluggy would require some work-arounds for calling, I think the collection / discovery functionality is still more than worth it
694
+
695
+ long-term and/or if reader becomes really popular, pluggy is definitely the way to go
696
+
687
697
 
688
698
  Reserved names
689
699
  ~~~~~~~~~~~~~~
@@ -721,6 +731,8 @@ Duplicate entries
721
731
  Duplicate entries are mainly handled by the :mod:`reader.entry_dedupe` plugin.
722
732
 
723
733
  * Using MinHash to speed up similarity checks (maybe): https://gist.github.com/lemon24/b9af5ade919713406bda9603847d32e5
734
+ * Discussion of unifying "on-line" dedupe (after an entry is added/updated),
735
+ and "on-demand" dedupe (backfill): :issue:`340`.
724
736
 
725
737
  However, it is also possible for a feed to have two entries with the same id
726
738
  – yes, even though in most (if not all) formats,
@@ -3,7 +3,7 @@ User guide
3
3
  ==========
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
7
 
8
8
 
9
9
  This page gives a tour of *reader*'s features,
@@ -272,34 +272,42 @@ Scheduled updates
272
272
  Because different feeds need to be updated at different rates,
273
273
  *reader* also provides a mechanism for scheduling updates.
274
274
 
275
- Each feed has an update interval that, on every update,
276
- determines when the feed should be updated next.
277
- Running :meth:`update_feeds(scheduled=True) <Reader.update_feeds>`
275
+ On every update, *reader* determines when a feed should be updated next
276
+ based on a configurable update interval;
277
+ :meth:`update_feeds(scheduled=True) <Reader.update_feeds>`
278
278
  updates only the feeds that should be updated at or before the current time.
279
279
 
280
- The global and per-feed update interval can be **configured by the user**
281
- via the ``.reader.update`` global/feed tag;
282
- the default interval is of one hour;
283
- see :data:`~reader.types.UpdateConfig` for the schema.
284
- In addition to the interval, the user can specify a jitter;
285
- for an interval of 24 hours, a jitter of 0.25 means
286
- the update will occur any time in the first 6 hours of the interval.
280
+ The interval can be **configured by the user**
281
+ through the ``.reader.update`` global tag,
282
+ and overridden for individual feeds through the matching feed tag;
283
+ if no configuration is provided, the default interval is one hour.
284
+ See :data:`~reader.types.UpdateConfig` for the config format.
287
285
 
288
- .. note::
286
+ In addition, the user can specify a jitter,
287
+ a random amount of time added to the interval
288
+ such that updates don't happen exactly at the the same time;
289
+ for example, with a one-hour interval, a jitter of 0.25 means
290
+ updates will occur any time in the first 15 minutes of each hour.
291
+
292
+ If the server sends the Retry-After HTTP header with
293
+ 429 Too Many Requests or 503 Service Unavailable responses,
294
+ :meth:`update_feeds(scheduled=True) <Reader.update_feeds>` will honor it.
289
295
 
290
- As of |version|, there is no way to specify a minimum update interval.
291
- If you want feeds to be updated no more often than e.g. every hour,
292
- you have to run :meth:`update_feeds(scheduled=True) <Reader.update_feeds>`
293
- no more often than every hour.
294
- Please :ref:`open an issue <issues>` if you need a minimum update interval.
295
296
 
296
- In a future version of *reader*,
297
- the same mechanism will be used to handle
298
- HTTP 429 Too Many Requests; see :issue:`307` for details.
297
+ .. note::
298
+
299
+ As of |version|, there is no way to enforce a minimum update interval;
300
+ if you want feeds to be updated no more often than e.g. every hour,
301
+ you have to run :meth:`~Reader.update_feeds` no more often than every hour.
302
+ Please :ref:`open an issue <issues>` or :ref:`submit a pull request <prs>`
303
+ if you need a minimum update interval.
299
304
 
300
305
 
301
306
  .. versionadded:: 3.13
302
307
 
308
+ .. versionchanged:: 3.15
309
+ Honor the Retry-After HTTP header.
310
+
303
311
 
304
312
  Update status
305
313
  ~~~~~~~~~~~~~
@@ -27,6 +27,20 @@ These packages will be installed automatically when installing *reader*:
27
27
  (at least SQLite 3.18 with the `JSON1`_ and `FTS5`_ extensions).
28
28
 
29
29
 
30
+ .. _no-vendored-feedparser:
31
+
32
+ .. note::
33
+
34
+ Because `feedparser`_ makes PyPI releases at a lower cadence,
35
+ *reader* uses a vendored version of feedparser's `develop`_ branch
36
+ by default since :ref:`version 2.9`.
37
+ To opt out of this behavior, and make *reader* use
38
+ the installed ``feedparser`` package,
39
+ set the ``READER_NO_VENDORED_FEEDPARSER`` environment variable to ``1``.
40
+
41
+ .. _develop: https://github.com/kurtmckee/feedparser
42
+
43
+
30
44
  .. _optional dependencies:
31
45
 
32
46
  Optional dependencies
@@ -45,7 +45,7 @@ Protocols
45
45
  ~~~~~~~~~
46
46
 
47
47
  .. module:: reader._parser
48
- :noindex:
48
+ :no-index:
49
49
 
50
50
  .. autoclass:: FeedArgument
51
51
  :members:
@@ -54,6 +54,14 @@ Protocols
54
54
  :members:
55
55
  :special-members: __call__
56
56
 
57
+ .. autoexception:: RetrieveError
58
+ :show-inheritance:
59
+ :members:
60
+
61
+ .. autoexception:: NotModified
62
+ :show-inheritance:
63
+ :members:
64
+
57
65
  .. autoclass:: FeedForUpdateRetrieverType
58
66
  :members:
59
67
  :show-inheritance:
@@ -62,7 +70,7 @@ Protocols
62
70
  :members:
63
71
  :special-members: __call__
64
72
 
65
- .. autoclass:: HTTPAcceptParserType
73
+ .. autoclass:: AcceptParserType
66
74
  :members:
67
75
  :show-inheritance:
68
76
 
@@ -72,7 +80,7 @@ Protocols
72
80
 
73
81
 
74
82
  .. module:: reader._parser.requests
75
- :noindex:
83
+ :no-index:
76
84
 
77
85
  .. autoclass:: RequestHook
78
86
  :members:
@@ -87,30 +95,21 @@ Data objects
87
95
  ~~~~~~~~~~~~
88
96
 
89
97
  .. module:: reader._parser
90
- :noindex:
98
+ :no-index:
91
99
 
92
100
  .. autoclass:: RetrieveResult
93
101
  :members:
94
102
 
95
- .. module:: reader._types
96
-
97
- .. autoclass:: ParsedFeed
103
+ .. autoclass:: RetrievedFeed
98
104
  :members:
99
105
 
100
- .. autoclass:: FeedData
106
+ .. autoclass:: ParseResult
101
107
  :members:
102
- :undoc-members:
103
-
104
- .. autoclass:: EntryData
105
- :members:
106
- :undoc-members:
107
108
 
108
- .. todo:: the following should be in the storage section, when we get one
109
-
110
- .. autoclass:: FeedForUpdate
109
+ .. autoclass:: ParsedFeed
111
110
  :members:
112
111
 
113
- .. autoclass:: EntryForUpdate
112
+ .. autoclass:: HTTPInfo
114
113
  :members:
115
114
 
116
115
 
@@ -155,7 +154,7 @@ but at least one other implementation needs to exists before that.
155
154
  .. autoattribute:: reader.Reader._search
156
155
 
157
156
  .. module:: reader._types
158
- :noindex:
157
+ :no-index:
159
158
 
160
159
  .. autoclass:: StorageType()
161
160
  :members:
@@ -171,6 +170,9 @@ but at least one other implementation needs to exists before that.
171
170
 
172
171
  .. _changes:
173
172
 
173
+ Change tracking
174
+ ~~~~~~~~~~~~~~~
175
+
174
176
  .. autoclass:: ChangeTrackingStorageType()
175
177
  :members:
176
178
  :show-inheritance:
@@ -193,12 +195,26 @@ but at least one other implementation needs to exists before that.
193
195
  Data objects
194
196
  ~~~~~~~~~~~~
195
197
 
198
+ .. autoclass:: FeedData
199
+ :members:
200
+ :undoc-members:
201
+
202
+ .. autoclass:: EntryData
203
+ :members:
204
+ :undoc-members:
205
+
196
206
  .. autoclass:: FeedFilter
197
207
  :members:
198
208
 
199
209
  .. autoclass:: EntryFilter
200
210
  :members:
201
211
 
212
+ .. autoclass:: FeedForUpdate
213
+ :members:
214
+
215
+ .. autoclass:: EntryForUpdate
216
+ :members:
217
+
202
218
  .. autoclass:: FeedUpdateIntent
203
219
  :members:
204
220
 
@@ -219,9 +235,14 @@ Type aliases
219
235
  Recipes
220
236
  -------
221
237
 
222
- .. include:: ../examples/parser_only.py
238
+ .. include:: ../examples/custom_headers.py
223
239
  :start-after: """
224
- :end-before: """ # docstring-end
240
+ :end-before: """
241
+ .. literalinclude:: ../examples/custom_headers.py
242
+ :start-at: import
225
243
 
244
+ .. include:: ../examples/parser_only.py
245
+ :start-after: """
246
+ :end-before: """
226
247
  .. literalinclude:: ../examples/parser_only.py
227
- :start-after: """ # docstring-end
248
+ :start-at: import
@@ -3,7 +3,7 @@ Plugins
3
3
  =======
4
4
 
5
5
  .. module:: reader
6
- :noindex:
6
+ :no-index:
7
7
 
8
8
 
9
9
  .. _built-in plugins:
@@ -116,3 +116,20 @@ For `built-in plugins`_, it is enough to use the plugin name (``reader.XYZ``).
116
116
  To load web application plugins, set the ``READER_APP_PLUGIN`` environment variable.
117
117
  To load CLI plugins (that customize the CLI),
118
118
  set the ``READER_CLI_PLUGIN`` environment variable.
119
+
120
+
121
+
122
+ Recipes
123
+ -------
124
+
125
+ I currently don't need this functionality,
126
+ but if you'd be interested in maintaining any of these
127
+ as an experimental or even built-in plugin,
128
+ please :doc:`submit a pull request <contributing>`.
129
+
130
+ .. include:: ../examples/feed_slugs.py
131
+ :start-after: """
132
+ :end-before: """
133
+ .. literalinclude:: ../examples/feed_slugs.py
134
+ :start-at: def init_reader
135
+ :end-before: if __name__
@@ -2,7 +2,7 @@ Tutorial
2
2
  ========
3
3
 
4
4
  .. module:: reader
5
- :noindex:
5
+ :no-index:
6
6
 
7
7
 
8
8
  In this tutorial we'll use *reader* to download all the episodes of a podcast,
@@ -0,0 +1,50 @@
1
+ """
2
+ Adding custom headers when retrieving feeds
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Example of adding custom request headers with :attr:`.SessionFactory.request_hooks`:
6
+
7
+ .. code-block:: console
8
+
9
+ $ python examples/custom_headers.py
10
+ updating...
11
+ server: Hello, world!
12
+ updated!
13
+
14
+ """
15
+
16
+ # fmt: off
17
+ # flake8: noqa
18
+
19
+ import http.server
20
+ import threading
21
+ from reader import make_reader
22
+
23
+ # start a background server that logs the received header
24
+
25
+ class Handler(http.server.BaseHTTPRequestHandler):
26
+ def log_message(self, *_): pass
27
+ def do_GET(self):
28
+ print("server:", self.headers.get('my-header'))
29
+ self.send_error(304)
30
+
31
+ server = http.server.HTTPServer(('localhost', 8080), Handler)
32
+ threading.Thread(target=server.handle_request).start()
33
+
34
+ # create a reader object
35
+
36
+ reader = make_reader(':memory:')
37
+ reader.add_feed('http://localhost:8080')
38
+
39
+ # set up a hook that adds the header to each request
40
+
41
+ def hook(session, request, **kwargs):
42
+ request.headers.setdefault('my-header', 'Hello, world!')
43
+
44
+ reader._parser.session_factory.request_hooks.append(hook)
45
+
46
+ # updating the feed sends the modified request to the server
47
+
48
+ print("updating...")
49
+ reader.update_feeds()
50
+ print("updated!")
@@ -0,0 +1,95 @@
1
+ """
2
+ Feed slugs
3
+ ~~~~~~~~~~
4
+
5
+ This is a recipe of what a "get feed by slug" plugin may look like
6
+ (e.g. for user-defined short URLs).
7
+
8
+ Usage::
9
+
10
+ >>> from reader import make_reader
11
+ >>> import feed_slugs
12
+ >>> reader = make_reader('db.sqlite', plugins=[feed_slugs.init_reader])
13
+ >>> reader.set_feed_slug('https://death.andgravity.com/_feed/index.xml', 'andgravity')
14
+ >>> reader.get_feed_by_slug('andgravity')
15
+ Feed(url='https://death.andgravity.com/_feed/index.xml', ...)
16
+ >>> reader.get_feed_slug(_.url)
17
+ 'andgravity'
18
+
19
+ ..
20
+ Originally implemented for https://github.com/lemon24/reader/issues/358.
21
+
22
+ """
23
+
24
+ # fmt: off
25
+ # flake8: noqa
26
+
27
+ def init_reader(reader):
28
+ # __get__() allows help(reader.get_feed_by_slug) to work
29
+ reader.get_feed_by_slug = get_feed_by_slug.__get__(reader)
30
+ reader.get_feed_slug = get_feed_slug.__get__(reader)
31
+ reader.set_feed_slug = set_feed_slug.__get__(reader)
32
+
33
+ def get_feed_by_slug(reader, slug):
34
+ tag = _make_tag(reader, slug)
35
+ return next(reader.get_feeds(tags=[tag], limit=1), None)
36
+
37
+ def get_feed_slug(reader, feed):
38
+ if tag := next(_get_tags(reader, feed), None):
39
+ return tag.removeprefix(_make_tag(reader, ''))
40
+ return None
41
+
42
+ def set_feed_slug(reader, feed, slug: str | None):
43
+ feed = reader.get_feed(feed)
44
+ tag = _make_tag(reader, slug)
45
+
46
+ if not slug:
47
+ reader.delete_tag(feed, tag, missing_ok=True)
48
+ return
49
+
50
+ reader.set_tag(feed, tag)
51
+
52
+ # ensure only one feed has the slug; technically a race condition,
53
+ # when it happens no feed will have the tag
54
+ for other_feed in reader.get_feeds(tags=[tag]):
55
+ if feed.url != other_feed.url:
56
+ reader.delete_tag(other_feed, tag, missing_ok=True)
57
+
58
+ # ensure feed has only one slug; technically a race condition,
59
+ # when it happens the feed will have no slug
60
+ for other_tag in _get_tags(reader, feed):
61
+ if tag != other_tag:
62
+ reader.delete_tag(feed, other_tag, missing_ok=True)
63
+
64
+ def _make_tag(reader, slug):
65
+ return reader.make_plugin_reserved_name('slug', slug)
66
+
67
+ def _get_tags(reader, resource):
68
+ prefix = _make_tag(reader, '')
69
+ # filter tags by prefix would make this faster,
70
+ # https://github.com/lemon24/reader/issues/309
71
+ return (t for t in reader.get_tag_keys(resource) if t.startswith(prefix))
72
+
73
+ if __name__ == '__main__':
74
+ from reader import make_reader
75
+
76
+ reader = make_reader('db.sqlite', plugins=[init_reader])
77
+ url = 'https://death.andgravity.com/_feed/index.xml'
78
+
79
+ reader.set_feed_slug(url, 'one')
80
+ print(
81
+ reader.get_feed_slug(url),
82
+ getattr(reader.get_feed_by_slug('one'), 'url', None),
83
+ )
84
+
85
+ reader.set_feed_slug(url, 'two')
86
+ print(
87
+ reader.get_feed_slug(url),
88
+ getattr(reader.get_feed_by_slug('two'), 'url', None),
89
+ )
90
+
91
+ reader.set_feed_slug('https://xkcd.com/atom.xml', 'two')
92
+ print(
93
+ reader.get_feed_slug(url),
94
+ getattr(reader.get_feed_by_slug('two'), 'url', None),
95
+ )
@@ -11,7 +11,7 @@ retrieved asynchronously with `HTTPX <https://www.python-httpx.org/>`_:
11
11
  death and gravity
12
12
  Has your password been pwned? Or, how I almost failed to search a 37 GB text file in under 1 millisecond (in Python)
13
13
 
14
- """ # docstring-end
14
+ """
15
15
 
16
16
  import asyncio
17
17
  import io
@@ -26,6 +26,7 @@ classifiers = [
26
26
  "Programming Language :: Python :: 3.10",
27
27
  "Programming Language :: Python :: 3.11",
28
28
  "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
29
30
  "Programming Language :: Python :: Implementation :: CPython",
30
31
  "Programming Language :: Python :: Implementation :: PyPy",
31
32
  "Topic :: Internet",
@@ -107,14 +108,15 @@ tests = [
107
108
  "pytest-cov",
108
109
  "requests-mock",
109
110
  # mechanicalsoup hard-depends on lxml (see below)
110
- 'mechanicalsoup; (implementation_name != "pypy" or python_version <= "3.9")',
111
+ 'mechanicalsoup; (implementation_name != "pypy" and python_version <= "3.12")',
111
112
  "requests-wsgi-adapter",
112
113
  # we want to test search with all known bs4 parsers.
113
- # lxml does not have a PyPy 3.10+ wheel.
114
- 'lxml; (implementation_name != "pypy" or python_version <= "3.9")',
114
+ # lxml usually does not have recent PyPy wheels.
115
+ # lxml usually does not have pre-relase CPython wheels.
116
+ 'lxml; (implementation_name != "pypy" and python_version <= "3.12")',
115
117
  "html5lib",
116
118
  # for bench.py
117
- 'numpy; (implementation_name != "pypy" and os_name == "posix")',
119
+ 'numpy; (implementation_name != "pypy" and os_name == "posix" and python_version <= "3.12")',
118
120
  # mypy does not work on pypy (yet).
119
121
  'mypy; implementation_name != "pypy"',
120
122
  "types-requests",