detextive 3.0__tar.gz → 3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detextive
3
- Version: 3.0
3
+ Version: 3.1
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
@@ -37,7 +37,7 @@ from . import exceptions
37
37
  # --- END: Injected by Copier ---
38
38
 
39
39
 
40
- __version__ = '3.0'
40
+ __version__ = '3.1'
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[
@@ -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
File without changes
File without changes
File without changes