Skip to content

Commit 6b6febb

Browse files
committed
Remove old Python 2.7 compatibility, add 3.6+ compatible type hints
1 parent b56962d commit 6b6febb

File tree

2 files changed

+32
-44
lines changed

2 files changed

+32
-44
lines changed

docs/changelog.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ Changelog
55

66
*2025-XX-XX*
77

8-
- TODO: nullcontext requires Python 3.7+
9-
8+
- guano-py now requires Python 3.6+
109
- Support reading from any file-like object (one that implements `seek`, `read`, and
1110
`tell`) - Thanks to Jeff Gerard of Conservation Metrics
1211
- Add `--dry-run` option to `wamd2guano.py`

guano.py

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"""
1515

1616
import os
17-
import sys
1817
import wave
1918
import struct
2019
import os.path
@@ -25,14 +24,11 @@
2524
from collections import OrderedDict, namedtuple
2625
from base64 import standard_b64encode as base64encode
2726
from base64 import standard_b64decode as base64decode
27+
from typing import Any, BinaryIO, Callable, Iterable, Tuple, Union
2828

2929
import logging
3030
log = logging.Logger(__name__)
3131

32-
if sys.version_info[0] > 2:
33-
unicode = str
34-
basestring = str
35-
3632

3733
__version__ = '1.0.16.dev0'
3834

@@ -70,7 +66,7 @@ class tzoffset(tzinfo):
7066
"""
7167

7268
def __init__(self, offset):
73-
if isinstance(offset, basestring):
69+
if isinstance(offset, str):
7470
# offset as ISO string '-07:00', '-0700', or '-07' format
7571
if len(offset) < 4:
7672
vals = offset, '00' # eg '-07'
@@ -98,7 +94,7 @@ def __repr__(self):
9894
return self.tzname(None)
9995

10096

101-
def parse_timestamp(s):
97+
def parse_timestamp(s) -> datetime:
10298
"""
10399
Parse a string in supported subset of ISO 8601 / RFC 3331 format to :class:`datetime.datetime`.
104100
The timestamp will be timezone-aware of a TZ is specified, or timezone-naive if in "local" fmt.
@@ -183,7 +179,7 @@ class GuanoFile(object):
183179
'Timestamp': lambda value: value.isoformat() if value else '',
184180
}
185181

186-
def __init__(self, file=None, strict=False):
182+
def __init__(self, file: Union[str, BinaryIO] = None, strict=False):
187183
"""
188184
Create a GuanoFile instance which represents a single file's GUANO metadata.
189185
If the file already contains GUANO metadata, it will be parsed immediately. If not, then
@@ -199,12 +195,12 @@ def __init__(self, file=None, strict=False):
199195
:raises ValueError: if the specified file doesn't represent a valid .WAV or if its
200196
existing GUANO metadata is broken
201197
"""
202-
if isinstance(file, basestring):
198+
if isinstance(file, str):
203199
self.filename = file
204200
self._file = None
205201
else:
206202
self.filename = file.name if hasattr(file, 'name') else None
207-
self._file = file # a file-like object
203+
self._file: BinaryIO = file # a file-like object
208204

209205
self.strict_mode = strict
210206

@@ -218,7 +214,7 @@ def __init__(self, file=None, strict=False):
218214
if self._file or (self.filename and os.path.isfile(self.filename)):
219215
self._load()
220216

221-
def _coerce(self, key, value):
217+
def _coerce(self, key: str, value: str) -> Any:
222218
"""Coerce a value from its Unicode representation to a specific data type"""
223219
if key in self._coersion_rules:
224220
try:
@@ -230,9 +226,9 @@ def _coerce(self, key, value):
230226
log.warning('Failed coercing "%s": %s', key, e)
231227
return value # default should already be a Unicode string
232228

233-
def _serialize(self, key, value):
229+
def _serialize(self, key: str, value: Any) -> str:
234230
"""Serialize a value from its real representation to GUANO Unicode representation"""
235-
serialize = self._serialization_rules.get(key, unicode)
231+
serialize = self._serialization_rules.get(key, str)
236232
try:
237233
return serialize(value)
238234
except (ValueError, TypeError) as e:
@@ -292,7 +288,7 @@ def _load(self):
292288

293289
def _parse(self, metadata_str):
294290
"""Parse metadata and populate our internal mappings"""
295-
if not isinstance(metadata_str, unicode):
291+
if not isinstance(metadata_str, str):
296292
try:
297293
metadata_str = metadata_str.decode('utf-8')
298294
except UnicodeDecodeError as e:
@@ -314,7 +310,7 @@ def _parse(self, metadata_str):
314310
return self
315311

316312
@classmethod
317-
def from_string(cls, metadata_str, *args, **kwargs):
313+
def from_string(cls, metadata_str, *args, **kwargs) -> 'GuanoFile':
318314
"""
319315
Create a :class:`GuanoFile` instance from a GUANO metadata string
320316
@@ -328,7 +324,7 @@ def from_string(cls, metadata_str, *args, **kwargs):
328324
return GuanoFile(*args, **kwargs)._parse(metadata_str)
329325

