diff --git a/.python-version b/.python-version index 424e179..3e72aa6 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.6.8 +3.11.10 diff --git a/.travis.yml b/.travis.yml index c2d6da6..e9c3b25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: python python: - - "3.6.5" + - "3.11.10" # command to install dependencies -install: "pip install -r requirements-dev.txt" +install: "make dev" # command to run tests script: - - flake8 email_parser tests - - nosetests --with-coverage --cover-inclusive --cover-erase --cover-package=email_parser --cover-min-percentage=70 + - make test + - make coverage diff --git a/Makefile b/Makefile index d647a49..32f2607 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,13 @@ PYTHON=venv/bin/python3 PIP=venv/bin/pip EI=venv/bin/easy_install -NOSE=venv/bin/nosetests +TEST_RUNNER=venv/bin/pytest FLAKE=venv/bin/flake8 EMAILS_TEMPLATES_URI=git@github.com:KeepSafe/emails.git EMAILS_PATH=emails GUI_BIN=ks-email-parser -FLAGS=--with-coverage --cover-inclusive --cover-erase --cover-package=email_parser --cover-min-percentage=70 +TEST_RUNNER_FLAGS=-s --durations=3 --durations-min=0.005 +COVERAGE=venv/bin/coverage PYPICLOUD_HOST=pypicloud.getkeepsafe.local TWINE=./venv/bin/twine @@ -39,17 +40,16 @@ flake: $(FLAKE) email_parser tests test: flake - $(NOSE) -s $(FLAGS) + $(COVERAGE) run -m pytest $(TEST_RUNNER_FLAGS) vtest: - $(NOSE) -s -v $(FLAGS) + $(COVERAGE) run -m pytest -v $(TEST_RUNNER_FLAGS) testloop: - while sleep 1; do $(NOSE) -s $(FLAGS); done + while sleep 1; do $(TEST_RUNNER) -s --lf $(TEST_RUNNER_FLAGS); done cov cover coverage: - $(NOSE) -s --with-cover --cover-html --cover-html-dir ./coverage $(FLAGS) - echo "open file://`pwd`/coverage/index.html" + $(COVERAGE) report -m clean: rm -rf `find . -name __pycache__` diff --git a/email_parser/__init__.py b/email_parser/__init__.py index b0c4d92..d4fc77e 100644 --- a/email_parser/__init__.py +++ b/email_parser/__init__.py @@ -76,7 +76,7 @@ def get_email_components(self, email_name, locale): def get_email_variants(self, email_name): email = fs.email(self.root_path, email_name, const.DEFAULT_LOCALE) _, placeholders = reader.read(self.root_path, email) - variants = set([name for _, p in placeholders.items() for name in p.variants.keys()]) + variants = {name for _, p in placeholders.items() for name in p.variants.keys()} return list(variants) def delete_email(self, email_name): diff --git a/email_parser/cmd.py b/email_parser/cmd.py index b2aa78d..0505ec2 100644 --- a/email_parser/cmd.py +++ b/email_parser/cmd.py @@ -27,7 +27,7 @@ class ProgressConsoleHandler(logging.StreamHandler): def __init__(self, err_queue, warn_queue, *args, **kwargs): self.err_msgs_queue = err_queue self.warn_msgs_queue = warn_queue - super(ProgressConsoleHandler, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _store_msg(self, msg, loglevel): if loglevel == logging.ERROR: @@ -60,7 +60,7 @@ def _flush_store(self, stream, msgs, header): stream.write(header) stream.write(self.terminator) for idx, msg in enumerate(msgs): - stream.write('%s. %s' % (idx + 1, msg)) + stream.write(f'{idx + 1}. {msg}') stream.write(self.terminator) def _flush_errors(self, stream): diff --git a/email_parser/fs.py b/email_parser/fs.py index 51f9a32..381e335 100644 --- a/email_parser/fs.py +++ b/email_parser/fs.py @@ -18,10 +18,10 @@ def _parse_params(pattern): params = [p for p in map(lambda e: e[1], Formatter().parse(pattern)) if p] if 'name' not in params: raise MissingPatternParamError( - '{{name}} is a required parameter in the pattern but it is not present in {}'.format(pattern)) + f'{{{{name}}}} is a required parameter in the pattern but it is not present in {pattern}') if 'locale' not in params: raise MissingPatternParamError( - '{{locale}} is a required parameter in the pattern but it is not present in {}'.format(pattern)) + f'{{{{locale}}}} is a required parameter in the pattern but it is not present in {pattern}') return params diff --git a/email_parser/link_shortener.py b/email_parser/link_shortener.py index b05cf41..84519cc 100644 --- a/email_parser/link_shortener.py +++ b/email_parser/link_shortener.py @@ -1,7 +1,7 @@ import requests -class NullShortener(object): +class NullShortener: name = 'null' def __init__(self, config): @@ -11,7 +11,7 @@ def shorten(self, link): return link -class KsShortener(object): +class KsShortener: name = 'keepsafe' url = 'http://4uon.ly/url/' diff --git a/email_parser/model.py b/email_parser/model.py index 0c7864b..967b900 100644 --- a/email_parser/model.py +++ b/email_parser/model.py @@ -55,11 +55,11 @@ def __iter__(self): if self._opt_attr: attributes.update(self._opt_attr) for k, v in attributes.items(): - if k is 'type': + if k == 'type': yield k, self.type.value - elif k is '_content': + elif k == '_content': yield 'content', v - elif k is '_opt_attr': + elif k == '_opt_attr': continue else: yield k, v @@ -106,10 +106,10 @@ def get_content(self, variant=None): div_style = "vertical-align: middle;text-align: center;" constraints = "" if self.alt: - optional += " alt=\"{}\"".format(self.alt) + optional += f" alt=\"{self.alt}\"" for style_tag in ['max-width', 'max-height']: if style_tag in mapping: - constraints += "{}: {};".format(style_tag, mapping[style_tag]) + constraints += f"{style_tag}: {mapping[style_tag]};" mapping.update({ 'style': div_style + constraints, 'id': self.id, diff --git a/email_parser/reader.py b/email_parser/reader.py index bff57f8..55218a3 100644 --- a/email_parser/reader.py +++ b/email_parser/reader.py @@ -33,7 +33,7 @@ def parse_placeholder(placeholder_str): attr_name, attr_value = attr_str.split('=') args[attr_name] = attr_value except ValueError: - ValueError('Malformed attributes definition: %s'.format(args_str)) + ValueError(f'Malformed attributes definition: {args_str}') return MetaPlaceholder(name, placeholder_type, args) @@ -43,7 +43,7 @@ def _placeholders(tree, prefix=''): is_global = (prefix == const.GLOBALS_PLACEHOLDER_PREFIX) result = OrderedDict() for element in tree.xpath('./string | ./string-array | bitmap | ./array'): - name = '{0}{1}'.format(prefix, element.get('name')) + name = '{}{}'.format(prefix, element.get('name')) placeholder_type = PlaceholderType[element.get('type', PlaceholderType.text.value)] opt_attrs = dict(element.items()) del opt_attrs['name'] @@ -176,12 +176,12 @@ def _read_xml(path): def _read_xml_from_content(content): if not content: return None + parser = etree.XMLParser(encoding='utf-8') try: - parser = etree.XMLParser(encoding='utf-8') root = etree.fromstring(content.encode('utf-8'), parser=parser) return etree.ElementTree(root) - except etree.ParseError as e: - logger.exception('Unable to parse XML content %s %s', content, e) + except (etree.ParseError, etree.XMLSyntaxError): + logger.error('Unable to parse XML content %s', content) return None except TypeError: # got None? no results diff --git a/email_parser/renderer.py b/email_parser/renderer.py index 57702e8..18a5822 100644 --- a/email_parser/renderer.py +++ b/email_parser/renderer.py @@ -27,7 +27,7 @@ def _md_to_html(text, base_url=None): def _split_subject(placeholders): return (placeholders.get(const.SUBJECT_PLACEHOLDER), - dict((k, v) for k, v in placeholders.items() if k != const.SUBJECT_PLACEHOLDER)) + {k: v for k, v in placeholders.items() if k != const.SUBJECT_PLACEHOLDER}) def _transform_extended_tags(content): @@ -35,7 +35,7 @@ def _transform_extended_tags(content): return re.sub(regex, lambda match: '{{%s}}' % match.group(2), content) -class HtmlRenderer(object): +class HtmlRenderer: """ Renders email' body as html. """ @@ -107,7 +107,7 @@ def _concat_parts(self, subject, parts, variant): content = _transform_extended_tags(self.template.content) return renderer.render(content, placeholders) except pystache.context.KeyNotFoundError as e: - message = 'template %s for locale %s has missing placeholders: %s' % (self.template.name, self.locale, e) + message = f'template {self.template.name} for locale {self.locale} has missing placeholders: {e}' raise MissingTemplatePlaceholderError(message) from e def render(self, placeholders, variant=None, highlight=None): @@ -118,7 +118,7 @@ def render(self, placeholders, variant=None, highlight=None): return html -class TextRenderer(object): +class TextRenderer: """ Renders email's body as text. """ @@ -138,7 +138,7 @@ def _html_to_text(self, html): href = anchor.get('href') or text # href = self.shortener.shorten(href) if href != text: - anchor.replace_with('{} ({})'.format(text, href)) + anchor.replace_with(f'{text} ({href})') elif href: anchor.replace_with(href) @@ -151,7 +151,7 @@ def _html_to_text(self, html): ordered_lists = soup('ol') for ordered_list in ordered_lists: for idx, element in enumerate(ordered_list('li')): - element.replace_with('%s. %s' % (idx + 1, element.string)) + element.replace_with(f'{idx + 1}. {element.string}') return soup.get_text() @@ -167,7 +167,7 @@ def render(self, placeholders, variant=None): return const.TEXT_EMAIL_PLACEHOLDER_SEPARATOR.join(v for v in filter(bool, parts)) -class SubjectRenderer(object): +class SubjectRenderer: """ Renders email's subject as text. """ @@ -190,7 +190,7 @@ def render(email_locale, template, placeholders, variant=None, highlight=None): try: html = html_renderer.render(placeholders, variant, highlight) except MissingTemplatePlaceholderError as e: - message = 'failed to generate html content for locale: {} with message: {}'.format(email_locale, e) + message = f'failed to generate html content for locale: {email_locale} with message: {e}' raise RenderingError(message) from e return subject, text, html diff --git a/setup.cfg b/setup.cfg index f64a1ca..fd7790d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,15 @@ [metadata] -description-file = README.md +description_file = README.md [flake8] -max-line-length = 120 +max_line_length = 120 ignore = F403, F405, F401 [pep8] -max-line-length = 120 +max_line_length = 120 + +[coverage:run] +branch = True + +[coverage:report] +fail_under = 85 diff --git a/setup.py b/setup.py index 4efd8de..b38367f 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,21 @@ import os from setuptools import setup, find_packages -version = '0.3.2' +version = '1.0.0' install_requires = [ 'Markdown < 3', 'beautifulsoup4 < 5', 'inlinestyler==0.2.1', - 'pystache < 0.6', + 'pystache < 0.7', + 'lxml < 5', 'parse < 2' ] tests_require = [ - 'nose', - 'flake8==2.5.4', - 'coverage', + 'pytest >= 8', + 'coverage >= 7', + 'flake8 < 4', ] devtools_require = [ diff --git a/tests/test_fs.py b/tests/test_fs.py index c0c004a..3b67c76 100644 --- a/tests/test_fs.py +++ b/tests/test_fs.py @@ -4,7 +4,7 @@ from email_parser.model import * -class MockPath(object): +class MockPath: def __init__(self, path, is_dir=False, parent='.'): self.path = path self._is_dir = is_dir @@ -29,6 +29,16 @@ def __str__(self): return self.path +class TestUtilities(TestCase): + def test__parse_params_exceptions(self): + with self.assertRaises(MissingPatternParamError): + fs._parse_params('test') + with self.assertRaises(MissingPatternParamError): + fs._parse_params('test {name}') + with self.assertRaises(MissingPatternParamError): + fs._parse_params('test {locale}') + + class TestFs(TestCase): def setUp(self): self.patch_path = patch('email_parser.fs.Path') diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 1d0beea..b2d0502 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -199,7 +199,8 @@ def test_rtl_locale(self): placeholders = {'content': Placeholder('content', 'dummy_content')} actual = r.render(placeholders) - self.assertEqual('
\n\n dummy_content\n
\n', actual) + expected = '\n\n dummy_content\n
\n\n' + self.assertEqual(expected, actual) def test_rtl_two_placeholders(self): email_locale = 'ar' @@ -214,7 +215,7 @@ def test_rtl_two_placeholders(self): actual = r.render(placeholders) expected = '\n\n dummy_content1\n
\n\n dummy_content2\n
\n