Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
yakky committed Dec 20, 2020
1 parent 85de0d5 commit c7ba99f
Show file tree
Hide file tree
Showing 28 changed files with 755 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Test with tox
env:
TOX_ENV: ${{ format('py-django{1}', matrix.python-version, matrix.django) }}
COMMAND: coverage run
PYTEST_ARGS: --cov=app_enabler
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github
run: |
Expand Down
6 changes: 3 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ See `usage`_ for more details.
Compatible packages
-------------------

`Up-to-date list of compatible packages <packages>`_
`Up-to-date list of compatible packages`_

.. |Gitter| image:: https://img.shields.io/badge/GITTER-join%20chat-brightgreen.svg?style=flat-square
:target: https://gitter.im/nephila/applications
Expand Down Expand Up @@ -70,5 +70,5 @@ Compatible packages
:alt: Code Climate


.. _usage: https://django-app-enabler.readthedocs.io/usage/
.. _packages: https://pypi.org/search/?q="django-app-enabler+addon"
.. _usage: https://django-app-enabler.readthedocs.io/en/latest/usage.html
.. _Up-to-date list of compatible packages: https://pypi.org/search/?q="django-app-enabler+addon"
2 changes: 1 addition & 1 deletion app_enabler/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .cli import cli

cli()
cli() # pragma: no cover
10 changes: 6 additions & 4 deletions app_enabler/cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import sys
from subprocess import CalledProcessError

import click

from .enable import enable as enable_fun
from .errors import messages
from .install import get_application_from_package, install as install_fun


Expand Down Expand Up @@ -52,19 +54,19 @@ def install(context: click.core.Context, package: str, pip_options: str):
try:
install_fun(package, verbose=verbose, pip_options=pip_options)
except CalledProcessError:
msg = f"Package {package} not installable in the current virtualenv"
msg = messages["install_error"].format(package=package)
if verbose:
raise RuntimeError(msg)
else:
print(msg)
sys.stderr.write(msg)
return
application = get_application_from_package(package)
if application:
enable_fun(application, verbose=verbose)
else:
msg = f"Package {package} not installed in the current virtualenv"
msg = messages["enable_error"].format(package=package)
if verbose:
raise RuntimeError(msg)
else:
print(msg)
sys.stderr.write(msg)
return
6 changes: 3 additions & 3 deletions app_enabler/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from importlib import import_module
from typing import Optional

from django.conf import LazySettings
import django.conf
from pkg_resources import resource_stream


Expand All @@ -22,7 +22,7 @@ def load_addon(module_name: str) -> Optional[dict]:
pass


def get_settings_path(setting: LazySettings) -> str:
def get_settings_path(setting: "django.conf.LazySettings") -> str:
"""
Get the path of the django settings file from the django settings object.
Expand All @@ -33,7 +33,7 @@ def get_settings_path(setting: LazySettings) -> str:
return settings_module.__file__


def get_urlconf_path(setting: LazySettings) -> str:
def get_urlconf_path(setting: "django.conf.LazySettings") -> str:
"""
Get the path of the django urlconf file from the django settings object.
Expand Down
5 changes: 5 additions & 0 deletions app_enabler/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
messages = {
"no_managepy": "app-enabler must be executed in the same directory as the project manage.py file",
"install_error": "Package {package} not installable in the current virtualenv",
"enable_error": "Package {package} not installed in the current virtualenv",
}
5 changes: 3 additions & 2 deletions app_enabler/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def install(package: str, verbose: bool = False, pip_options: str = ""):
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
sys.stdout.write(output.decode("utf-8"))
except Exception as e: # pragma: no cover
return True
except subprocess.CalledProcessError as e: # pragma: no cover
logger.error("cmd : {} :{}".format(e.cmd, e.output))
raise

