detextive 3.0__tar.gz → 3.2__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.
@@ -5,3 +5,5 @@ __pycache__/
5
5
  bugs/
6
6
  build/
7
7
  dist/
8
+
9
+ uv.lock
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: detextive
3
- Version: 3.0
3
+ Version: 3.2
4
4
  Summary: Detects textual content.
5
5
  Project-URL: Homepage, https://github.com/emcd/python-detextive
6
6
  Project-URL: Documentation, https://emcd.github.io/python-detextive
@@ -15,15 +15,13 @@ Classifier: Development Status :: 5 - Production/Stable
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: Apache Software License
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
21
19
  Classifier: Programming Language :: Python :: 3.13
22
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
23
22
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
- Classifier: Programming Language :: Python :: Implementation :: PyPy
25
23
  Classifier: Topic :: Software Development
26
- Requires-Python: >=3.10
24
+ Requires-Python: >=3.12
27
25
  Requires-Dist: absence~=1.1
28
26
  Requires-Dist: accretive~=4.1
29
27
  Requires-Dist: chardet
@@ -113,6 +111,8 @@ Key Features ⭐
113
111
  using control character and printability heuristics.
114
112
 
115
113
 
114
+
115
+
116
116
  Installation 📦
117
117
  ===============================================================================
118
118
 
@@ -229,12 +229,14 @@ For complete bytes-to-text processing with automatic charset detection and valid
229
229
  print( f"Decoded text: {text}" )
230
230
 
231
231
 
232
+
233
+
232
234
  Contribution 🤝
233
235
  ===============================================================================
234
236
 
235
237
  Contribution to this project is welcome! However, it must follow the `code of
236
238
  conduct
237
- <https://emcd.github.io/python-project-common/stable/sphinx-html/common/conduct.html>`_
239
+ <https://emcd.github.io/documentation-common/v1/sphinx-html/common/conduct.html>`_
238
240
  for the project.
239
241
 
240
242
  Please file bug reports and feature requests in the `issue tracker
@@ -286,9 +288,15 @@ Other Projects by This Author 🌟
286
288
  ===============================================================================
287
289
 
288
290
 
289
- * `python-absence <https://github.com/emcd/python-absence>`_ (`absence <https://pypi.org/project/absence/>`_ on PyPI)
291
+ * `agentmux <https://github.com/emcd/agentmux>`_ (`agentmux <https://crates.io/crates/agentmux/>`_ on Crates.io)
290
292
 
291
- 🕳️ A Python library package which provides a **sentinel for absent values** - a falsey, immutable singleton that represents the absence of a value in contexts where ``None`` or ``False`` may be valid values.
293
+ 📡 Provides **relays for inter-agent communication** and a single pane of glass for managing multiple agent harness sessions.
294
+ * `cistella <https://github.com/emcd/cistella>`_ (`cistella <https://crates.io/crates/cistella/>`_ on Crates.io)
295
+
296
+ 🧱 A **sandboxing application** which **isolates agent sessions** via various technologies.
297
+ * `nb-mcp-server <https://github.com/emcd/nb-mcp-server>`_ (`nb-mcp-server <https://crates.io/crates/nb-mcp-server/>`_ on Crates.io)
298
+
299
+ 🗒️ An MCP server wrapping the ``nb`` CLI for **LLM-friendly note-taking**.
292
300
  * `python-accretive <https://github.com/emcd/python-accretive>`_ (`accretive <https://pypi.org/project/accretive/>`_ on PyPI)
293
301
 
294
302
  🌌 A Python library package which provides **accretive data structures** - collections which can grow but never shrink.
@@ -304,12 +312,6 @@ Other Projects by This Author 🌟
304
312
  * `python-frigid <https://github.com/emcd/python-frigid>`_ (`frigid <https://pypi.org/project/frigid/>`_ on PyPI)
305
313
 
306
314
  🔒 A Python library package which provides **immutable data structures** - collections which cannot be modified after creation.
