diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b6fcb12 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: Run CI + +# Run this workflow every time a new commit pushed to your repository +on: + push: + branches: + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.10', '3.11', '3.12'] + django: ['3.2', '4.2'] + + name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }}) + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install dependencies + run: pip install tox tox-gh-actions + + - name: Run tests + run: tox + env: + PYTHON_VERSION: ${{ matrix.python }} + DJANGO: ${{ matrix.django }} + + - name: Publish coverage report + uses: codecov/codecov-action@v3 + + publish: + name: Publish package to PyPI + runs-on: ubuntu-latest + needs: tests + + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Build sdist and wheel + run: | + pip install build --upgrade + python -m build + + # TODO: switch to verified publishers + - name: Publish a Python distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml new file mode 100644 index 0000000..0cc5c96 --- /dev/null +++ b/.github/workflows/code_quality.yml @@ -0,0 +1,34 @@ +name: Code quality checks + +# Run this workflow every time a new commit pushed to your repository +on: + push: + branches: + - master + tags: + - '*' + paths: + - '**.py' + pull_request: + paths: + - '**.py' + workflow_dispatch: + +jobs: + linting: + name: Code-quality checks + runs-on: ubuntu-latest + strategy: + matrix: + # toxenv: [isort, black, flake8, docs] + toxenv: [isort, black, flake8] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install dependencies + run: pip install tox + - run: tox + env: + TOXENV: ${{ matrix.toxenv }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 37c136c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: python -python: - - "3.5" - -services: - - postgresql - -addons: - postgresql: "9.4" - -install: - - pip install codecov coveralls tox -script: - - tox -- -after_success: - - coveralls - - codecov -env: - - TOXENV=py27-django18 - - TOXENV=py34-django18 - - TOXENV=py35-django18 - - - TOXENV=py27-django19 - - TOXENV=py34-django19 - - TOXENV=py35-django19 - - - TOXENV=py27-django110 - - TOXENV=py34-django110 - - TOXENV=py35-django110 diff --git a/License b/LICENSE similarity index 100% rename from License rename to LICENSE diff --git a/MANIFEST.in b/MANIFEST.in index ba979f0..aa032d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include License include README.rst recursive-include mail_editor/templates * recursive-include bin * +global-exclude __pycache__ +global-exclude *.py[co] diff --git a/README.rst b/README.rst index 73ebd0a..0cf7fd9 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,9 @@ MailEditor ========== -.. image:: https://travis-ci.org/maykinmedia/mail-editor.svg?branch=master - :target: https://travis-ci.org/maykinmedia/mail-editor - :alt: Travis .. image:: https://codecov.io/gh/maykinmedia/mail-editor/branch/develop/graph/badge.svg :target: https://codecov.io/gh/maykinmedia/mail-editor :alt: Coverage -.. image:: https://lintly.com/gh/maykinmedia/mail-editor/badge.svg - :target: https://lintly.com/gh/maykinmedia/mail-editor/ - :alt: Lintly -.. image:: https://bettercodehub.com/edge/badge/maykinmedia/mail-editor?branch=master - :target: https://bettercodehub.com/results/maykinmedia/mail-editor - :alt: BCH compliance .. image:: https://codeclimate.com/github/codeclimate/codeclimate/badges/gpa.svg :target: https://codeclimate.com/github/codeclimate/codeclimate :alt: Code Climate @@ -39,10 +30,10 @@ For e-mail sending and logging, we recommend using a solution such as `Django Yu This is only tested on a postgres database. -Supported (read: Travis tested) are: +Supported are: -- python 2.7, 3.4, 3.5 -- Django 1.8, 1.9, 1.10, 1.11 +- python 3.10, 3.11, 3.12 +- Django 3.2, 4.2 - PostgreSQL Warning @@ -120,8 +111,8 @@ The following settings are an example: MAIL_EDITOR_CONF = { 'activation': { - 'name': ugettext_noop('Activation Email'), - 'description': ugettext_noop('This email is used when people need to activate their account.'), + 'name': gettext_noop('Activation Email'), + 'description': gettext_noop('This email is used when people need to activate their account.'), 'subject_default': 'Activeer uw account voor {{site_name}}', 'body_default': """

Hallo {{ name }},

