~netlandish/django-wiki

3979aa81c2de8923d8b3def46855533881ab0c6a — Benjamin Bach 8 years ago 217c8b9
Consolidate requirements, remove django\<1.8 artifacts, update tests and reverse monkey_patching
13 files changed, 138 insertions(+), 244 deletions(-)

A .coveragerc
M Makefile
M pytest.ini
M setup.py
M testproject/testproject/settings/base.py
M tox.ini
M wiki/models/__init__.py
M wiki/plugins/attachments/urls.py
R runtests.py => wiki/tests/settings.py -rwxr-xr-x => -rw-r--r--
M wiki/tests/test_urls.py
M wiki/tests/test_views.py
M wiki/tests/testdata/urls.py
M wiki/urls.py
A .coveragerc => .coveragerc +2 -0
@@ 0,0 1,2 @@
[run]
omit = */tests/*

M Makefile => Makefile +1 -1
@@ 27,7 27,7 @@ lint:
	pep8 wiki

test:
	./runtests.py
	pytest

test-all:
	tox

M pytest.ini => pytest.ini +1 -0
@@ 3,3 3,4 @@ django_find_project = false
python_files=test_*.py
testpaths=wiki
norecursedirs=testproject .svn _build tmp* dist *.egg-info
DJANGO_SETTINGS_MODULE=wiki.tests.settings

M setup.py => setup.py +5 -45
@@ 3,8 3,6 @@
from __future__ import absolute_import, unicode_literals

import os
from sys import version_info as PYTHON_VERSION

from setuptools import find_packages, setup

from wiki import __version__


@@ 14,8 12,6 @@ from wiki import __version__
# Used for the long_description.  It's nice, because now 1) we have a top level
# README file and 2) it's easier to type in the README file than to put a raw
# string in below ...


def get_path(fname):
    return os.path.join(os.path.dirname(__file__), fname)



@@ 25,52 21,16 @@ def read(fname):


requirements = [
    "Django>=1.5",
    "Django>=1.8",
    "Pillow",
    "django-nyt>=0.9.7.2,<1.0",
    "six",
    "django-mptt>=0.8.6,<0.9",
    "django-sekizai>=0.10",
    "sorl-thumbnail>=12,<13",
    "Markdown>=2.6,<2.7",
]


# Requirements that depend on Django version: South and sorl-thumbnail
try:
    from django import VERSION as DJANGO_VERSION
except ImportError:
    # No Django so assuming that one will get installed, but we don't know which
    # one.
    # We will assume it's a very recent one and base the requirements on that...
    requirements.append("sorl-thumbnail>=12")
    # 0.6.1 broken: https://github.com/django-mptt/django-mptt/issues/316
    requirements.append("django-mptt>=0.8")
    requirements.append("django-sekizai>=0.9")
else:
    if DJANGO_VERSION < (1, 7):
        requirements.append("South>=1.0.1")
        requirements.append("django-mptt>=0.7.1,<0.8")
        requirements.append("django-sekizai<0.9")
    elif DJANGO_VERSION < (1, 8):
        # Fixes
        # AttributeError: 'URLPath' object has no attribute 'get_deferred_fields'
        requirements.append("django-mptt>=0.7.1,<0.8")
        requirements.append("django-sekizai>=0.9")
    else:
        # Latest django-mptt only works for Django 1.8+
        requirements.append("django-mptt>=0.8.6,<0.9")
        requirements.append("django-sekizai>=0.9")
    if DJANGO_VERSION < (1, 5):
        # For Django 1.4, use sorl-thumbnail<11.12.1:
        # https://github.com/mariocesar/sorl-thumbnail/issues/255
        requirements.append("sorl-thumbnail<11.12.1")
    else:
        requirements.append("sorl-thumbnail>=12,<13")

if PYTHON_VERSION < (2, 7):
    # For Python 2.6, use Markdown<2.5.0, see
    # https://github.com/waylan/Python-Markdown/issues/349
    requirements.append("Markdown>2.2.0,<2.5.0")
else:
    requirements.append("Markdown>2.2.0,<2.7")

packages = find_packages()



M testproject/testproject/settings/base.py => testproject/testproject/settings/base.py +2 -8
@@ 13,7 13,6 @@ from __future__ import unicode_literals

import os

from django import VERSION
from django.core.urlresolvers import reverse_lazy




@@ 32,9 31,6 @@ DEBUG = False
ALLOWED_HOSTS = []


# Application definition


INSTALLED_APPS = [
    'django.contrib.humanize',
    'django.contrib.auth',


@@ 57,10 53,8 @@ INSTALLED_APPS = [
    'wiki.plugins.notifications',
    'mptt',
]
if VERSION < (1, 7):
    INSTALLED_APPS.append('south')
else:
    TEST_RUNNER = 'django.test.runner.DiscoverRunner'

TEST_RUNNER = 'django.test.runner.DiscoverRunner'


MIDDLEWARE_CLASSES = [

M tox.ini => tox.ini +8 -5
@@ 5,24 5,27 @@ envlist = {py27,py34,py35}-django{18,19,110}
[testenv]

commands =
     {envbindir}/coverage run --source=wiki runtests.py
     {envbindir}/pytest --cov=wiki --cov-config .coveragerc

usedevelop = true

deps =
     coverage
     pytest
     pytest-django
     pytest-cov
     Pillow==2.3.0
     django-classy-tags==0.4
     six>=1.9
     mock>=2.0
     Markdown==2.6.5
     Markdown==2.6.7
     django_nyt==0.9.8
     django18: Django==1.8.2
     django19: Django==1.9
     django110: Django==1.10
     django110: Django==1.10.2
     django{18,19,110}: django-mptt==0.8.6
     django{18,19,110}: django-sekizai==0.9.0
     django{18,19,110}: django-sekizai==0.10.0
     django{18,19,110}: sorl-thumbnail==12.3
     django{18,19,110}: pytest-django>=3

basepython =
     py27: python2.7

M wiki/models/__init__.py => wiki/models/__init__.py +19 -14
@@ 1,17 1,16 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
# -*- coding: utf-8 -*-

from django import VERSION
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
import warnings
from six import string_types
from six import string_types, text_type

# TODO: Don't use wildcards
from .article import *  # noqa
from .pluginbase import *  # noqa
from .urlpath import *  # noqa
from django.utils.functional import lazy

# TODO: Should the below stuff be executed a more logical place?
# Follow Django's default_settings.py / settings.py pattern and put these


@@ 64,16 63,6 @@ if 'django_notify' in django_settings.INSTALLED_APPS:
    raise ImproperlyConfigured(
        'django-wiki: You need to change from django_notify to django_nyt in INSTALLED_APPS and your urlconfig.')

######################
# Warnings
######################


if VERSION < (1, 7):
    if 'south' not in django_settings.INSTALLED_APPS:
        warnings.warn(
            "django-wiki: No south in your INSTALLED_APPS. This is highly discouraged.")


from django.core import urlresolvers  # noqa



@@ 108,4 97,20 @@ def reverse(*args, **kwargs):
    return url

# Now we redefine reverse method
reverse_lazy = lazy(reverse, text_type)
urlresolvers.reverse = reverse
urlresolvers.reverse_lazy = reverse_lazy

# Patch up other locations of the reverse function
try:
    from django.urls import base
    from django import urls
    from django import shortcuts
    base.reverse = reverse
    base.reverse_lazy = reverse_lazy
    urls.reverse = reverse
    urls.reverse_lazy = reverse_lazy
    shortcuts.reverse = reverse
    urls.reverse_lazy = reverse_lazy
except ImportError:
    pass

M wiki/plugins/attachments/urls.py => wiki/plugins/attachments/urls.py +1 -5
@@ 1,7 1,6 @@
from __future__ import absolute_import, unicode_literals

from django import VERSION as DJANGO_VERSION
from django.conf.urls import patterns, url
from django.conf.urls import url
from wiki.plugins.attachments import views

urlpatterns = [


@@ 33,6 32,3 @@ urlpatterns = [
        views.AttachmentChangeRevisionView.as_view(),
        name='attachments_revision_change'),
]

if DJANGO_VERSION < (1, 8):
    urlpatterns = patterns('', *urlpatterns)

R runtests.py => wiki/tests/settings.py +57 -109
@@ 1,111 1,59 @@
#!/usr/bin/env python
from __future__ import absolute_import, unicode_literals

import sys

import django
import pytest
from django.conf import settings

# Run py.tests
# Compatibility testing patches on the py-moneyed

settings_dict = dict(
    DEBUG=True,
    AUTH_USER_MODEL='testdata.CustomUser',
    WIKI_GROUP_MODEL='testdata.CustomGroup',
    DATABASES={
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
        }
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
    }
}

DEBUG = True
AUTH_USER_MODEL = 'testdata.CustomUser'
WIKI_GROUP_MODEL = 'testdata.CustomGroup'
SITE_ID = 1
ROOT_URLCONF = 'wiki.tests.testdata.urls'
INSTALLED_APPS = [
    'wiki.tests.testdata',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.admin',
    'django.contrib.humanize',
    'django.contrib.sites',
    'django_nyt',
    'mptt',
    'sekizai',
    'sorl.thumbnail',
    'wiki',
    'wiki.plugins.attachments',
    'wiki.plugins.notifications',
    'wiki.plugins.images',
    'wiki.plugins.macros',
]
MIDDLEWARE_CLASSES = [
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
]
USE_TZ = True
SECRET_KEY = 'b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!'
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                "django.contrib.auth.context_processors.auth",
                "django.template.context_processors.debug",
                "django.template.context_processors.i18n",
                "django.template.context_processors.media",
                "django.template.context_processors.request",
                "django.template.context_processors.static",
                "django.template.context_processors.tz",
                "django.contrib.messages.context_processors.messages",
                "sekizai.context_processors.sekizai",
            ]
        },
    },
    SITE_ID=1,
    ROOT_URLCONF='wiki.tests.testdata.urls',
    INSTALLED_APPS=[
        'wiki.tests.testdata',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.admin',
        'django.contrib.humanize',
        'django.contrib.sites',
        'django_nyt',
        'mptt',
        'sekizai',
        'sorl.thumbnail',
        'wiki',
        'wiki.plugins.attachments',
        'wiki.plugins.notifications',
        'wiki.plugins.images',
        'wiki.plugins.macros',
    ] + (['south'] if django.VERSION < (1, 7) else []),
    MIDDLEWARE_CLASSES=[
        'django.middleware.common.CommonMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
    ],
    USE_TZ=True,
    SOUTH_TESTS_MIGRATE=True,
    SECRET_KEY='b^fv_)t39h%9p40)fnkfblo##jkr!$0)lkp6bpy!fi*f$4*92!',
)

TEMPLATE_CONTEXT_PROCESSORS = [
    "django.contrib.auth.context_processors.auth",
    "django.template.context_processors.debug",
    "django.template.context_processors.i18n",
    "django.template.context_processors.media",
    "django.template.context_processors.request",
    "django.template.context_processors.static",
    "django.template.context_processors.tz",
    "django.contrib.messages.context_processors.messages",
    "sekizai.context_processors.sekizai",
]

if django.VERSION < (1, 8):
    settings_dict.update(dict(
        TEMPLATE_CONTEXT_PROCESSORS=[p.replace('django.template.context_processors',
                                               'django.core.context_processors')
                                     for p in TEMPLATE_CONTEXT_PROCESSORS]
    ))
else:
    settings_dict.update(dict(
        TEMPLATES=[
            {
                'BACKEND': 'django.template.backends.django.DjangoTemplates',
                'APP_DIRS': True,
                'OPTIONS': {
                    'context_processors': TEMPLATE_CONTEXT_PROCESSORS
                },
            },
        ]
    ))

settings.configure(**settings_dict)


# If you use South for migrations, uncomment this to monkeypatch
# syncdb to get migrations to run.
if django.VERSION < (1, 7):
    from south.management.commands import patch_for_test_db_setup
    patch_for_test_db_setup()

argv = [sys.argv[0], "test", "--traceback"]

# python setup.py test calls script with just 'test'
if len(sys.argv) == 1 or sys.argv[1] == 'test':
    # Nothing following 'runtests.py':
    if django.VERSION < (1, 6):
        argv.extend(["wiki", "attachments"])
    else:
        argv.extend(["wiki.tests", "wiki.plugins.attachments.tests"])
else:
    # Allow tests to be specified:
    argv.extend(sys.argv[1:])


failures = pytest.main()

if failures:
    sys.exit(failures)

M wiki/tests/test_urls.py => wiki/tests/test_urls.py +1 -7
@@ 1,7 1,7 @@
from __future__ import absolute_import, unicode_literals

from django import VERSION as DJANGO_VERSION
from django.conf.urls import patterns, url
from django.conf.urls import url
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.test.testcases import TestCase


@@ 14,9 14,6 @@ from wiki.urls import WikiURLPatterns
User = get_user_model()





class WikiCustomUrlPatterns(WikiURLPatterns):

    def get_article_urls(self):


@@ 41,9 38,6 @@ urlpatterns = [
    url(r'', get_wiki_pattern(url_config_class=WikiCustomUrlPatterns))
]

if DJANGO_VERSION < (1, 8):
    urlpatterns = patterns('', *urlpatterns)


@wiki_override_settings(WIKI_URL_CONFIG_CLASS='wiki.tests.test_models.WikiCustomUrlPatterns',
                        ROOT_URLCONF='wiki.tests.test_urls')

M wiki/tests/test_views.py => wiki/tests/test_views.py +1 -1
@@ 3,7 3,7 @@ from __future__ import absolute_import, print_function, unicode_literals
import pprint

from django.contrib.auth import authenticate
from django.core.urlresolvers import reverse
from wiki.models import reverse

from .base import ArticleWebTestBase, WebTestBase


M wiki/tests/testdata/urls.py => wiki/tests/testdata/urls.py +1 -5
@@ 1,8 1,7 @@
from __future__ import absolute_import, unicode_literals

from django import VERSION as DJANGO_VERSION
from django.conf import settings
from django.conf.urls import include, patterns, url
from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django_nyt.urls import get_pattern as get_notify_pattern


@@ 28,6 27,3 @@ urlpatterns += [
    url(r'^notify/', get_notify_pattern()),
    url(r'', get_wiki_pattern())
]

if DJANGO_VERSION < (1, 8):
    urlpatterns = patterns('', *urlpatterns)

M wiki/urls.py => wiki/urls.py +39 -44
@@ 1,7 1,6 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

from django import VERSION as DJANGO_VERSION
from django.conf.urls import include, url
from wiki.conf import settings
from wiki.core.plugins import registry


@@ 62,20 61,20 @@ class WikiURLPatterns(object):

    def get_root_urls(self):
        urlpatterns = [
            url('^$',
            url(r'^$',
                self.article_view_class.as_view(),
                name='root',
                kwargs={'path': ''}),
            url('^create-root/$',
            url(r'^create-root/$',
                article.CreateRootView.as_view(),
                name='root_create'),
            url('^missing-root/$',
            url(r'^missing-root/$',
                article.MissingRootView.as_view(),
                name='root_missing'),
            url('^_search/$',
            url(r'^_search/$',
                get_class_from_str(self.search_view_class).as_view(),
                name='search'),
            url('^_revision/diff/(?P<revision_id>\d+)/$',
            url(r'^_revision/diff/(?P<revision_id>\d+)/$',
                self.article_diff_view,
                name='diff'),
        ]


@@ 92,16 91,16 @@ class WikiURLPatterns(object):
    def get_accounts_urls(self):
        if settings.ACCOUNT_HANDLING:
            urlpatterns = [
                url('^_accounts/sign-up/$',
                url(r'^_accounts/sign-up/$',
                    self.signup_view_class.as_view(),
                    name='signup'),
                url('^_accounts/logout/$',
                url(r'^_accounts/logout/$',
                    self.logout_view_class.as_view(),
                    name='logout'),
                url('^_accounts/login/$',
                url(r'^_accounts/login/$',
                    self.login_view_class.as_view(),
                    name='login'),
                url('^_accounts/settings/$',
                url(r'^_accounts/settings/$',
                    self.profile_update_view_class.as_view(),
                    name='profile_update'),
            ]


@@ 114,14 113,14 @@ class WikiURLPatterns(object):
            # This one doesn't work because it don't know
            # where to redirect after...
            url(
                '^_revision/change/(?P<article_id>\d+)/(?P<revision_id>\d+)/$',
                r'^_revision/change/(?P<article_id>\d+)/(?P<revision_id>\d+)/$',
                self.revision_change_view_class.as_view(),
                name='change_revision'),
            url('^_revision/preview/(?P<article_id>\d+)/$',
            url(r'^_revision/preview/(?P<article_id>\d+)/$',
                self.article_preview_view_class.as_view(),
                name='preview_revision'),
            url(
                '^_revision/merge/(?P<article_id>\d+)/(?P<revision_id>\d+)/preview/$',
                r'^_revision/merge/(?P<article_id>\d+)/(?P<revision_id>\d+)/preview/$',
                self.revision_merge_view,
                name='merge_revision_preview',
                kwargs={


@@ 132,39 131,39 @@ class WikiURLPatterns(object):
    def get_article_urls(self):
        urlpatterns = [
            # Paths decided by article_ids
            url('^(?P<article_id>\d+)/$',
            url(r'^(?P<article_id>\d+)/$',
                self.article_view_class.as_view(),
                name='get'),
            url('^(?P<article_id>\d+)/delete/$',
            url(r'^(?P<article_id>\d+)/delete/$',
                self.article_delete_view_class.as_view(),
                name='delete'),
            url('^(?P<article_id>\d+)/deleted/$',
            url(r'^(?P<article_id>\d+)/deleted/$',
                self.article_deleted_view_class.as_view(),
                name='deleted'),
            url('^(?P<article_id>\d+)/edit/$',
            url(r'^(?P<article_id>\d+)/edit/$',
                self.article_edit_view_class.as_view(),
                name='edit'),
            url('^(?P<article_id>\d+)/preview/$',
            url(r'^(?P<article_id>\d+)/preview/$',
                self.article_preview_view_class.as_view(),
                name='preview'),
            url('^(?P<article_id>\d+)/history/$',
            url(r'^(?P<article_id>\d+)/history/$',
                self.article_history_view_class.as_view(),
                name='history'),
            url('^(?P<article_id>\d+)/settings/$',
            url(r'^(?P<article_id>\d+)/settings/$',
                self.article_settings_view_class.as_view(),
                name='settings'),
            url('^(?P<article_id>\d+)/source/$',
            url(r'^(?P<article_id>\d+)/source/$',
                self.article_source_view_class.as_view(),
                name='source'),
            url(
                '^(?P<article_id>\d+)/revision/change/(?P<revision_id>\d+)/$',
                r'^(?P<article_id>\d+)/revision/change/(?P<revision_id>\d+)/$',
                self.revision_change_view_class.as_view(),
                name='change_revision'),
            url(
                '^(?P<article_id>\d+)/revision/merge/(?P<revision_id>\d+)/$',
                r'^(?P<article_id>\d+)/revision/merge/(?P<revision_id>\d+)/$',
                self.revision_merge_view,
                name='merge_revision'),
            url('^(?P<article_id>\d+)/plugin/(?P<slug>\w+)/$',
            url(r'^(?P<article_id>\d+)/plugin/(?P<slug>\w+)/$',
                self.article_plugin_view_class.as_view(),
                name='plugin'),
        ]


@@ 173,46 172,46 @@ class WikiURLPatterns(object):
    def get_article_path_urls(self):
        urlpatterns = [
            # Paths decided by URLs
            url('^(?P<path>.+/|)_create/$',
            url(r'^(?P<path>.+/|)_create/$',
                self.article_create_view_class.as_view(),
                name='create'),
            url('^(?P<path>.+/|)_delete/$',
            url(r'^(?P<path>.+/|)_delete/$',
                self.article_delete_view_class.as_view(),
                name='delete'),
            url('^(?P<path>.+/|)_deleted/$',
            url(r'^(?P<path>.+/|)_deleted/$',
                self.article_deleted_view_class.as_view(),
                name='deleted'),
            url('^(?P<path>.+/|)_edit/$',
            url(r'^(?P<path>.+/|)_edit/$',
                self.article_edit_view_class.as_view(),
                name='edit'),
            url('^(?P<path>.+/|)_preview/$',
            url(r'^(?P<path>.+/|)_preview/$',
                self.article_preview_view_class.as_view(),
                name='preview'),
            url('^(?P<path>.+/|)_history/$',
            url(r'^(?P<path>.+/|)_history/$',
                self.article_history_view_class.as_view(),
                name='history'),
            url('^(?P<path>.+/|)_dir/$',
            url(r'^(?P<path>.+/|)_dir/$',
                self.article_dir_view_class.as_view(),
                name='dir'),
            url('^(?P<path>.+/|)_settings/$',
            url(r'^(?P<path>.+/|)_settings/$',
                self.article_settings_view_class.as_view(),
                name='settings'),
            url('^(?P<path>.+/|)_source/$',
            url(r'^(?P<path>.+/|)_source/$',
                self.article_source_view_class.as_view(),
                name='source'),
            url(
                '^(?P<path>.+/|)_revision/change/(?P<revision_id>\d+)/$',
                r'^(?P<path>.+/|)_revision/change/(?P<revision_id>\d+)/$',
                self.revision_change_view_class.as_view(),
                name='change_revision'),
            url(
                '^(?P<path>.+/|)_revision/merge/(?P<revision_id>\d+)/$',
                r'^(?P<path>.+/|)_revision/merge/(?P<revision_id>\d+)/$',
                self.revision_merge_view,
                name='merge_revision'),
            url('^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$',
            url(r'^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$',
                self.article_plugin_view_class.as_view(),
                name='plugin'),
            # This should always go last!
            url('^(?P<path>.+/|)$',
            url(r'^(?P<path>.+/|)$',
                self.article_view_class.as_view(),
                name='get'),
        ]


@@ 226,14 225,14 @@ class WikiURLPatterns(object):
            if slug:
                article_urlpatterns = plugin.urlpatterns.get('article', [])
                urlpatterns += [
                    url('^(?P<article_id>\d+)/plugin/' + slug + '/',
                    url(r'^(?P<article_id>\d+)/plugin/' + slug + '/',
                        include(article_urlpatterns)),
                    url('^(?P<path>.+/|)_plugin/' + slug + '/',
                    url(r'^(?P<path>.+/|)_plugin/' + slug + '/',
                        include(article_urlpatterns)),
                ]
                root_urlpatterns = plugin.urlpatterns.get('root', [])
                urlpatterns += [
                    url('^_plugin/' + slug + '/', include(root_urlpatterns)),
                    url(r'^_plugin/' + slug + '/', include(root_urlpatterns)),
                ]
        return urlpatterns



@@ 252,10 251,6 @@ def get_pattern(app_name="wiki", namespace="wiki", url_config_class=None):
            url_config_class = get_class_from_str(url_config_classname)
    urlpatterns = url_config_class().get_urls()

    if DJANGO_VERSION < (1, 8):
        from django.conf.urls import patterns
        urlpatterns = patterns('', *urlpatterns)

    return urlpatterns, app_name, namespace