Skip to content

Commit 1ad89a6

Browse files
jasonhildebrandCito
authored andcommitted
Ability to load plugins in non-web situations (#1)
Refactor plugin loading out of Application, to make it possible to load plugins in non-web situations (scripts, cron jobs, etc.). Document how to bootstrap Webware for command-line scripts.
1 parent 02d0456 commit 1ad89a6

File tree

6 files changed

+176
-36
lines changed

6 files changed

+176
-36
lines changed

docs/appdev.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,25 @@ Now run this file in your IDE in debug mode. For instance, in PyCharm, right-cli
363363
Some IDEs like PyCharm can also debug remote processes. This could be useful to debug a test or production server.
364364

365365

366+
Bootstrap Webware from Command line
367+
-----------------------------------
368+
369+
You may be in a situation where you want to execute some part of your Webware app from the command line, for example to implement a cron job or
370+
maintenance script. In these situations you probably don't want to instantiate a full-fledged `Application` -- some of the downsides are that doing so
371+
would cause standard output and standard error to be redirected to the log file, and that it sets up the session sweeper, task manager, etc.
372+
But you may still need access to plugins such as MiscUtils, MiddleKit, which you may not be able to import directly.
373+
374+
Here is a lightweight approach which allows you to bootstrap Webware and plugins::
375+
376+
import webware
377+
webware.add_to_python_path()
378+
webware.load_plugins('/your/app/directory')
379+
380+
# now plugins are available...
381+
import MiscUtils
382+
import MiddleKit
383+
384+
366385
How do I Develop an App?
367386
------------------------
368387

@@ -381,3 +400,5 @@ The answer to that question might not seem clear after being deluged with all th
381400
* With this additional knowledge, create more sophisticated pages.
382401

383402
* If you need to secure your pages using a login screen, you'll want to look at the SecurePage, LoginPage, and SecureCountVisits examples in ``Examples``. You'll need to modify them to suit your particular needs.
403+
404+

webware/Application.py

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919

2020
from time import time, localtime
2121

22-
import pkg_resources
23-
2422
from MiscUtils import NoDefault
2523
from MiscUtils.Funcs import asclocaltime
2624
from MiscUtils.NamedValueAccess import valueForName
@@ -32,9 +30,9 @@
3230
from ExceptionHandler import ExceptionHandler
3331
from HTTPRequest import HTTPRequest
3432
from HTTPExceptions import HTTPException, HTTPSessionExpired
35-
from PlugIn import PlugIn
3633
from Transaction import Transaction
3734
from WSGIStreamOut import WSGIStreamOut
35+
from PlugInLoader import PlugInLoader
3836

3937
import URLParser
4038

@@ -237,6 +235,7 @@ def __init__(self, path=None, settings=None, development=None):
237235
if self.setting('UseSessionSweeper'):
238236
self.startSessionSweeper()
239237

238+
self._plugInLoader = None
240239
self.loadPlugIns()
241240

242241
self._wasShutDown = False
@@ -1093,19 +1092,7 @@ def loadPlugIn(self, name, module):
10931092
May return None if loading was unsuccessful (in which case this method
10941093
prints a message saying so). Used by `loadPlugIns` (note the **s**).
10951094
"""
1096-
try:
1097-
plugIn = PlugIn(self, name, module)
1098-
willNotLoadReason = plugIn.load()
1099-
if willNotLoadReason:
1100-
print(f' Plug-in {name} cannot be loaded because:\n'
1101-
f' {willNotLoadReason}')
1102-
return None
1103-
plugIn.install()
1104-
except Exception:
1105-
print()
1106-
print(f'Plug-in {name} raised exception.')
1107-
raise
1108-
return plugIn
1095+
return self._plugInLoader(name, module)
11091096

11101097
def loadPlugIns(self):
11111098
"""Load all plug-ins.
@@ -1115,24 +1102,8 @@ def loadPlugIns(self):
11151102
Application at startup time, just before listening for requests.
11161103
See the docs in `PlugIn` for more info.
11171104
"""
1118-
plugInNames = set(self.setting('PlugIns'))
1119-
plugInNames.add('Webware')
1120-
plugIns = [
1121-
(entry_point.name, entry_point.load())
1122-
for entry_point
1123-
in pkg_resources.iter_entry_points('webware.plugins')
1124-
if entry_point.name in plugInNames
1125-
]
1126-
1127-
print('Plug-ins list:', ', '.join(
1128-
name for name, _module in plugIns if name != 'Webware'))
1129-
1130-
# Now that we have our plug-in list, load them...
1131-
for name, module in plugIns:
1132-
plugIn = self.loadPlugIn(name, module)
1133-
if plugIn:
1134-
self._plugIns[name] = plugIn
1135-
print()
1105+
self._plugInLoader = loader = PlugInLoader(self)
1106+
self._plugIns = loader.loadPlugIns(self.setting('PlugIns'))
11361107

11371108
# endregion Plug-in loading
11381109

webware/MockApplication.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import os
2+
3+
from ConfigurableForServerSidePath import ConfigurableForServerSidePath
4+
5+
6+
class MockImportManager(object):
7+
8+
def recordFile(self, filename, isfile=None):
9+
pass
10+
11+
12+
defaultConfig = dict(
13+
CacheDir='Cache',
14+
PlugIns=['MiscUtils', 'WebUtils', 'TaskKit', 'UserKit', 'PSP'],
15+
)
16+
17+
18+
class MockApplication(ConfigurableForServerSidePath):
19+
"""
20+
A minimal implementation which is compatible with Application
21+
and which is sufficient to load plugins.
22+
"""
23+
24+
def __init__(self, path=None, settings=None, development=None):
25+
ConfigurableForServerSidePath.__init__(self)
26+
if path is None:
27+
path = os.getcwd()
28+
self._serverSidePath = os.path.abspath(path)
29+
self._webwarePath = os.path.abspath(os.path.dirname(__file__))
30+
if development is None:
31+
development = bool(os.environ.get('WEBWARE_DEVELOPMENT'))
32+
self._development = development
33+
34+
appConfig = self.config() # get and cache the configuration
35+
if settings:
36+
appConfig.update(settings)
37+
self._cacheDir = self.serverSidePath(self.setting('CacheDir') or 'Cache')
38+
from MiscUtils.PropertiesObject import PropertiesObject
39+
props = PropertiesObject(os.path.join(
40+
self._webwarePath, 'Properties.py'))
41+
self._webwareVersion = props['version']
42+
self._webwareVersionString = props['versionString']
43+
self._imp = MockImportManager()
44+
for path in (self._cacheDir,):
45+
if path and not os.path.exists(path):
46+
os.makedirs(path)
47+
48+
def defaultConfig(self):
49+
return defaultConfig
50+
51+
def configReplacementValues(self):
52+
"""Get config values that need to be escaped."""
53+
return dict(
54+
ServerSidePath=self._serverSidePath,
55+
WebwarePath=self._webwarePath,
56+
Development=self._development)
57+
58+
def configFilename(self):
59+
return self.serverSidePath('Configs/Application.config')
60+
61+
def serverSidePath(self, path=None):
62+
if path:
63+
return os.path.normpath(
64+
os.path.join(self._serverSidePath, path))
65+
return self._serverSidePath
66+
67+
def hasContext(self, context):
68+
return False
69+
70+
def addServletFactory(self, factory):
71+
pass

webware/PlugIn.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,14 @@ def __init__(self, application, name, module):
8181
self._cacheDir = os.path.join(self._app._cacheDir, self._name)
8282
self._examplePages = self._examplePagesContext = None
8383

84-
def load(self):
84+
def load(self, verbose=True):
8585
"""Loads the plug-in into memory, but does not yet install it.
8686
8787
Will return None on success, otherwise a message (string) that says
8888
why the plug-in could not be loaded.
8989
"""
90-
print(f'Loading plug-in: {self._name} at {self._path}')
90+
if verbose:
91+
print(f'Loading plug-in: {self._name} at {self._path}')
9192

9293
# Grab the Properties.py
9394
self._properties = PropertiesObject(

webware/PlugInLoader.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pkg_resources
2+
3+
from PlugIn import PlugIn
4+
5+
6+
class PlugInLoader(object):
7+
8+
def __init__(self, app):
9+
self.app = app
10+
self._plugIns = {}
11+
12+
def loadPlugIn(self, name, module, verbose=True):
13+
"""Load and return the given plug-in.
14+
15+
May return None if loading was unsuccessful (in which case this method
16+
prints a message saying so). Used by `loadPlugIns` (note the **s**).
17+
"""
18+
try:
19+
plugIn = PlugIn(self.app, name, module)
20+
willNotLoadReason = plugIn.load(verbose=verbose)
21+
if willNotLoadReason:
22+
print(f' Plug-in {name} cannot be loaded because:\n'
23+
f' {willNotLoadReason}')
24+
return None
25+
plugIn.install()
26+
except Exception:
27+
print()
28+
print(f'Plug-in {name} raised exception.')
29+
raise
30+
return plugIn
31+
32+
def loadPlugIns(self, plugInNames, verbose=True):
33+
"""Load all plug-ins.
34+
35+
A plug-in allows you to extend the functionality of Webware without
36+
necessarily having to modify its source. Plug-ins are loaded by
37+
Application at startup time, just before listening for requests.
38+
See the docs in `PlugIn` for more info.
39+
"""
40+
plugInNames = set(plugInNames)
41+
plugInNames.add('Webware')
42+
plugIns = [
43+
(entry_point.name, entry_point.load())
44+
for entry_point
45+
in pkg_resources.iter_entry_points('webware.plugins')
46+
if entry_point.name in plugInNames
47+
]
48+
49+
if verbose:
50+
print('Plug-ins list:', ', '.join(
51+
name for name, _module in plugIns if name != 'Webware'))
52+
53+
# Now that we have our plug-in list, load them...
54+
for name, module in plugIns:
55+
plugIn = self.loadPlugIn(name, module, verbose=verbose)
56+
if plugIn:
57+
self._plugIns[name] = plugIn
58+
if verbose:
59+
print()
60+
return self._plugIns

webware/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
11
"""Webware for Python"""
2+
3+
import sys
4+
5+
6+
def add_to_python_path():
7+
webwarePath = __path__[0]
8+
if webwarePath not in sys.path:
9+
sys.path.insert(0, webwarePath)
10+
11+
12+
def load_plugins(path, settings=None, development=None):
13+
from MockApplication import MockApplication
14+
from PlugInLoader import PlugInLoader
15+
app = MockApplication(path, settings, development)
16+
loader = PlugInLoader(app)
17+
loader.loadPlugIns(app.setting('PlugIns'), verbose=False)

0 commit comments

Comments
 (0)