330326
@classmethod
331-
def register(cls, namespace, keys, coerce_function, serialize_function=str):
327+
def register(cls, namespace: str, keys: Union[str, Iterable[str]], coerce_function: Callable, serialize_function: Callable = str):
332328
"""
333329
Configure the GUANO parser to recognize new namespaced keys.
334330
@@ -339,14 +335,14 @@ def register(cls, namespace, keys, coerce_function, serialize_function=str):
339335
:param serialize_function: an optional function for serializing the value to UTF-8 string
340336
:type serialize_function: callable
341337
"""
342-
if isinstance(keys, basestring):
338+
if isinstance(keys, str):
343339
keys = [keys]
344340
for k in keys:
345341
full_key = namespace+'|'+k if namespace else k
346342
cls._coersion_rules[full_key] = coerce_function
347343
cls._serialization_rules[full_key] = serialize_function
348344

349-
def _split_key(self, item):
345+
def _split_key(self, item) -> Tuple[str, str]:
350346
if isinstance(item, tuple):
351347
namespace, key = item[0], item[1]
352348
elif '|' in item:
@@ -355,11 +351,11 @@ def _split_key(self, item):
355351
namespace, key = '', item
356352
return namespace, key
357353

358-
def __getitem__(self, item):
354+
def __getitem__(self, item) -> Any:
359355
namespace, key = self._split_key(item)
360356
return self._md[namespace][key]
361357

362-
def get(self, item, default=None):
358+
def get(self, item, default=None) -> Any:
363359
try:
364360
return self[item]
365361
except KeyError:
@@ -375,7 +371,7 @@ def __setitem__(self, key, value):
375371
self._md[namespace] = {}
376372
self._md[namespace][key] = value
377373

378-
def __contains__(self, item):
374+
def __contains__(self, item) -> bool:
379375
namespace, key = self._split_key(item)
380376
return namespace in self._md and key in self._md[namespace]
381377

@@ -385,21 +381,20 @@ def __delitem__(self, key):
385381
if not self._md[namespace]:
386382
del self._md[namespace]
387383

388-
def __bool__(self):
384+
def __bool__(self) -> bool:
389385
return bool(self._md)
390-
__nonzero__ = __bool__ # py2
391386

392-
def __repr__(self):
393-
return '%s(%s)' % (self.__class__.__name__, self._file)
387+
def __repr__(self) -> str:
388+
return '%s(%s)' % (self.__class__.__name__, self.filename or self._file)
394389

395-
def get_namespaces(self):
390+
def get_namespaces(self) -> list:
396391
"""
397392
Get list of all namespaces represented by this metadata.
398393
This includes the 'GUANO' namespace, and the '' (empty string) namespace for well-known fields.
399394
"""
400-
return self._md.keys()
395+
return list(self._md.keys())
401396

402-
def items(self, namespace=None):
397+
def items(self, namespace: str = None) -> Iterable[Tuple[str, Any]]:
403398
"""Iterate over (key, value) for entire metadata or for specified namespace of fields"""
404399
if namespace is not None:
405400
for k, v in self._md[namespace].items():
@@ -410,17 +405,17 @@ def items(self, namespace=None):
410405
k = '%s|%s' % (namespace, k) if namespace else k
411406
yield k, v
412407

413-
def items_namespaced(self):
408+
def items_namespaced(self) -> Iterable[Tuple[str, str, Any]]:
414409
"""Iterate over (namespace, key, value) for entire metadata"""
415410
for namespace, data in self._md.items():
416411
for k, v in data.items():
417412
yield namespace, k, v
418413

419-
def well_known_items(self):
414+
def well_known_items(self) -> Iterable[Tuple[str, Any]]:
420415
"""Iterate over (key, value) for all the well-known (defined) fields"""
421416
return self.items('')
422417

423-
def to_string(self):
418+
def to_string(self) -> str:
424419
"""Represent the GUANO metadata as a Unicode string"""
425420
lines = []
426421
for namespace, data in self._md.items():
@@ -430,7 +425,7 @@ def to_string(self):
430425
lines.append(u'%s: %s' % (k, v))
431426
return u'\n'.join(lines)
432427

433-
def serialize(self, pad='\n'):
428+
def serialize(self, pad='\n') -> bytes:
434429
"""Serialize the GUANO metadata as UTF-8 encoded bytes"""
435430
md_bytes = bytearray(self.to_string(), 'utf-8')
436431
if pad is not None and len(md_bytes) % 2:
@@ -439,7 +434,7 @@ def serialize(self, pad='\n'):
439434
return md_bytes
440435

441436
@property
442-
def wav_data(self):
437+
def wav_data(self) -> bytes:
443438
"""Actual audio data from the wav `data` chunk. Lazily loaded and cached."""
444439
if not self._wav_data_size:
445440
raise ValueError()
@@ -452,7 +447,7 @@ def wav_data(self):
452447
return self._wav_data
453448

454449
@wav_data.setter
455-
def wav_data(self, data):
450+
def wav_data(self, data: bytes):
456451
self._wav_data_size = len(data)
457452
self._wav_data = data
458453

@@ -527,11 +522,5 @@ def __exit__(self, *excinfo):
527522
pass
528523

529524

530-
# This ugly hack prevents a warning if application-level code doesn't configure logging
531-
if sys.version_info[0] > 2:
532-
NullHandler = logging.NullHandler
533-
else:
534-
class NullHandler(logging.Handler):
535-
def emit(self, record):
536-
pass
537-
log.addHandler(NullHandler())
525+
# prevents a warning if application-level code doesn't configure logging
526+
log.addHandler(logging.NullHandler())

0 commit comments

Comments
 (0)