Expand All @@ -53,5 +54,5 @@ def get_application_from_package(package: str) -> Optional[str]:
return
try:
return distribution.get_metadata("top_level.txt").split()[0]
except (FileNotFoundError, IndexError):
except (FileNotFoundError, IndexError): # pragma: no cover
return
21 changes: 17 additions & 4 deletions app_enabler/patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from typing import Any, Dict

import astor
import django

from .errors import messages


def setup_django():
Expand All @@ -16,12 +17,14 @@ def setup_django():
Manage.py is monkeypatched in memory to remove the call "execute_from_command_line" and executed from memory.
"""
import django

try:
managed_command = monkeypatch_manage("manage.py")
eval(managed_command)
django.setup()
except FileNotFoundError:
print("app-enabler must be execute in the same directory as the project manage.py file")
sys.stderr.write(messages["no_managepy"])
sys.exit(1)


Expand All @@ -35,6 +38,7 @@ def monkeypatch_manage(manage_file: str) -> CodeType:
:return: patched manage.py code
"""
parsed = astor.parse_file(manage_file)
# first patch run replace __name__ != '__main__' with a function call
modified = DisableExecute().visit(parsed)
# patching the module with the call to the main function as the standard one is not executed because
# __name__ != '__main__'
Expand All @@ -50,7 +54,7 @@ class DisableExecute(ast.NodeTransformer):