307
- * `python-icecream-truck <https://github.com/emcd/python-icecream-truck>`_ (`icecream-truck <https://pypi.org/project/icecream-truck/>`_ on PyPI)
308
-
309
- 🍦 **Flavorful Debugging** - A Python library which enhances the powerful and well-known ``icecream`` package with flavored traces, configuration hierarchies, customized outputs, ready-made recipes, and more.
310
- * `python-librovore <https://github.com/emcd/python-librovore>`_ (`librovore <https://pypi.org/project/librovore/>`_ on PyPI)
311
-
312
- 🐲 **Documentation Search Engine** - An intelligent documentation search and extraction tool that provides both a command-line interface for humans and an MCP (Model Context Protocol) server for AI agents. Search across Sphinx and MkDocs sites with fuzzy matching, extract clean markdown content, and integrate seamlessly with AI development workflows.
313
- * `python-mimeogram <https://github.com/emcd/python-mimeogram>`_ (`mimeogram <https://pypi.org/project/mimeogram/>`_ on PyPI)
315
+ * `python-ictr <https://github.com/emcd/python-ictr>`_ (`ictr <https://pypi.org/project/ictr/>`_ on PyPI)
314
316
 
315
- 📨 A command-line tool for **exchanging collections of files with Large Language Models** - bundle multiple files into a single clipboard-ready document while preserving directory structure and metadata... good for code reviews, project sharing, and LLM interactions.
317
+ 🍦 **Non-Intrusive Debugging** - A Python library which provides flavored traces, configuration hierarchies, customized outputs, and ready-made recipes for logging and debug printing.
@@ -69,6 +69,8 @@ Key Features ⭐
69
69
  using control character and printability heuristics.
70
70
 
71
71
 
72
+
73
+
72
74
  Installation 📦
73
75
  ===============================================================================
74
76
 
@@ -185,12 +187,14 @@ For complete bytes-to-text processing with automatic charset detection and valid
185
187
  print( f"Decoded text: {text}" )
186
188
 
187
189
 
190
+
191
+
188
192
  Contribution 🤝
189
193
  ===============================================================================
190
194
 
191
195
  Contribution to this project is welcome! However, it must follow the `code of
192
196
  conduct
193
- <https://emcd.github.io/python-project-common/stable/sphinx-html/common/conduct.html>`_
197
+ <https://emcd.github.io/documentation-common/v1/sphinx-html/common/conduct.html>`_
194
198
  for the project.
195
199
 
196
200
  Please file bug reports and feature requests in the `issue tracker
@@ -242,9 +246,15 @@ Other Projects by This Author 🌟
242
246
  ===============================================================================
243
247
 
244
248
 
245
- * `python-absence <https://github.com/emcd/python-absence>`_ (`absence <https://pypi.org/project/absence/>`_ on PyPI)
249
+ * `agentmux <https://github.com/emcd/agentmux>`_ (`agentmux <https://crates.io/crates/agentmux/>`_ on Crates.io)
246
250
 
247
- 🕳️ A Python library package which provides a **sentinel for absent values** - a falsey, immutable singleton that represents the absence of a value in contexts where ``None`` or ``False`` may be valid values.
251
+ 📡 Provides **relays for inter-agent communication** and a single pane of glass for managing multiple agent harness sessions.
252
+ * `cistella <https://github.com/emcd/cistella>`_ (`cistella <https://crates.io/crates/cistella/>`_ on Crates.io)
253
+
254
+ 🧱 A **sandboxing application** which **isolates agent sessions** via various technologies.
255
+ * `nb-mcp-server <https://github.com/emcd/nb-mcp-server>`_ (`nb-mcp-server <https://crates.io/crates/nb-mcp-server/>`_ on Crates.io)
256
+
257
+ 🗒️ An MCP server wrapping the ``nb`` CLI for **LLM-friendly note-taking**.
248
258
  * `python-accretive <https://github.com/emcd/python-accretive>`_ (`accretive <https://pypi.org/project/accretive/>`_ on PyPI)
249
259
 
250
260
  🌌 A Python library package which provides **accretive data structures** - collections which can grow but never shrink.
@@ -260,12 +270,6 @@ Other Projects by This Author 🌟
260
270
  * `python-frigid <https://github.com/emcd/python-frigid>`_ (`frigid <https://pypi.org/project/frigid/>`_ on PyPI)
