diff --git a/mt940/__about__.py b/mt940/__about__.py index ab22519..4b6e192 100644 --- a/mt940/__about__.py +++ b/mt940/__about__.py @@ -1,11 +1,9 @@ __title__ = 'MT940' __package_name__ = 'mt-940' __author__ = 'Rick van Hattem (wolph)' -__description__ = ' '.join( - """ -A library to parse MT940 files and returns smart Python collections for -statistics and manipulation. -""".strip().split() +__description__ = ( + 'A library to parse MT940 files and returns smart Python collections for ' + 'statistics and manipulation.' ) __email__ = 'wolph@wol.ph' __version__ = '4.30.0' diff --git a/mt940/__init__.py b/mt940/__init__.py index bdac26b..63070e7 100644 --- a/mt940/__init__.py +++ b/mt940/__init__.py @@ -1,5 +1,4 @@ - -from . import models, parser, processors, tags, utils +from . import json, models, parser, processors, tags, utils from .json import JSONEncoder parse = parser.parse diff --git a/mt940/_compat.py b/mt940/_compat.py deleted file mode 100644 index ed9e67d..0000000 --- a/mt940/_compat.py +++ /dev/null @@ -1,133 +0,0 @@ -import sys - -PY2 = sys.version_info[0] == 2 - - -def _identity(x): # pragma: no cover - return x - - -__all__ = [ - 'PY2', - 'BytesIO', - 'StringIO', - '_identity', - 'ascii_lowercase', - 'cmp', - 'configparser', - 'console_to_str', - 'imap', - 'input', - 'integer_types', - 'iteritems', - 'iterkeys', - 'itervalues', - 'izip', - 'number_types', - 'pickle', - 'range_type', - 'reraise', - 'string_types', - 'text_to_native', - 'text_type', - 'unichr', - 'urllib', - 'urlparse', - 'urlparse', - 'urlretrieve', -] - -if PY2: # pragma: no cover - unichr = unichr - text_type = unicode - string_types = (str, unicode) - integer_types = (int, long) - from urllib import urlretrieve - - def text_to_native(s, enc): - return s.encode(enc) - - def iterkeys(d): - return d.iterkeys() - - def itervalues(d): - return d.itervalues() - - def iteritems(d): - return d.iteritems() - - from itertools import imap, izip - - import ConfigParser as configparser - import cPickle as pickle - from StringIO import StringIO - from cStringIO import StringIO as BytesIO - - range_type = xrange - - cmp = cmp - - input = raw_input - from string import lower as ascii_lowercase - - import urlparse - - def console_to_str(s): - return s.decode('utf_8') - - exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') - -else: # pragma: no cover - unichr = chr - text_type = str - string_types = (str,) - integer_types = (int,) - - def text_to_native(s, enc): - return s - - def iterkeys(d): - return iter(d.keys()) - - def itervalues(d): - return iter(d.values()) - - def iteritems(d): - return iter(d.items()) - - import configparser - import pickle - from io import BytesIO, StringIO - - izip = zip - imap = map - range_type = range - - def cmp(a, b): - return (a > b) - (a < b) - - input = input - import urllib.parse as urllib - import urllib.parse as urlparse - from string import ascii_lowercase - from urllib.request import urlretrieve - - if getattr(sys, '__stdout__', None): - console_encoding = sys.__stdout__.encoding - else: - console_encoding = sys.stdout.encoding - - def console_to_str(s): - """From pypa/pip project, pip.backwardwardcompat. License MIT.""" - try: - return s.decode(console_encoding) - except UnicodeDecodeError: - return s.decode('utf_8') - - def reraise(tp, value, tb=None): - if value.__traceback__ is not tb: - raise (value.with_traceback(tb)) - raise value - - -number_types = (*integer_types, float) diff --git a/mt940/json.py b/mt940/json.py index 9ec6933..69a8cc5 100644 --- a/mt940/json.py +++ b/mt940/json.py @@ -1,12 +1,13 @@ import datetime import decimal import json +from typing import Any from . import models class JSONEncoder(json.JSONEncoder): - def default(self, value): + def default(self, o: Any) -> Any: # The following types should simply be cast to strings str_types = ( datetime.date, @@ -22,25 +23,25 @@ def default(self, value): ) # Handle native types that should be converted to strings - if isinstance(value, str_types): - return str(value) + if isinstance(o, str_types): + return str(o) # Handling of the Transaction objects to include the actual # transactions - elif isinstance(value, models.Transactions): - data = value.data.copy() - data['transactions'] = value.transactions + elif isinstance(o, models.Transactions): + data = o.data.copy() + data['transactions'] = o.transactions return data # If an object has a `data` attribute, return that instead of the # `__dict__` ro prevent loops - elif hasattr(value, 'data'): - return value.data + elif hasattr(o, 'data'): + return o.data # Handle types that have a `__dict__` containing the data (doesn't work # for classes using `__slots__` such as `datetime`) - elif isinstance(value, dict_types): - return value.__dict__ + elif isinstance(o, dict_types): + return o.__dict__ else: # pragma: no cover - return json.JSONEncoder.default(self, value) + return super().default(o) diff --git a/mt940/models.py b/mt940/models.py index 4877966..76c7130 100644 --- a/mt940/models.py +++ b/mt940/models.py @@ -10,7 +10,7 @@ import mt940 -from . import _compat, processors +from . import processors class Model: @@ -391,7 +391,9 @@ def sanitize_tag_id_matches(self, matches): tag_id = self.normalize_tag_id(match.group('tag')) # tag should be known - assert tag_id in self.tags, f'Unknown tag {tag_id!r} ' f'in line: {match.group(0)!r}' + assert tag_id in self.tags, ( + f'Unknown tag {tag_id!r} ' f'in line: {match.group(0)!r}' + ) # special treatment for long tag content with possible # bad line wrap which produces tag_id like line beginnings @@ -487,7 +489,7 @@ def parse(self, data): # Combine multiple results together as one string, Rabobank has # multiple :86: tags for a single transaction - for k, v in _compat.iteritems(result): + for k, v in result.items(): if k in transaction.data and hasattr(v, 'strip'): transaction.data[k] += f'\n{v.strip()}' else: @@ -509,7 +511,7 @@ def __repr__(self): self.__class__.__name__, ']['.join( '{}: {}'.format(k.replace('_balance', ''), v) - for k, v in _compat.iteritems(self.data) + for k, v in self.data.items() if k.endswith('balance') ), ) @@ -531,3 +533,12 @@ def __repr__(self): self.data.get('date'), self.data.get('amount'), ) + + +class TransactionsAndTransaction(Transactions, Transaction): + """ + Subclass of both Transactions and Transaction for scope definitions. + + This is useful for the non-swift data for example which can function both + as details for a transaction and for a collection of transactions. + """ diff --git a/mt940/parser.py b/mt940/parser.py index 953ff3e..c579789 100644 --- a/mt940/parser.py +++ b/mt940/parser.py @@ -1,5 +1,4 @@ """ - Format --------------------- @@ -14,7 +13,6 @@ - `Rabobank MT940`_ :: - [] = optional ! = fixed length a = Text @@ -25,12 +23,20 @@ n = Numeric """ +from __future__ import annotations + import os +from typing import Any import mt940 -def parse(src, encoding=None, processors=None, tags=None): +def parse( + src: Any, + encoding: str | None = None, + processors: dict[str, list[Any]] | None = None, + tags: dict[Any, Any] | None = None, +) -> mt940.models.Transactions: """ Parses mt940 data and returns transactions object @@ -39,9 +45,9 @@ def parse(src, encoding=None, processors=None, tags=None): :rtype: Transactions """ - def safe_is_file(filename): + def safe_is_file(filename: Any) -> bool: try: - return os.path.isfile(src) + return os.path.isfile(filename) except ValueError: # pragma: no cover return False diff --git a/mt940/processors.py b/mt940/processors.py index dcb2c08..0aeabaf 100644 --- a/mt940/processors.py +++ b/mt940/processors.py @@ -177,7 +177,7 @@ def _parse_mt940_details(detail_str, space=False): def _parse_mt940_gvcodes(purpose): result = {} - for key, value in GVC_KEYS.items(): + for value in GVC_KEYS.values(): result[value] = None tmp = {} diff --git a/mt940/tags.py b/mt940/tags.py index 40a70d2..a63be99 100644 --- a/mt940/tags.py +++ b/mt940/tags.py @@ -72,23 +72,10 @@ n = Numeric """ - +import enum import logging import re -try: - import enum -except ImportError: # pragma: no cover - - - class enum: - @staticmethod - def unique(*args, **kwargs): - return [] - - Enum = object - - from . import models logger = logging.getLogger(__name__) @@ -264,9 +251,7 @@ class NonSwift(Tag): Pattern: `2!n35x | *x` """ - class scope(models.Transaction, models.Transactions): - pass - + scope = models.TransactionsAndTransaction id = 'NS' pattern = r""" diff --git a/mt940/utils.py b/mt940/utils.py index a836d73..4bbe235 100644 --- a/mt940/utils.py +++ b/mt940/utils.py @@ -1,7 +1,8 @@ import enum +from typing import Any, Optional -def coalesce(*args): +def coalesce(*args: Any) -> Optional[Any]: """ Return the first non-None argument @@ -26,11 +27,11 @@ class Strip(enum.IntEnum): BOTH = 3 -def join_lines(string, strip=Strip.BOTH): +def join_lines(string: str, strip: Strip = Strip.BOTH) -> str: """ Join strings together and strip whitespace in between if needed """ - lines = [] + lines: list[str] = [] for line in string.splitlines(): if strip & Strip.RIGHT: diff --git a/mt940_tests/test_sta_parsing.py b/mt940_tests/test_sta_parsing.py index 58db718..bf717d6 100644 --- a/mt940_tests/test_sta_parsing.py +++ b/mt940_tests/test_sta_parsing.py @@ -23,12 +23,6 @@ logger = logging.getLogger(__name__) -try: # pragma: no cover - string_type = unicode -except NameError: - string_type = str - - def get_sta_files(): base_path = os.path.relpath(os.path.dirname(__file__)) for path, _dirs, files in os.walk(base_path): @@ -230,16 +224,16 @@ def test_parse(sta_file): # Test string and representation methods for v in transactions.data.values(): - string_type(v) + str(v) repr(v) # Test string and representation methods for transaction in transactions: repr(transaction) - string_type(transaction) + str(transaction) for v in transaction.data.values(): - string_type(v) + str(v) repr(v) # Compare transaction data diff --git a/pytest.ini b/pytest.ini index 5b33e5a..c778b74 100644 --- a/pytest.ini +++ b/pytest.ini @@ -15,7 +15,6 @@ addopts = flake8-ignore = docs/*.py ALL - mt940/_compat.py ALL *.py W391 norecursedirs = diff --git a/ruff.toml b/ruff.toml index ab644ef..06b8611 100644 --- a/ruff.toml +++ b/ruff.toml @@ -76,6 +76,7 @@ select = [ [lint.per-file-ignores] 'mt940_tests/*' = ['N802', 'N803'] +'mt940/processors.py' = ['N802', 'N803'] [lint.pydocstyle] convention = 'google'