def visit_Expr(self, node: ast.AST) -> Any: # noqa
"""Visit the Expr node and remove it if it matches execute_from_command_line."""
# long chained checks, but we have to remove the entire call, thus w have to remove the Expr node
# long chained checks, but we have to remove the entire call, thus we have to remove the Expr node
if (
isinstance(node.value, ast.Call)
and isinstance(node.value.func, ast.Name) # noqa
Expand Down Expand Up @@ -78,6 +82,13 @@ def update_setting(project_setting: str, config: Dict[str, Any]):
addon_apps = [ast.Constant(app) for app in config["installed-apps"] if app not in installed_apps]
node.value.elts.extend(addon_apps)
elif isinstance(node, ast.Assign) and node.targets[0].id in config["settings"].keys(): # noqa
config_param = config["settings"][node.targets[0].id]
if isinstance(node.value, ast.List) and (
isinstance(config_param, list) or isinstance(config_param, tuple)
):
# breakpoint()
for config_value in config_param:
node.value.elts.append(ast.Str(config_value))
existing_setting.append(node.targets[0].id)
for name, value in config["settings"].items():
if name not in existing_setting:
Expand All @@ -101,7 +112,9 @@ def update_urlconf(project_urls: str, config: Dict[str, Any]):
parsed = astor.parse_file(project_urls)

for node in parsed.body:
if isinstance(node, ast.Assign) and node.targets[0].id == "urlpatterns":
if isinstance(node, ast.ImportFrom) and node.module == "django.urls":
node.names.append(ast.alias(name="include", asname=None))
elif isinstance(node, ast.Assign) and node.targets[0].id == "urlpatterns":
existing_url = []
for url_line in node.value.elts:
calls = [
Expand Down
1 change: 1 addition & 0 deletions changes/2.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add tests
3 changes: 3 additions & 0 deletions docs/addon_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Addon configuration specification
``django-app-enabler`` support can be enabled by adding a :ref:`addon_json` to any django application
(see below for the structure).


.. note: To make easier to find compatible packages, add ``django-app-enabler`` to the package keywords.
See :ref:`limitations` for limitations and caveats.

.. _addon_json:
Expand Down
4 changes: 1 addition & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath(".."))
import helper # isort:skip # noqa: E402
import app_enabler # isort:skip # noqa: E402

helper.setup()


# -- General configuration ------------------------------------------------

Expand Down Expand Up @@ -291,6 +288,7 @@
todo_include_todos = True

autodoc_mock_imports = [
"django.conf",
"django.utils.translation",
]
autodoc_default_flags = [
Expand Down
20 changes: 10 additions & 10 deletions docs/todo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
Planned features
################

* `More standard django settings than INSTALLED_APPS <issue_5>`_
* `Support extra-requirements <issue_6>`_
* `Django settings splitted in multiple files <issue_7>`_
* `Django urlconf splitted in multiple files <issue_8>`_
* `Add external addon.json <issue_9>`_
* More standard django settings than INSTALLED_APPS `issue-5`_
* Support extra-requirements `issue-6`_
* Django settings splitted in multiple files `issue-7`_
* Django urlconf splitted in multiple files `issue-8`_
* Add external addon.json `issue-9`_




.. _issue_5: https://github.com/nephila/django-app-enabler/issues/5
.. _issue_6: https://github.com/nephila/django-app-enabler/issues/6
.. _issue_7: https://github.com/nephila/django-app-enabler/issues/7
.. _issue_8: https://github.com/nephila/django-app-enabler/issues/8
.. _issue_9: https://github.com/nephila/django-app-enabler/issues/9
.. _issue-5: https://github.com/nephila/django-app-enabler/issues/5
.. _issue-6: https://github.com/nephila/django-app-enabler/issues/6
.. _issue-7: https://github.com/nephila/django-app-enabler/issues/7
.. _issue-8: https://github.com/nephila/django-app-enabler/issues/8
.. _issue-9: https://github.com/nephila/django-app-enabler/issues/9
30 changes: 0 additions & 30 deletions helper.py

This file was deleted.

2 changes: 2 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
coverage
coveralls>=2.0
django-app-helper>=2.0.0
pytest
pytest-cov
6 changes: 3 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ classifiers =
Framework :: Django :: 3.0
Framework :: Django :: 3.1
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9

[options]
include_package_data = True
Expand All @@ -57,8 +57,8 @@ install_requires =
setup_requires =
setuptools
packages = app_enabler
python_requires = >=3.5
test_suite = cms_helper.run
python_requires = >=3.6
test_suite = pytest
zip_safe = False

[options.package_data]
Expand Down
78 changes: 78 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
import shutil
from pathlib import Path
from typing import Any, Dict

import pytest

from app_enabler.install import install
from app_enabler.patcher import setup_django
from tests.utils import get_project_dir, unload_django, working_directory

pytest_plugins = "pytester"


@pytest.fixture
def blog_package():
"""Ensure djangocms-blog is installed."""
install("djangocms-blog")


@pytest.fixture
def django_setup(project_dir: str):
"""Setup django environment."""
with working_directory(project_dir):
os.environ["DJANGO_SETTINGS_MODULE"] = "test_project.settings"
setup_django()
yield
unload_django()


@pytest.fixture
def teardown_django():
"""
Reset django imports and configuration, undoing django.setup call.
Use this fixture whenever django.setup is called during test execution (either explicitly or implicitly).
Already called by :py:func:`django_setup`.
"""
yield
unload_django()


@pytest.fixture
def project_dir(pytester) -> Path:
"""Create a temporary django project structure."""
original_project = get_project_dir()
tmp_project = pytester.path / "tmp_project"
shutil.rmtree(tmp_project, ignore_errors=True)
shutil.copytree(original_project, tmp_project)
yield tmp_project
shutil.rmtree(tmp_project, ignore_errors=True)


@pytest.fixture
def addon_config() -> Dict[str, Any]:
"""Sample addon config."""
return {
"package-name": "djangocms-blog",
"installed-apps": [
"filer",
"easy_thumbnails",
"aldryn_apphooks_config",
"parler",
"taggit",
"taggit_autosuggest",
"meta",
"djangocms_blog",
"sortedm2m",
],
"settings": {
"META_SITE_PROTOCOL": "https",
"META_USE_SITES": True,
"MIDDLEWARE": ["django.middleware.gzip.GZipMiddleware"],
},
"urls": [["", "djangocms_blog.taggit_urls"]],
"message": "Please check documentation to complete the setup",
}
Loading

0 comments on commit c7ba99f

Please sign in to comment.