261
271
 
262
272
  🔒 A Python library package which provides **immutable data structures** - collections which cannot be modified after creation.
263
- * `python-icecream-truck <https://github.com/emcd/python-icecream-truck>`_ (`icecream-truck <https://pypi.org/project/icecream-truck/>`_ on PyPI)
264
-
265
- 🍦 **Flavorful Debugging** - A Python library which enhances the powerful and well-known ``icecream`` package with flavored traces, configuration hierarchies, customized outputs, ready-made recipes, and more.
266
- * `python-librovore <https://github.com/emcd/python-librovore>`_ (`librovore <https://pypi.org/project/librovore/>`_ on PyPI)
267
-
268
- 🐲 **Documentation Search Engine** - An intelligent documentation search and extraction tool that provides both a command-line interface for humans and an MCP (Model Context Protocol) server for AI agents. Search across Sphinx and MkDocs sites with fuzzy matching, extract clean markdown content, and integrate seamlessly with AI development workflows.
269
- * `python-mimeogram <https://github.com/emcd/python-mimeogram>`_ (`mimeogram <https://pypi.org/project/mimeogram/>`_ on PyPI)
273
+ * `python-ictr <https://github.com/emcd/python-ictr>`_ (`ictr <https://pypi.org/project/ictr/>`_ on PyPI)
270
274
 
271
- 📨 A command-line tool for **exchanging collections of files with Large Language Models** - bundle multiple files into a single clipboard-ready document while preserving directory structure and metadata... good for code reviews, project sharing, and LLM interactions.
275
+ 🍦 **Non-Intrusive Debugging** - A Python library which provides flavored traces, configuration hierarchies, customized outputs, and ready-made recipes for logging and debug printing.
@@ -13,7 +13,7 @@ description = 'Detects textual content.'
13
13
  dynamic = [ 'version' ]
14
14
  license = 'Apache-2.0'
15
15
  readme = { 'file' = 'README.rst', 'content-type' = 'text/x-rst' }
