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`.
|
||||
|
||||
app provider
|
||||
A :term:`provider` which pertains to the :term:`app handler`.
|
||||
See :doc:`narr/providers/app`.
|
||||
|
||||
app title
|
||||
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
|
||||
: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
|
||||
Table in the :term:`app database` which is used to store
|
||||
:term:`config settings<config setting>`. See also
|
||||
|
|
|
@ -9,4 +9,5 @@ Documentation
|
|||
config/index
|
||||
cli/index
|
||||
handlers/index
|
||||
providers/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 warnings
|
||||
|
||||
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
|
||||
instance of :class:`~wuttjamaican.conf.WuttaConfig`.
|
||||
|
||||
.. attribute:: providers
|
||||
|
||||
Dictionary of :class:`AppProvider` instances, as returned by
|
||||
:meth:`get_all_providers()`.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
|
@ -66,6 +72,47 @@ class AppHandler:
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
Establish an :term:`app dir` at the given path.
|
||||
|
@ -201,3 +248,36 @@ class AppHandler:
|
|||
from .db import get_setting
|
||||
|
||||
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):
|
||||
self.config = WuttaConfig(appname='wuttatest')
|
||||
self.app = app.AppHandler(self.config)
|
||||
self.config.app = self.app
|
||||
|
||||
def test_init(self):
|
||||
self.assertIs(self.app.config, self.config)
|
||||
|
@ -109,3 +110,81 @@ class TestAppHandler(TestCase):
|
|||
session.execute(sa.text("insert into setting values ('foo', 'bar');"))
|
||||
value = self.app.get_setting(session, 'foo')
|
||||
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