diff --git a/HISTORY.rst b/HISTORY.rst index dfc2b1b..c68f4b8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,7 @@ tnefparse 1.4.0 (unreleased) - drop Python 3.5 support (jugmac00) - add Python 3.9 support (jugmac00) - command-line support for zipped export of attachments (Beercow) +- introduce using type annotations (jugmac00) tnefparse 1.3.1 (2020-09-30) ============================= diff --git a/tnefparse/__init__.py b/tnefparse/__init__.py index cd12fad..289b935 100644 --- a/tnefparse/__init__.py +++ b/tnefparse/__init__.py @@ -1,12 +1,5 @@ import logging -import warnings from .tnef import TNEF, TNEFAttachment, TNEFObject # noqa: F401 logging.getLogger(__package__).addHandler(logging.NullHandler()) - - -def parseFile(fileobj): - "a convenience function that returns a TNEF object" - warnings.warn("parseFile will be deprecated after 1.3", DeprecationWarning) - return TNEF(fileobj.read()) diff --git a/tnefparse/cmdline.py b/tnefparse/cmdline.py index bc0b2b1..9bbdf01 100644 --- a/tnefparse/cmdline.py +++ b/tnefparse/cmdline.py @@ -50,7 +50,7 @@ help="extract a json dump of the tnef contents") -def tnefparse(): +def tnefparse() -> None: "command-line script" if len(sys.argv) == 1: @@ -86,7 +86,7 @@ def tnefparse(): try: print(" " + properties.CODE_TO_NAME[p.name]) except KeyError: - logger.warning("Unknown MAPI Property: %s" % hex(p.name)) + logger.warning("Unknown MAPI Property: 0x%x", p.name) print("") elif args.dump: @@ -108,7 +108,7 @@ def tnefparse(): sys.stderr.write("Successfully wrote attachments.zip\n") sys.exit() - def print_body(attr, description): + def print_body(attr: str, description: str) -> None: body = getattr(t, attr) if body is None: sys.exit("No %s found" % description) diff --git a/tnefparse/codepage.py b/tnefparse/codepage.py index 6584b97..02107ec 100644 --- a/tnefparse/codepage.py +++ b/tnefparse/codepage.py @@ -1,3 +1,5 @@ +from typing import Optional, Union + # https://docs.microsoft.com/en-us/windows/desktop/intl/code-page-identifiers CODEPAGE_MAP = { 20127: 'ascii', @@ -9,10 +11,10 @@ class Codepage: - def __init__(self, codepage): + def __init__(self, codepage: int) -> None: self.cp = codepage - def best_guess(self): + def best_guess(self) -> Optional[str]: if CODEPAGE_MAP.get(self.cp): return CODEPAGE_MAP.get(self.cp) elif self.cp <= 1258: # max cpXXXX page in python @@ -20,14 +22,14 @@ def best_guess(self): else: return None - def codepage(self): + def codepage(self) -> str: bg = self.best_guess() if bg: return bg else: return 'cp%d' % self.cp - def decode(self, byte_str): + def decode(self, byte_str: Union[str, bytes]) -> str: if isinstance(byte_str, bytes): return byte_str.decode(self.best_guess() or FALLBACK) else: diff --git a/tnefparse/mapi.py b/tnefparse/mapi.py index 1d5adc3..9878138 100644 --- a/tnefparse/mapi.py +++ b/tnefparse/mapi.py @@ -56,7 +56,7 @@ def decode_mapi(data, codepage='cp1252', starting_offset=None): try: for i in range(num_properties): if offset >= dataLen: - logger.warn("Skipping property '%i'" % i) + logger.warn("Skipping property %r", i) continue attr_type = uint16(data, offset) @@ -106,12 +106,9 @@ def decode_mapi(data, codepage='cp1252', starting_offset=None): if (num_mv_properties or 1) % 2 and attr_type in (SZMAPI_SHORT, SZMAPI_BOOLEAN): offset += 2 - except Exception as e: - import traceback - - stack = traceback.format_exc() - logger.error('decode_mapi Exception %s' % e) - logger.debug(stack) + except Exception as exc: + logger.error('decode_mapi exception: %s', exc) + logger.debug('exception details:', exc_info=True) if starting_offset is not None: return (offset, attrs) diff --git a/tnefparse/tnef.py b/tnefparse/tnef.py index 1bd5c0b..c854c58 100644 --- a/tnefparse/tnef.py +++ b/tnefparse/tnef.py @@ -35,7 +35,7 @@ def __init__(self, data, do_checksum=False): if do_checksum: calc_checksum = checksum(self.data) if calc_checksum != att_checksum: - logger.warning(f"Checksum: {calc_checksum} != {att_checksum}") + logger.warning("Checksum: %s != %s", calc_checksum, att_checksum) else: calc_checksum = att_checksum @@ -101,13 +101,13 @@ def __init__(self, codepage): self.data = b'' @property - def name(self): + def name(self) -> str: if isinstance(self._name, bytes): return self._name.decode().strip('\x00') else: return self._name.strip('\x00') - def long_filename(self): + def long_filename(self) -> str: atname = Attribute.MAPI_ATTACH_LONG_FILENAME name = [a.data for a in self.mapi_attrs if a.name == atname] if name: @@ -141,7 +141,7 @@ def add_attr(self, attribute): pass # this is a WMF file of some kind else: - logger.debug("Unknown attribute name: %s" % attribute) + logger.debug("Unknown attribute name: %s", attribute) def __str__(self): return "" % self.long_filename() @@ -319,9 +319,9 @@ def __init__(self, data, do_checksum=True): obj.data = typtime(obj.data) self.msgprops.append(obj) except ValueError: - logger.debug("TNEF Object not a valid date: %s" % obj) + logger.debug("TNEF Object not a valid date: %s", obj) else: - logger.debug("Unhandled TNEF Object: %s" % obj) + logger.debug("Unhandled TNEF Object: %s", obj) def has_body(self): return True if (self.body or self.htmlbody or self._rtfbody) else False @@ -330,7 +330,8 @@ def has_body(self): def rtfbody(self): if self._rtfbody: try: - from compressed_rtf import decompress + # compressed_rtf is not typed yet + from compressed_rtf import decompress # type: ignore return decompress(self._rtfbody + b'\x00') except ImportError: logger.warning("Returning compressed RTF. Install compressed_rtf to decompress") diff --git a/tnefparse/util.py b/tnefparse/util.py index 24e4a65..165c8b3 100644 --- a/tnefparse/util.py +++ b/tnefparse/util.py @@ -77,25 +77,23 @@ def raw_mapi(dataLen, data): while loop <= dataLen: if (loop + 16) < dataLen: logger.debug( - "%2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x" - % ( - ord(data[loop]), - ord(data[loop + 1]), - ord(data[loop + 2]), - ord(data[loop + 3]), - ord(data[loop + 4]), - ord(data[loop + 5]), - ord(data[loop + 6]), - ord(data[loop + 7]), - ord(data[loop + 8]), - ord(data[loop + 9]), - ord(data[loop + 10]), - ord(data[loop + 11]), - ord(data[loop + 12]), - ord(data[loop + 13]), - ord(data[loop + 14]), - ord(data[loop + 15]), - ) + "%2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x %2.2x%2.2x", + ord(data[loop]), + ord(data[loop + 1]), + ord(data[loop + 2]), + ord(data[loop + 3]), + ord(data[loop + 4]), + ord(data[loop + 5]), + ord(data[loop + 6]), + ord(data[loop + 7]), + ord(data[loop + 8]), + ord(data[loop + 9]), + ord(data[loop + 10]), + ord(data[loop + 11]), + ord(data[loop + 12]), + ord(data[loop + 13]), + ord(data[loop + 14]), + ord(data[loop + 15]), ) loop += 16 loop -= 16 @@ -106,4 +104,4 @@ def raw_mapi(dataLen, data): if i != 0 and subr == 0: strList.append(' ') strList.append('%2.2x' % ord(data[loop + i])) - logger.debug('%s' % ''.join(strList)) + logger.debug(''.join(strList)) diff --git a/tox.ini b/tox.ini index b040bc1..788dc17 100644 --- a/tox.ini +++ b/tox.ini @@ -43,6 +43,13 @@ deps = commands = check-manifest +[testenv:mypy] +deps = + mypy +commands = + # do not lint tests yet + mypy tnefparse {posargs} + [flake8] # The GitHub editor is 127 chars wide. max-line-length = 127