16
- requires-python = '>= 3.10'
16
+ requires-python = '>= 3.12'
17
17
  dependencies = [
18
18
  'accretive~=4.1',
19
19
  'chardet',
@@ -31,13 +31,11 @@ classifiers = [ # https://pypi.org/classifiers
31
31
  'License :: OSI Approved :: Apache Software License',
32
32
  'Programming Language :: Python :: 3 :: Only',
33
33
  # --- BEGIN: Injected by Copier ---
34
- 'Programming Language :: Python :: 3.10',
35
- 'Programming Language :: Python :: 3.11',
36
34
  'Programming Language :: Python :: 3.12',
37
35
  'Programming Language :: Python :: 3.13',
38
36
  'Programming Language :: Python :: 3.14',
37
+ 'Programming Language :: Python :: 3.15',
39
38
  'Programming Language :: Python :: Implementation :: CPython',
40
- 'Programming Language :: Python :: Implementation :: PyPy',
41
39
  # --- END: Injected by Copier ---
42
40
  'Topic :: Software Development',
43
41
  ]
@@ -103,7 +101,7 @@ strict-naming = false
103
101
  # --- BEGIN: Injected by Copier ---
104
102
  # --- END: Injected by Copier ---
105
103
  [tool.hatch.envs.default]
106
- python = '3.10'
104
+ python = '3.12'
107
105
  [tool.hatch.envs.develop]
108
106
  description = ''' Development environment. '''
109
107
  builder = true
@@ -113,7 +111,6 @@ dependencies = [
113
111
  'furo',
114
112
  'isort',
115
113
  'myst-parser',
116
- 'pre-commit',
117
114
  'pyright',
118
115
  'pytest',
119
116
  'ruff',
@@ -141,13 +138,23 @@ docsgen = [
141
138
  """sphinx-build -a -d .auxiliary/caches/sphinx --quiet \
142
139
  documentation .auxiliary/artifacts/sphinx-html""",
143
140
  ]
141
+ fragments = [
142
+ """python .auxiliary/scripts/validate-fragments.py""",
143
+ ]
144
+ module-sizes = [
145
+ """python .auxiliary/scripts/validate-module-sizes.py""",
146
+ ]
144
147
  linters = [
145
148
  """ruff check --quiet sources documentation tests""",
146
- """vibelinter check""",
149
+ # External install only (e.g. uv tool install): a develop dependency
150
+ # cycles for packages below emcd-vibe-linter in the stack.
151
+ #"""vibelinter check""",
147
152
  # --- BEGIN: Injected by Copier ---
148
153
  # --- END: Injected by Copier ---
149
154
  """isort --check-only --diff sources tests""",
150
155
  """pyright sources""",
156
+ 'fragments',
157
+ 'module-sizes',
151
158
  ]
152
159
  packagers = [
153
160
  """hatch build""",
@@ -184,13 +191,10 @@ description = ''' Quality assurance environment. '''
184
191
  template = 'develop'
185
192
  [[tool.hatch.envs.qa.matrix]]
186
193
  python = [
187
- '3.10',
188
- '3.11',
189
194
  '3.12',
190
195
  '3.13',
191
196
  '3.14',
192
- 'pypy3.10',
193
- 'pypy3.11',
197
+ '3.15',
194
198
  ]
195
199
  [tool.hatch.version]
196
200
  path = 'sources/detextive/__init__.py'
@@ -37,7 +37,7 @@ from . import exceptions
37
37
  # --- END: Injected by Copier ---
38
38
 
39
39
 
40
- __version__ = '3.0'
40
+ __version__ = '3.2'
41
41
 
42
42
 
43
43
  __.immut.finalize_module( __name__, recursive = True )
@@ -35,7 +35,7 @@ from .core import ( # isort: skip
35
35
  )
36
36
 
37
37
 
38
- def attempt_decodes( # noqa: C901,PLR0912,PLR0913,PLR0915
38
+ def attempt_decodes( # noqa: PLR0913
39
39
  content: _nomina.Content, /, *,
40
40
  behaviors: _Behaviors = _BEHAVIORS_DEFAULT,
41
41
  inference: __.Absential[ str ] = __.absent,
@@ -52,29 +52,20 @@ def attempt_decodes( # noqa: C901,PLR0912,PLR0913,PLR0915
52
52
  '''
53
53
  confidence = _core.confidence_from_bytes_quantity(
54
54
  content, behaviors = behaviors )
55
- on_decode_error = behaviors.on_decode_error
55
+ trial_codecs = _collect_trial_codecs(
56
+ content,
57
+ behaviors = behaviors,
58
+ inference = inference,
59
+ supplement = supplement )
56
60
  trials: set[ str ] = set( )
57
- for codec in behaviors.trial_codecs:
58
- match codec:
59
- case _CodecSpecifiers.FromInference:
60
- if __.is_absent( inference ): continue
61
- charset = inference
62
- case _CodecSpecifiers.OsDefault:
63
- charset = discover_os_charset_default( )
64
- case _CodecSpecifiers.PythonDefault:
65
- charset = __.locale.getpreferredencoding( )
66
- case _CodecSpecifiers.UserSupplement:
67
- if __.is_absent( supplement ): continue
68
- charset = supplement
69
- case str( ): charset = codec
70
- case _: continue
71
- charset = normalize_charset(
72
- charset, bom_cognizant = behaviors.remove_bom )
73
- if charset in trials: continue
74
- try: text = content.decode( charset, errors = on_decode_error )
61
+ for trial_codec in trial_codecs:
62
+ try: text = content.decode(
63
+ trial_codec, errors = behaviors.on_decode_error )
75
64
  except UnicodeDecodeError: continue
76
- finally: trials.add( charset )
77
- result = _CharsetResult( charset = charset, confidence = confidence )
65
+ finally: trials.add( trial_codec )
66
+ result = _CharsetResult(
67
+ charset = normalize_charset_for_content( content, trial_codec ),
68
+ confidence = confidence )
78
69
  if not __.is_absent( validator ):
79
70
  try: validator( text, result )
80
71
  except _exceptions.TextInvalidity: continue
@@ -97,6 +88,23 @@ def normalize_charset( charset: str, bom_cognizant: bool = False ) -> str:
97
88
  return charset_
98
89
 
99
90
 
91
+ def normalize_charset_for_content(
92
+ content: _nomina.Content, charset: str
93
+ ) -> str:
94
+ ''' Normalizes charset reporting based on byte-order mark provenance. '''
95
+ charset_ = normalize_charset( charset )
96
+ bom_charset = _discover_utf_bom_charset( content )
97
+ if charset_ in ( 'utf-8', 'utf-8-sig' ):
98
+ if bom_charset == 'utf-8-sig': return 'utf-8-sig'
99
+ return 'utf-8'
100
+ for family in ( 'utf-16', 'utf-32' ):
101
+ if not charset_.startswith( family ): continue
102
+ if bom_charset in ( f"{family}-le", f"{family}-be" ):
103
+ return family
104
+ return charset_
105
+ return charset_
106
+
107
+
100
108
  def trial_decode_as_confident( # noqa: PLR0913
101
109
  content: _nomina.Content, /, *,
102
110
  behaviors: _Behaviors = _BEHAVIORS_DEFAULT,
@@ -126,3 +134,68 @@ def trial_decode_as_confident( # noqa: PLR0913
126
134
  if __.is_absent( inference ):
127
135
  raise _exceptions.CharsetDetectFailure( location = location )
128
136
  return _CharsetResult( charset = inference, confidence = confidence )
137
+
138
+
139
+ def _collect_trial_codecs(
140
+ content: _nomina.Content, /, *,
141
+ behaviors: _Behaviors,
142
+ inference: __.Absential[ str ],
143
+ supplement: __.Absential[ str ],
144
+ ) -> tuple[ str, ... ]:
145
+ codecs: list[ str ] = [ ] # No set needed; this candidate list is tiny.
146
+ for codec in behaviors.trial_codecs:
147
+ charset = _resolve_trial_codec(
148
+ codec, inference = inference, supplement = supplement )
149
+ if __.is_absent( charset ): continue
150
+ charset = normalize_charset( charset )
151
+ if _is_ambiguous_utf_trial( content, charset, behaviors ): continue
152
+ if behaviors.remove_bom and charset == 'utf-8': charset = 'utf-8-sig'
153
+ if charset in codecs: continue
154
+ codecs.append( charset )
155
+ return tuple( codecs )
156
+
157
+
158
+ def _discover_utf_bom_charset(
159
+ content: _nomina.Content
160
+ ) -> __.typx.Optional[ str ]:
161
+ # Must check UTF-32 markers first, since they prefix-match UTF-16 markers.
162
+ if content.startswith( __.codecs.BOM_UTF32_LE ): return 'utf-32-le'
163
+ if content.startswith( __.codecs.BOM_UTF32_BE ): return 'utf-32-be'
164
+ if content.startswith( __.codecs.BOM_UTF8 ): return 'utf-8-sig'
165
+ if content.startswith( __.codecs.BOM_UTF16_LE ): return 'utf-16-le'
166
+ if content.startswith( __.codecs.BOM_UTF16_BE ): return 'utf-16-be'
167
+ return None
168
+
169
+
170
+ def _is_ambiguous_utf_trial(
171
+ content: _nomina.Content, charset: str, behaviors: _Behaviors
172
+ ) -> bool:
173
+ if not behaviors.utf_16_32_requires_byte_order: return False
174
+ match charset:
175
+ case 'utf-16':
176
+ bom_charset = _discover_utf_bom_charset( content )
177
+ return bom_charset not in ( 'utf-16-le', 'utf-16-be' )
178
+ case 'utf-32':
179
+ bom_charset = _discover_utf_bom_charset( content )
180
+ return bom_charset not in ( 'utf-32-le', 'utf-32-be' )
181
+ case _: return False
182
+
183
+
184
+ def _resolve_trial_codec(
185
+ codec: __.typx.Any, /, *,
186
+ inference: __.Absential[ str ],
187
+ supplement: __.Absential[ str ],
188
+ ) -> __.Absential[ str ]:
189
+ charset: __.Absential[ str ] = __.absent
190
+ match codec:
191
+ case _CodecSpecifiers.FromInference:
192
+ if not __.is_absent( inference ): charset = inference
193
+ case _CodecSpecifiers.OsDefault:
194
+ charset = discover_os_charset_default( )
195
+ case _CodecSpecifiers.PythonDefault:
196
+ charset = __.locale.getpreferredencoding( )
197
+ case _CodecSpecifiers.UserSupplement:
198
+ if not __.is_absent( supplement ): charset = supplement
199
+ case str( ): charset = codec
200
+ case _: pass
201
+ return charset
@@ -128,6 +128,12 @@ class Behaviors( __.immut.DataclassObject ):
128
128
  trial_decode_confidence: __.typx.Annotated[
129
129
  float, __.ddoc.Doc( ''' Minimum confidence to skip trial decode. ''')
130
130
  ] = 0.80
131
+ utf_16_32_requires_byte_order: __.typx.Annotated[
132
+ bool,
133
+ __.ddoc.Doc(
134
+ ''' Require explicit byte order for BOM-less generic UTF-16/32? '''
135
+ ),
136
+ ] = False
131
137
 
132
138
  def __post_init__( self ) -> None:
133
139
  if not isinstance( self.charset_detect, bool ):
@@ -136,6 +142,9 @@ class Behaviors( __.immut.DataclassObject ):
136
142
  if not isinstance( self.mimetype_detect, bool ):
137
143
  raise _exceptions.BehaviorsInvalidity(
138
144
  'mimetype_detect', 'a boolean' )
145
+ if not isinstance( self.utf_16_32_requires_byte_order, bool ):
146
+ raise _exceptions.BehaviorsInvalidity(
147
+ 'utf_16_32_requires_byte_order', 'a boolean' )
139
148
 
140
149
 
141
150
  BehaviorsArgument: __.typx.TypeAlias = __.typx.Annotated[
@@ -160,15 +160,16 @@ def _decode_content_charset_result( # noqa: PLR0913
160
160
  if not __.is_absent( httpct_result ): return httpct_result
161
161
  behaviors_ = __.dcls.replace(
162
162
  behaviors, trial_decode = _BehaviorTristate.Never )
163
- with __.ctxl.suppress( _exceptions.CharsetDetectFailure ):
164
- result = _detectors.detect_charset_confidence(
165
- content,
166
- behaviors = behaviors_,
167
- supplement = charset_supplement,
168
- location = location )
169
- if ( result.charset
170
- and result.confidence >= behaviors.trial_decode_confidence
171
- ): charset = result.charset
163
+ if behaviors.charset_detect:
164
+ with __.ctxl.suppress( _exceptions.CharsetDetectFailure ):
165
+ result = _detectors.detect_charset_confidence(
166
+ content,
167
+ behaviors = behaviors_,
168
+ supplement = charset_supplement,
169
+ location = location )
170
+ if ( result.charset
171
+ and result.confidence >= behaviors.trial_decode_confidence
172
+ ): charset = result.charset
172
173
  validator = __.funct.partial(
173
174
  _validate_text_in_decode_attempt,
174
175
  behaviors = behaviors,
@@ -340,11 +340,9 @@ mimetype_detectors[ 'puremagic' ] = _detect_via_puremagic
340
340
 
341
341
 
342
342
  def _normalize_charset_detection(
343
- content: _nomina.Content, behaviors: _Behaviors, result: _CharsetResult
343
+ content: _nomina.Content, _behaviors: _Behaviors, result: _CharsetResult
344
344
  ) -> _CharsetResult:
345
345
  if result.charset is None: return result # pragma: no cover
346
- charset = _charsets.normalize_charset( result.charset )
347
- # TODO? Consider endianness variations for BOM.
348
- if charset == 'utf-8-sig' and not content.startswith( __.codecs.BOM ):
349
- charset = 'utf-8'
346
+ charset = _charsets.normalize_charset_for_content(
347
+ content, result.charset )
350
348
  return _CharsetResult( charset = charset, confidence = result.confidence )
File without changes