Add app providers, tests, docs
This commit is contained in:
parent
3cafa28ab9
commit
3a8bd1fce9
|
@ -42,6 +42,10 @@ Glossary
|
||||||
|
|
||||||
See also the human-friendly :term:`app title`.
|
See also the human-friendly :term:`app title`.
|
||||||
|
|
||||||
|
app provider
|
||||||
|
A :term:`provider` which pertains to the :term:`app handler`.
|
||||||
|
See :doc:`narr/providers/app`.
|
||||||
|
|
||||||
app title
|
app title
|
||||||
Human-friendly name for the :term:`app` (e.g. "Wutta Poser").
|
Human-friendly name for the :term:`app` (e.g. "Wutta Poser").
|
||||||
|
|
||||||
|
@ -104,6 +108,11 @@ Glossary
|
||||||
modules etc. which is installed via ``pip``. See also
|
modules etc. which is installed via ``pip``. See also
|
||||||
:doc:`narr/install/pkg`.
|
:doc:`narr/install/pkg`.
|
||||||
|
|
||||||
|
provider
|
||||||
|
Python object which "provides" extra functionality to some
|
||||||
|
portion of the :term:`app`. Similar to a "plugin" concept; see
|
||||||
|
:doc:`narr/providers/index`.
|
||||||
|
|
||||||
settings table
|
settings table
|
||||||
Table in the :term:`app database` which is used to store
|
Table in the :term:`app database` which is used to store
|
||||||
:term:`config settings<config setting>`. See also
|
:term:`config settings<config setting>`. See also
|
||||||
|
|
|
@ -9,4 +9,5 @@ Documentation
|
||||||
config/index
|
config/index
|
||||||
cli/index
|
cli/index
|
||||||
handlers/index
|
handlers/index
|
||||||
|
providers/index
|
||||||
db/index
|
db/index
|
||||||
|
|
57
docs/narr/providers/app.rst
Normal file
57
docs/narr/providers/app.rst
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
App Providers
|
||||||
|
=============
|
||||||
|
|
||||||
|
An :term:`app provider` is a :term:`provider` which can "extend" the
|
||||||
|
main :term:`app handler`.
|
||||||
|
|
||||||
|
The provider generally does this by adding extra methods to the app
|
||||||
|
handler. Note that it does this regardless of which app handler is
|
||||||
|
configured to be used.
|
||||||
|
|
||||||
|
:class:`~wuttjamaican.app.AppProvider` is the base class.
|
||||||
|
|
||||||
|
|
||||||
|
Adding a new Provider
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
First define your provider class. Note that the method names should
|
||||||
|
include a "prefix" unique to your project (``poser_`` in this case).
|
||||||
|
This is to avoid naming collisions with the app handler itself, as
|
||||||
|
well as other app providers. So e.g. in ``poser/app.py``::
|
||||||
|
|
||||||
|
from wuttjamaican.app import AppProvider
|
||||||
|
|
||||||
|
class PoserAppProvider(AppProvider):
|
||||||
|
"""
|
||||||
|
App provider for Poser system
|
||||||
|
"""
|
||||||
|
|
||||||
|
# nb. method name uses 'poser_' prefix
|
||||||
|
def poser_do_something(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Do something for Poser
|
||||||
|
"""
|
||||||
|
print("did something")
|
||||||
|
|
||||||
|
Register the :term:`entry point` in your ``setup.cfg``:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
|
||||||
|
wutta.providers =
|
||||||
|
poser = poser.app:PoserAppProvider
|
||||||
|
|
||||||
|
Assuming you have not customized the app handler proper, then you will
|
||||||
|
be using the *default* app handler yet it will behave as though it has
|
||||||
|
the "provided" methods::
|
||||||
|
|
||||||
|
from wuttjamaican.conf import make_config
|
||||||
|
|
||||||
|
# make normal app
|
||||||
|
config = make_config()
|
||||||
|
app = config.get_app()
|
||||||
|
|
||||||
|
# whatever this does..
|
||||||
|
app.poser_do_something()
|
39
docs/narr/providers/arch.rst
Normal file
39
docs/narr/providers/arch.rst
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
Architecture
|
||||||
|
============
|
||||||
|
|
||||||
|
:term:`Providers<provider>` are similar to a "plugin" concept in that
|
||||||
|
multiple providers may be installed by different
|
||||||
|
:term:`packages<package>`. But whereas plugins are typically limited
|
||||||
|
to a particular interface (method list/signatures etc.) a provider can
|
||||||
|
also "bolt on" entirely new methods which may be used elsewhere in the
|
||||||
|
:term:`app`.
|
||||||
|
|
||||||
|
In that sense providers can perhaps be more accurately thought of as
|
||||||
|
"extensions" rather than plugins.
|
||||||
|
|
||||||
|
Providers may be related to :term:`handlers<handler>` in some cases,
|
||||||
|
but not all. But whereas there is only *one handler* configured for a
|
||||||
|
given portion of the app, multiple providers of the same type would
|
||||||
|
*all contribute* to the overall app. In other words they are always
|
||||||
|
enabled if installed. (Some may require a :term:`config setting` to
|
||||||
|
be "active" - but that is up to each provider.)
|
||||||
|
|
||||||
|
There can be many "types" of providers; each pertains to a certain
|
||||||
|
aspect of the overall app. A given type of provider will apply to a
|
||||||
|
certain "parent" class.
|
||||||
|
|
||||||
|
|
||||||
|
What a Provider Does
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Each type of provider pertains to a certain parent class. The app
|
||||||
|
itself will define the need for a provider type.
|
||||||
|
|
||||||
|
For instance there might be a "dashboard" class which can show various
|
||||||
|
blocks of info (charts etc.). Providers might be used to supplement the
|
||||||
|
parent dashboard class, by adding extra blocks to the display.
|
||||||
|
|
||||||
|
But in that example, providers look an awful lot like plugins.
|
||||||
|
|
||||||
|
For a better (and real) example see :doc:`app`.
|
10
docs/narr/providers/index.rst
Normal file
10
docs/narr/providers/index.rst
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
Providers
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
overview
|
||||||
|
arch
|
||||||
|
app
|
13
docs/narr/providers/overview.rst
Normal file
13
docs/narr/providers/overview.rst
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
The :term:`provider` concept is a way to "supplement" the main app
|
||||||
|
logic. It is different from a :term:`handler` though:
|
||||||
|
|
||||||
|
Providers are *more* analagous to "plugins" than are handlers. For
|
||||||
|
instance multiple :term:`app providers<app provider>` may be installed
|
||||||
|
by various packages and *each of these* will supplement the (one and
|
||||||
|
only) :term:`app handler`. See also :doc:`arch`.
|
||||||
|
|
||||||
|
So far there is only one provider type defined; see :doc:`app`.
|
|
@ -25,6 +25,7 @@ WuttJamaican - app handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
|
|
||||||
from wuttjamaican.util import load_entry_points, load_object, parse_bool
|
from wuttjamaican.util import load_entry_points, load_object, parse_bool
|
||||||
|
|
||||||
|
@ -46,6 +47,11 @@ class AppHandler:
|
||||||
|
|
||||||
:param config: Config object for the app. This should be an
|
:param config: Config object for the app. This should be an
|
||||||
instance of :class:`~wuttjamaican.conf.WuttaConfig`.
|
instance of :class:`~wuttjamaican.conf.WuttaConfig`.
|
||||||
|
|
||||||
|
.. attribute:: providers
|
||||||
|
|
||||||
|
Dictionary of :class:`AppProvider` instances, as returned by
|
||||||
|
:meth:`get_all_providers()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
|
@ -66,6 +72,47 @@ class AppHandler:
|
||||||
"""
|
"""
|
||||||
return self.config.appname
|
return self.config.appname
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
"""
|
||||||
|
Custom attribute getter, called when the app handler does not
|
||||||
|
already have an attribute named with ``name``.
|
||||||
|
|
||||||
|
This will delegate to the set of :term:`app providers<app
|
||||||
|
provider>`; the first provider with an appropriately-named
|
||||||
|
attribute wins, and that value is returned.
|
||||||
|
|
||||||
|
:returns: The first value found among the set of app
|
||||||
|
providers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if name == 'providers':
|
||||||
|
self.providers = self.get_all_providers()
|
||||||
|
return self.providers
|
||||||
|
|
||||||
|
# if 'providers' not in self.__dict__:
|
||||||
|
# self.__dict__['providers'] = self.get_all_providers()
|
||||||
|
|
||||||
|
for provider in self.providers.values():
|
||||||
|
if hasattr(provider, name):
|
||||||
|
return getattr(provider, name)
|
||||||
|
|
||||||
|
return super().__getattr__(name)
|
||||||
|
|
||||||
|
def get_all_providers(self):
|
||||||
|
"""
|
||||||
|
Load and return all registered providers.
|
||||||
|
|
||||||
|
Note that you do not need to call this directly; instead just
|
||||||
|
use :attr:`providers`.
|
||||||
|
|
||||||
|
:returns: Dictionary keyed by entry point name; values are
|
||||||
|
:class:`AppProvider` *instances*.
|
||||||
|
"""
|
||||||
|
providers = load_entry_points(f'{self.appname}.providers')
|
||||||
|
for key in list(providers):
|
||||||
|
providers[key] = providers[key](self.config)
|
||||||
|
return providers
|
||||||
|
|
||||||
def make_appdir(self, path, subfolders=None, **kwargs):
|
def make_appdir(self, path, subfolders=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Establish an :term:`app dir` at the given path.
|
Establish an :term:`app dir` at the given path.
|
||||||
|
@ -201,3 +248,36 @@ class AppHandler:
|
||||||
from .db import get_setting
|
from .db import get_setting
|
||||||
|
|
||||||
return get_setting(session, name)
|
return get_setting(session, name)
|
||||||
|
|
||||||
|
|
||||||
|
class AppProvider:
|
||||||
|
"""
|
||||||
|
Base class for :term:`app providers<app provider>`.
|
||||||
|
|
||||||
|
These can add arbitrary extra functionality to the main :term:`app
|
||||||
|
handler`. See also :doc:`/narr/providers/app`.
|
||||||
|
|
||||||
|
:param config: Config object for the app. This should be an
|
||||||
|
instance of :class:`~wuttjamaican.conf.WuttaConfig`.
|
||||||
|
|
||||||
|
Instances have the following attributes:
|
||||||
|
|
||||||
|
.. attribute:: config
|
||||||
|
|
||||||
|
Reference to the config object.
|
||||||
|
|
||||||
|
.. attribute:: app
|
||||||
|
|
||||||
|
Reference to the parent app handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
|
||||||
|
if isinstance(config, AppHandler):
|
||||||
|
warnings.warn("passing app handler to app provider is deprecated; "
|
||||||
|
"must pass config object instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
config = config.config
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
self.app = config.get_app()
|
||||||
|
|
|
@ -20,6 +20,7 @@ class TestAppHandler(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.config = WuttaConfig(appname='wuttatest')
|
self.config = WuttaConfig(appname='wuttatest')
|
||||||
self.app = app.AppHandler(self.config)
|
self.app = app.AppHandler(self.config)
|
||||||
|
self.config.app = self.app
|
||||||
|
|
||||||
def test_init(self):
|
def test_init(self):
|
||||||
self.assertIs(self.app.config, self.config)
|
self.assertIs(self.app.config, self.config)
|
||||||
|
@ -109,3 +110,81 @@ class TestAppHandler(TestCase):
|
||||||
session.execute(sa.text("insert into setting values ('foo', 'bar');"))
|
session.execute(sa.text("insert into setting values ('foo', 'bar');"))
|
||||||
value = self.app.get_setting(session, 'foo')
|
value = self.app.get_setting(session, 'foo')
|
||||||
self.assertEqual(value, 'bar')
|
self.assertEqual(value, 'bar')
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppProvider(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.config = WuttaConfig(appname='wuttatest')
|
||||||
|
self.app = app.AppHandler(self.config)
|
||||||
|
self.config.app = self.app
|
||||||
|
|
||||||
|
def test_constructor(self):
|
||||||
|
|
||||||
|
# config object is expected
|
||||||
|
provider = app.AppProvider(self.config)
|
||||||
|
self.assertIs(provider.config, self.config)
|
||||||
|
self.assertIs(provider.app, self.app)
|
||||||
|
|
||||||
|
# but can pass app handler instead
|
||||||
|
provider = app.AppProvider(self.app)
|
||||||
|
self.assertIs(provider.config, self.config)
|
||||||
|
self.assertIs(provider.app, self.app)
|
||||||
|
|
||||||
|
def test_get_all_providers(self):
|
||||||
|
|
||||||
|
class FakeProvider(app.AppProvider):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# nb. we specify *classes* here
|
||||||
|
fake_providers = {'fake': FakeProvider}
|
||||||
|
|
||||||
|
with patch('wuttjamaican.app.load_entry_points') as load_entry_points:
|
||||||
|
load_entry_points.return_value = fake_providers
|
||||||
|
|
||||||
|
# sanity check, we get *instances* back from this
|
||||||
|
providers = self.app.get_all_providers()
|
||||||
|
load_entry_points.assert_called_once_with('wuttatest.providers')
|
||||||
|
self.assertEqual(len(providers), 1)
|
||||||
|
self.assertIn('fake', providers)
|
||||||
|
self.assertIsInstance(providers['fake'], FakeProvider)
|
||||||
|
|
||||||
|
def test_hasattr(self):
|
||||||
|
|
||||||
|
class FakeProvider(app.AppProvider):
|
||||||
|
def fake_foo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.app.providers = {'fake': FakeProvider(self.config)}
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(self.app, 'fake_foo'))
|
||||||
|
self.assertFalse(hasattr(self.app, 'fake_method_does_not_exist'))
|
||||||
|
|
||||||
|
def test_getattr(self):
|
||||||
|
|
||||||
|
class FakeProvider(app.AppProvider):
|
||||||
|
def fake_foo(self):
|
||||||
|
return 42
|
||||||
|
|
||||||
|
# nb. using instances here
|
||||||
|
fake_providers = {'fake': FakeProvider(self.config)}
|
||||||
|
|
||||||
|
with patch.object(self.app, 'get_all_providers') as get_all_providers:
|
||||||
|
get_all_providers.return_value = fake_providers
|
||||||
|
|
||||||
|
self.assertNotIn('providers', self.app.__dict__)
|
||||||
|
self.assertIs(self.app.providers, fake_providers)
|
||||||
|
get_all_providers.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_getattr_providers(self):
|
||||||
|
|
||||||
|
# collection of providers is loaded on demand
|
||||||
|
self.assertNotIn('providers', self.app.__dict__)
|
||||||
|
self.assertIsNotNone(self.app.providers)
|
||||||
|
|
||||||
|
# custom attr does not exist yet
|
||||||
|
self.assertRaises(AttributeError, getattr, self.app, 'foo_value')
|
||||||
|
|
||||||
|
# but provider can supply the attr
|
||||||
|
self.app.providers['mytest'] = MagicMock(foo_value='bar')
|
||||||
|
self.assertEqual(self.app.foo_value, 'bar')
|
||||||
|
|
Loading…
Reference in a new issue