Skip to content

Commit

Permalink
Add support for external configuration json (#29)
Browse files Browse the repository at this point in the history
* Add support for external configuration json

* Add documentation

* Add test
  • Loading branch information
yakky committed Dec 27, 2020
1 parent 7a0fe6e commit 8410a0e
Show file tree
Hide file tree
Showing 16 changed files with 221 additions and 23 deletions.
20 changes: 19 additions & 1 deletion app_enabler/cli.py
@@ -1,10 +1,12 @@
import os
import sys
from pathlib import Path
from subprocess import CalledProcessError
from typing import List

import click

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

Expand Down Expand Up @@ -38,6 +40,22 @@ def enable(context: click.core.Context, application: str):
enable_fun(application, verbose=context.obj["verbose"])


@cli.command()
@click.argument("config_set", nargs=-1)
@click.pass_context
def apply(context: click.core.Context, config_set: List[str]):
"""
Apply configuration stored in one or more json files.
CONFIG_SET: Path to configuration files
\f
:param click.core.Context context: Click context
:param list config_set: list of paths to addon configuration to load and apply
"""
apply_configuration_set([Path(config) for config in config_set], verbose=context.obj["verbose"])


@cli.command()
@click.argument("package")
@click.option("--pip-options", default="", help="Additional options passed as is to pip")
Expand Down
55 changes: 45 additions & 10 deletions app_enabler/enable.py
@@ -1,11 +1,14 @@
import json
import sys
from importlib import import_module
from pathlib import Path
from types import ModuleType
from typing import Any, Dict
from typing import Any, Dict, List

import django.conf

from .django import get_settings_path, get_urlconf_path, load_addon
from .errors import messages
from .patcher import setup_django, update_setting, update_urlconf


Expand All @@ -17,7 +20,7 @@ def _verify_settings(imported: ModuleType, application_config: Dict[str, Any]) -
:param dict application_config: addon configuration
"""
test_passed = True
for app in application_config["installed-apps"]:
for app in application_config.get("installed-apps", []):
test_passed = test_passed and app in imported.INSTALLED_APPS
for key, value in application_config.get("settings", {}).items():
if isinstance(value, list):
Expand Down Expand Up @@ -85,21 +88,53 @@ def output_message(message: str):
sys.stdout.write(message)


def enable(application: str, verbose: bool = False):
def apply_configuration(application_config: Dict[str, Any]):
"""
Enable django application in the current project
:param dict application_config: addon configuration
"""

setting_file = get_settings_path(django.conf.settings)
urlconf_file = get_urlconf_path(django.conf.settings)
update_setting(setting_file, application_config)
update_urlconf(urlconf_file, application_config)
if verify_installation(django.conf.settings, application_config):
output_message(application_config.get("message", ""))
else:
output_message(messages["verify_error"].format(package=application_config.get("package-name")))


def enable_application(application: str, verbose: bool = False):
"""
Enable django application in the current project
:param str application: python module name to enable. It must be the name of a Django application.
:param bool verbose: Verbose output (currently unused)
"""

setup_django()

setting_file = get_settings_path(django.conf.settings)
urlconf_file = get_urlconf_path(django.conf.settings)
application_config = load_addon(application)
if application_config:
update_setting(setting_file, application_config)
update_urlconf(urlconf_file, application_config)
if verify_installation(django.conf.settings, application_config):
output_message(application_config.get("message", ""))
apply_configuration(application_config)


def apply_configuration_set(config_set: List[Path], verbose: bool = False):
"""
Apply settings from the list of input files.
:param list config_set: list of paths to addon configuration to load and apply
:param bool verbose: Verbose output (currently unused)
"""
setup_django()

for config_path in config_set:
try:
config_data = json.loads(config_path.read_text())
except OSError:
config_data = []
if config_data:
if not isinstance(config_data, list):
config_data = [config_data]
for item in config_data:
apply_configuration(item)
1 change: 1 addition & 0 deletions app_enabler/errors.py
Expand Up @@ -2,4 +2,5 @@
"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",
"verify_error": "Error verifying {package} configuration",
}
2 changes: 1 addition & 1 deletion app_enabler/patcher.py
Expand Up @@ -81,7 +81,7 @@ def update_setting(project_setting: str, config: Dict[str, Any]):
parsed = astor.parse_file(project_setting)
existing_setting = []
addon_settings = config.get("settings", {})
addon_installed_apps = config["installed-apps"]
addon_installed_apps = config.get("installed-apps", [])
for node in parsed.body:
if isinstance(node, ast.Assign) and node.targets[0].id == "INSTALLED_APPS":
installed_apps = [name.s for name in node.value.elts]
Expand Down
1 change: 1 addition & 0 deletions changes/9.feature
@@ -0,0 +1 @@
Add support for external configuration json
12 changes: 12 additions & 0 deletions docs/addon_configuration.rst
Expand Up @@ -24,6 +24,18 @@ the minimal setup to make the application up an running on a clean django projec
.. warning:: The file must be included in root of the first (alphabetically) module of your application package.
See :ref:`packaging` for details.

.. _extra_json:

****************************************
Extra configuration files specifications
****************************************

Extra configuration files (applied via :ref:`apply_cmd`) must conform to the same specifications below with two exceptions:

- all attributes are optional (i.e.: they can be completely omitted)
- the json file can contain a single object like for the ``addon.json`` case, or a list of objects conforming to the specifications.


Attributes
===========

Expand Down
13 changes: 8 additions & 5 deletions docs/api/index.rst
Expand Up @@ -4,34 +4,37 @@
API
###

********
Commands
********

.. click:: app_enabler.cli:cli
:prog: django-enabler
:show-nested:

*******
CLI
*******

.. automodule:: app_enabler.cli
:members:
:private-members:

.. automodule:: app_enabler.enable
:members:
:private-members:

.. automodule:: app_enabler.install
:members:
:private-members:

*******
Loaders
*******

.. automodule:: app_enabler.django
:members:
:private-members:

********
Patchers
********

.. automodule:: app_enabler.patcher
:members:
:private-members:
1 change: 1 addition & 0 deletions docs/conf.py
Expand Up @@ -41,6 +41,7 @@
"sphinx.ext.coverage",
"sphinx.ext.ifconfig",
"sphinx.ext.viewcode",
"sphinx_click.ext",
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
2 changes: 0 additions & 2 deletions docs/todo.rst
Expand Up @@ -8,7 +8,6 @@ Planned features
* Support extra-requirements `issue-6`_
* Support Django settings split in multiple files `issue-7`_
* Support Django urlconf split in multiple files `issue-8`_
* Add support for external addon.json `issue-9`_



Expand All @@ -17,4 +16,3 @@ Planned features
.. _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
25 changes: 25 additions & 0 deletions docs/usage.rst
Expand Up @@ -18,6 +18,7 @@ Installation
Commands
*************************

* :ref:`apply \<path_to_json\> \<path_to_json\> <apply_cmd>`: Apply configuration from json files
* :ref:`enable \<module_name\> <enable_cmd>`: Configure an application
* :ref:`install \<package-name\> <install_cmd>`: Install and configure an application

Expand Down Expand Up @@ -64,6 +65,30 @@ Example:
django-enabler enable djangocms_blog
See :ref:`limitations` for limitations and caveats.


.. _apply_cmd:

*************************
Apply configurations
*************************

``django-app-enabler`` can also apply configuration from arbitrary json files not included in any Django application.

Each configuration file must comply with :ref:`extra_json`.

.. note:: Django ``settings`` and ``urlconf`` are patched unconditionally.
No attempt to verify that applications declared in ``installed_apps``
or added to the ``urlconf`` are available in the virtualenv is made.

Example:

.. code-block:: bash
django-enabler apply /path/to/config1.json /path/to/config2.json
See :ref:`limitations` for limitations and caveats.

.. _install_cmd:
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -68,6 +68,7 @@ app_enabler = *.html *.png *.gif *js *jpg *jpeg *svg *py *mo *po
[options.extras_require]
docs =
django<3.1
sphinx-click

[options.entry_points]
console_scripts =
Expand Down
25 changes: 25 additions & 0 deletions tests/sample/config/1.json
@@ -0,0 +1,25 @@
[
{
"installed-apps": [
"taggit"
],
"settings": {
"MY_SETTING_A": "some_value"
},
"urls": [
[
"",
"djangocms_blog.taggit_urls"
]
],
"message": "json1-a"
},
{
"installed-apps": [
],
"settings": {
"MY_SETTING_B": "some_value"
},
"message": "json1-b"
}
]
6 changes: 6 additions & 0 deletions tests/sample/config/2.json
@@ -0,0 +1,6 @@
{
"settings": {
"MY_SETTING_2": "some_value"
},
"message": "json2"
}
20 changes: 20 additions & 0 deletions tests/test_cli.py
@@ -1,5 +1,6 @@
import os
import sys
from pathlib import Path
from subprocess import CalledProcessError
from unittest.mock import call, patch

Expand Down Expand Up @@ -121,6 +122,25 @@ def test_cli_enable(verbose: bool):
assert enable_fun.call_args_list == [call("djangocms_blog", verbose=verbose)]


@pytest.mark.parametrize("verbose", (True, False))
def test_cli_apply(verbose: bool):
"""Running apply command calls the business functions with the correct arguments."""
with patch("app_enabler.cli.apply_configuration_set") as apply_configuration_set:
runner = CliRunner()
if verbose:
args = ["--verbose"]
else:
args = []

configs = ("/path/config1.json", "/path/config2.json")
args.extend(("apply", *configs))
result = runner.invoke(cli, args)
assert result.exit_code == 0

apply_configuration_set.assert_called_once()
assert apply_configuration_set.call_args_list == [call([Path(config) for config in configs], verbose=verbose)]


@pytest.mark.parametrize("verbose", (True, False))
def test_cli_function(verbose: bool):
"""Running cli without commands return info message."""
Expand Down

0 comments on commit 8410a0e

Please sign in to comment.