@@ -132,21 +123,21 @@ The following settings are an example: """, 'subject': [{ 'name': 'site_name', - 'description': ugettext_noop('This is the name of the site. From the sites'), - 'example': ugettext_noop('Example site'), + 'description': gettext_noop('This is the name of the site. From the sites'), + 'example': gettext_noop('Example site'), }], 'body': [{ 'name': 'name', - 'description': ugettext_noop('This is the name of the user'), - 'example': ugettext_noop('Jane Doe'), + 'description': gettext_noop('This is the name of the user'), + 'example': gettext_noop('Jane Doe'), }, { 'name': 'site_name', - 'description': ugettext_noop('This is the name of the site. From the sites'), - 'example': ugettext_noop('Example site'), + 'description': gettext_noop('This is the name of the site. From the sites'), + 'example': gettext_noop('Example site'), }, { 'name': 'activation_link', - 'description': ugettext_noop('This is the link to activate their account.'), - 'example': ugettext_noop('/'), + 'description': gettext_noop('This is the link to activate their account.'), + 'example': gettext_noop('/'), }] }, ... diff --git a/fix.sh b/fix.sh index fdb2e72..6499bcf 100755 --- a/fix.sh +++ b/fix.sh @@ -7,3 +7,4 @@ black ./mail_editor isort --profile black ./tests autoflake --in-place --remove-all-unused-imports -r ./tests black ./tests + diff --git a/mail_editor/mail_template.py b/mail_editor/mail_template.py index d8fb3b1..5cf581f 100644 --- a/mail_editor/mail_template.py +++ b/mail_editor/mail_template.py @@ -2,8 +2,6 @@ Defines helpers for validating e-mail templates """ -from __future__ import absolute_import, unicode_literals - from django.core.exceptions import ValidationError from django.template import ( # TODO: should be able to specify engine Context, @@ -63,7 +61,8 @@ def check_syntax_errors(self, value): def check_variables(self, template, field): variables_seen = set() required_vars = {var.name for var in self.config[field] if var.required} - optional_vars = {var.name for var in self.config[field] if not var.required} + # TODO do we need to check optional_vars? the following line was here but never used + # optional_vars = {var.name for var in self.config[field] if not var.required} for node in template.nodelist.get_nodes_by_type(VariableNode): var_name = node.filter_expression.var.var variables_seen.add(var_name) @@ -80,8 +79,8 @@ def check_variables(self, template, field): params={field: message}, message=message, code=self.code ) - def _is_attribute(self, vars, known_vars): - for var in vars: + def _is_attribute(self, var_names, known_vars): + for var in var_names: if any(var.startswith("{}.".format(known_var)) for known_var in known_vars): return True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..29da416 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,100 @@ +[build-system] +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "mail_editor" +version = "0.3.6" +description = "A Django package for email template editing" +authors = [ + {name = "Maykin Media", email = "support@maykinmedia.nl"} +] +readme = "README.rst" +license = {file = "LICENSE"} +keywords = ["TODO"] +classifiers = [ + "Development Status :: 3 - Alpha", + 'Environment :: Web Environment', + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Intended Audience :: Developers", + 'License :: OSI Approved :: BSD License', + "Operating System :: Unix", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + 'Programming Language :: Python', + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.10" +dependencies = [ + "django>=3.2", + "django-choices", + "django-ckeditor", + "requests", + "lxml", + "css_inline" +] + +[project.urls] +Homepage = "https://github.com/maykinmedia/mail-editor" +Documentation = "https://github.com/maykinmedia/mail-editor" +"Bug Tracker" = "https://github.com/maykinmedia/mail-editor/issues" +"Source Code" = "https://github.com/maykinmedia/mail-editor" + +[project.optional-dependencies] +tests = [ + "pytest", + "pytest-django", + "tox", + "isort", + "black", + "flake8", + "autoflake", + "django_webtest", +] +coverage = [ + "pytest-cov", +] +release = [ + "bump-my-version", + "twine", +] + +[tool.setuptools.packages.find] +include = ["mail_editor*"] +namespaces = false + +[tool.isort] +profile = "black" +combine_as_imports = true +known_django = "django" +known_first_party="mail_editor" +sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +DJANGO_SETTINGS_MODULE = "tests.settings" + +[tool.bumpversion] +current_version = "0.3.5" +files = [ + {filename = "pyproject.toml"}, + {filename = "README.rst"}, +] + +[tool.coverage.report] +exclude_also = [ + "if (typing\\.)?TYPE_CHECKING:", + "@(typing\\.)?overload", + "class .*\\(.*Protocol.*\\):", + "@(abc\\.)?abstractmethod", + "raise NotImplementedError", + "\\.\\.\\.", + "pass", +] diff --git a/setup.cfg b/setup.cfg index 288390b..44f7e3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,26 +6,12 @@ universal=1 [pep8] ignore=W293,W291,E501,E261 - max-line-length=120 - exclude=migrations,static,media -[isort] -combine_as_imports = true -default_section = THIRDPARTY -include_trailing_comma = false -line_length = 79 -multi_line_output = 5 -skip = env,node_modules -skip_glob = **/migrations/** -not_skip = __init__.py -known_django=django -known_first_party=mail_editor -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER - -[tool:pytest] -django_find_project=false -DJANGO_SETTINGS_MODULE=tests.settings +[flake8] +ignore=E203,E261,E501,E731,F405,W293,W291,W503,F841,E741 +max-line-length=120 +exclude=env,.tox python_paths=. diff --git a/setup.py b/setup.py deleted file mode 100644 index 1d58515..0000000 --- a/setup.py +++ /dev/null @@ -1,65 +0,0 @@ -import os - -from setuptools import find_packages, setup - -# allow setup.py to be run from any path -os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) - -setup( - name='mail_editor', - version='0.3.5', - license='BSD', - - # packaging - install_requires=[ - 'Django>=1.8', - 'django-choices', - 'django-ckeditor', - 'requests', - 'lxml', - 'css_inline', - ], - include_package_data=True, - scripts=[], - packages=find_packages(exclude=["tests"]), - - # tests - setup_requires=['pytest-runner'], - tests_require=[ - 'factory-boy', - 'psycopg2', - 'pytest', - 'pytest-cov', - 'pytest-django', - 'pytest-pep8', - 'pytest-pylint', - 'pytest-pythonpath', - 'django-webtest', - ], - - # metadata - description='A Django package for email template editing', - url='https://github.com/maykinmedia/mail_editor', - author='Sergei Maertens, Maykin Media, Jorik Kraaikamp', - author_email='sergei@maykinmedia.nl', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 1.8', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - ], -) diff --git a/tests/test_send.py b/tests/test_send.py index b227d46..96a923f 100644 --- a/tests/test_send.py +++ b/tests/test_send.py @@ -3,7 +3,7 @@ from django.core import mail from django.test import TestCase, override_settings -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from mail_editor.helpers import find_template diff --git a/tests/test_template_rendering.py b/tests/test_template_rendering.py index a3546ac..edcee0c 100644 --- a/tests/test_template_rendering.py +++ b/tests/test_template_rendering.py @@ -41,7 +41,7 @@ def test_simple(self): subject, body = template.render(body_context, subj_context=subject_context) - self.assertEquals(subject, "Important message for 111") + self.assertEqual(subject, "Important message for 111") self.assertIn("Test mail sent from testcase with 111", body) @override_settings(MAIL_EDITOR_CONF=CONFIG, MAIL_EDITOR_BASE_CONTEXT={"id": "BASE"}) @@ -53,7 +53,7 @@ def test_base_context(self): subject, body = template.render(body_context, subj_context=subject_context) - self.assertEquals(subject, "Important message for BASE") + self.assertEqual(subject, "Important message for BASE") self.assertIn("Test mail sent from testcase with BASE", body) @override_settings( @@ -68,7 +68,7 @@ def test_dynamic_context(self): subject, body = template.render(body_context, subj_context=subject_context) - self.assertEquals(subject, "Important message for DYNAMIC") + self.assertEqual(subject, "Important message for DYNAMIC") self.assertIn("Test mail sent from testcase with DYNAMIC", body) @override_settings(MAIL_EDITOR_CONF=CONFIG) @@ -81,7 +81,7 @@ def test_incorrect_base_path(self): subject, body = template.render(body_context, subj_context=subject_context) - self.assertEquals(subject, "Important message for 222") + self.assertEqual(subject, "Important message for 222") self.assertIn("Test mail sent from testcase with 222", body) @override_settings(MAIL_EDITOR_CONF=CONFIG) @@ -98,7 +98,7 @@ def test_base_template_errors(self): subject, body = template.render(body_context, subj_context=subject_context) - self.assertEquals(subject, "Important message for 333") + self.assertEqual(subject, "Important message for 333") self.assertIn("Test mail sent from testcase with 333", body) @override_settings(MAIL_EDITOR_CONF=CONFIG) @@ -110,6 +110,6 @@ def test_render_preview(self): subject, body = template.render(body_context, subj_context=subject_context) # rendered placeholder - self.assertEquals(subject, "Important message for --id--") + self.assertEqual(subject, "Important message for --id--") # rendered example self.assertIn("Test mail sent from testcase with 321", body) diff --git a/tox.ini b/tox.ini index 8b24d8d..fd3f690 100644 --- a/tox.ini +++ b/tox.ini @@ -1,34 +1,48 @@ [tox] -envlist = py{27,34,35,py}-django{18,19,110},isort +envlist = + py{310,311,312}-django{32,42} + isort + black + flake8 skip_missing_interpreters = true +[gh-actions] +python = + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[gh-actions:env] +DJANGO = + 3.2: django32 + 4.2: django42 + [testenv] +setenv = + DJANGO_SETTINGS_MODULE=tests.settings + PYTHONPATH={toxinidir} +extras = + tests + coverage deps = - factory-boy - mock - psycopg2 - pytest - pytest-cov - pytest-django - pytest-pep8 - pytest-pylint - pytest-pythonpath - pytest-runner - django18: Django>=1.8,<1.9 - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 -commands= - py.test \ - --cov-report=xml \ - --cov=mail_editor \ - --verbose \ - --junit-xml=junit.xml \ - --color=yes \ - tests \ - {posargs} - + django42: Django~=4.2.0 + django32: Django~=3.2.0 +commands = + py.test tests \ + --cov --cov-report xml:reports/coverage-{envname}.xml \ + {posargs} [testenv:isort] -deps = isort +extras = tests +skipsdist = True +commands = isort --check-only --diff . + +[testenv:black] +extras = tests +skipsdist = True +commands = black --check mail_editor tests + +[testenv:flake8] +extras = tests skipsdist = True -commands = isort --recursive --check-only --diff mail_editor +commands = flake8 .