Merge "Remove turbogears integration"

This commit is contained in:
Zuul 2019-05-02 18:34:56 +00:00 committed by Gerrit Code Review
commit cf4d3b10ae
9 changed files with 19 additions and 681 deletions

View File

@ -62,14 +62,13 @@ Main features
- Extensible : easy to add more protocols or more base types.
- Framework independence : adapters are provided to easily integrate
your API in any web framework, for example a wsgi container,
Pecan_, TurboGears_, Flask_, cornice_...
Pecan_, Flask_, cornice_...
- Very few runtime dependencies: webob, simplegeneric. Optionnaly lxml and
simplejson if you need better performances.
- Integration in `Sphinx`_ for making clean documentation with
``wsmeext.sphinxext``.
.. _Pecan: http://pecanpy.org/
.. _TurboGears: http://www.turbogears.org/
.. _Flask: http://flask.pocoo.org/
.. _cornice: http://pypi.python.org/pypi/cornice

View File

@ -1,11 +1,26 @@
Changes
=======
0.9.4 (future)
1.0.0 (future)
--------------
* SQLAlchemy support is deprecated and will be removed in one of the next
releases. It has never actually worked to begin with.
* Remove support for turbogears
* Remove SQLAlchemy support. It has never actually worked to begin with.
0.9.2 (2017-02-14)
------------------
TODO.
0.9.1 (2017-01-04)
------------------
Fix packaging issues.
0.9.0 (2017-01-04)
------------------
TODO.
0.8.0 (2015-08-25)
------------------

View File

@ -241,74 +241,6 @@ The `example <http://pecan.readthedocs.org/en/latest/rest.html#nesting-restcontr
.. _adapter-tg1:
Turbogears 1.x
--------------
The TG adapters have an api very similar to TGWebServices. Migrating from it
should be straightforward (a little howto migrate would not hurt though, and it
will be written as soon as possible).
:mod:`wsmeext.tg11` -- TG 1.1 adapter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. module:: wsmeext.tg11
.. function:: wsexpose(return_type, \*arg_types, \*\*options)
See @\ :func:`signature` for parameters documentation.
Can be used on any function of a controller
instead of the expose decorator from TG.
.. function:: wsvalidate(\*arg_types)
Set the argument types of an exposed function. This decorator is provided
so that WSME is an almost drop-in replacement for TGWebServices. If
starting from scratch you can use \ :func:`wsexpose` only
.. function:: adapt(wsroot)
Returns a TG1 controller instance that publish a :class:`wsme.WSRoot`.
It can then be mounted on a TG1 controller.
Because the adapt function modifies the cherrypy filters of the controller
the 'webpath' of the WSRoot instance must be consistent with the path it
will be mounted on.
:mod:`wsmeext.tg15` -- TG 1.5 adapter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. module:: wsmeext.tg15
This adapter has the exact same api as :mod:`wsmeext.tg11`.
Example
~~~~~~~
In a freshly quickstarted tg1 application (let's say, wsmedemo), you can add
REST-ish functions anywhere in your controller tree. Here directly on the root,
in controllers.py:
.. code-block:: python
# ...
# For tg 1.5, import from wsmeext.tg15 instead :
from wsmeext.tg11 import wsexpose, WSRoot
class Root(controllers.RootController):
# Having a WSRoot on /ws is only required to enable additional
# protocols. For REST-only services, it can be ignored.
ws = adapt(
WSRoot(webpath='/ws', protocols=['soap'])
)
@wsexpose(int, int, int)
def multiply(self, a, b):
return a * b
.. _TurboGears: http://www.turbogears.org/
Other frameworks
----------------

View File

@ -14,8 +14,6 @@ be done :
- Implement adapters for other frameworks :
- TurboGears 2
- Pylons
- CherryPy

View File

@ -1,196 +0,0 @@
import wsmeext.tg11
from wsme import WSRoot
from wsmeext.tg11 import wsexpose, wsvalidate
import wsmeext.tg1
from turbogears.controllers import RootController
import cherrypy
import unittest
import simplejson
from wsmeext.tests import test_soap
class WSController(WSRoot):
pass
class Subcontroller(object):
@wsexpose(int, int, int)
def add(self, a, b):
return a + b
class Root(RootController):
class UselessSubClass:
# This class is here only to make sure wsmeext.tg1.scan_api
# does its job properly
pass
ws = WSController(webpath='/ws')
ws.addprotocol(
'soap',
tns=test_soap.tns,
typenamespace=test_soap.typenamespace,
baseURL='/ws/'
)
ws = wsmeext.tg11.adapt(ws)
@wsexpose(int)
@wsvalidate(int, int)
def multiply(self, a, b):
return a * b
@wsexpose(int)
@wsvalidate(int, int)
def divide(self, a, b):
if b == 0:
raise cherrypy.HTTPError(400, 'Cannot divide by zero!')
return a / b
sub = Subcontroller()
from turbogears import testutil, config, startup
class TestController(unittest.TestCase):
root = Root
def setUp(self):
"Tests the output of the index method"
self.app = testutil.make_app(self.root)
testutil.start_server()
def tearDown(self):
# implementation copied from turbogears.testutil.stop_server.
# The only change is that cherrypy.root is set to None
# AFTER stopTurbogears has been called so that wsmeext.tg11
# can correctly uninstall its filter.
if config.get("cp_started"):
cherrypy.server.stop()
config.update({"cp_started": False})
if config.get("server_started"):
startup.stopTurboGears()
config.update({"server_started": False})
def test_restcall(self):
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json'}
)
print response
assert simplejson.loads(response.body) == 50
response = self.app.post("/sub/add",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json'}
)
print response
assert simplejson.loads(response.body) == 15
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json', 'Accept': 'application/json'}
)
print response
assert simplejson.loads(response.body) == 50
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json', 'Accept': 'text/javascript'}
)
print response
assert simplejson.loads(response.body) == 50
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json',
'Accept': 'text/xml'}
)
print response
assert response.body == "<result>50</result>"
def test_custom_clientside_error(self):
response = self.app.post(
"/divide",
simplejson.dumps({'a': 5, 'b': 0}),
{'Content-Type': 'application/json', 'Accept': 'application/json'},
expect_errors=True
)
assert response.status_int == 400
assert simplejson.loads(response.body) == {
"debuginfo": None,
"faultcode": "Server",
"faultstring": "(400, 'Cannot divide by zero!')"
}
response = self.app.post(
"/divide",
simplejson.dumps({'a': 5, 'b': 0}),
{'Content-Type': 'application/json', 'Accept': 'text/xml'},
expect_errors=True
)
assert response.status_int == 400
assert response.body == ("<error><faultcode>Server</faultcode>"
"<faultstring>(400, 'Cannot divide by zero!')"
"</faultstring><debuginfo /></error>")
def test_soap_wsdl(self):
ts = test_soap.TestSOAP('test_wsdl')
ts.app = self.app
ts.ws_path = '/ws/'
ts.run()
#wsdl = self.app.get('/ws/api.wsdl').body
#print wsdl
#assert 'multiply' in wsdl
def test_soap_call(self):
ts = test_soap.TestSOAP('test_wsdl')
ts.app = self.app
ts.ws_path = '/ws/'
print ts.ws_path
assert ts.call('multiply', a=5, b=10, _rt=int) == 50
def test_scan_api_loops(self):
class MyRoot(object):
pass
MyRoot.loop = MyRoot()
root = MyRoot()
api = list(wsmeext.tg1._scan_api(root))
print(api)
self.assertEqual(len(api), 0)
def test_scan_api_maxlen(self):
class ARoot(object):
pass
def make_subcontrollers(n):
c = type('Controller%s' % n, (object,), {})
return c
c = ARoot
for n in range(55):
subc = make_subcontrollers(n)
c.sub = subc()
c = subc
root = ARoot()
self.assertRaises(ValueError, list, wsmeext.tg1._scan_api(root))
def test_templates_content_type(self):
self.assertEqual(
"application/json",
wsmeext.tg1.AutoJSONTemplate().get_content_type('dummy')
)
self.assertEqual(
"text/xml",
wsmeext.tg1.AutoXMLTemplate().get_content_type('dummy')
)

View File

@ -1,177 +0,0 @@
import wsmeext.tg15
from wsme import WSRoot
from turbogears.controllers import RootController
import cherrypy
from wsmeext.tests import test_soap
import simplejson
class Subcontroller(object):
@wsmeext.tg15.wsexpose(int, int, int)
def add(self, a, b):
return a + b
class Root(RootController):
class UselessSubClass:
# This class is here only to make sure wsmeext.tg1.scan_api
# does its job properly
pass
sub = Subcontroller()
ws = WSRoot(webpath='/ws')
ws.addprotocol('soap',
tns=test_soap.tns,
typenamespace=test_soap.typenamespace,
baseURL='/ws/'
)
ws = wsmeext.tg15.adapt(ws)
@wsmeext.tg15.wsexpose(int)
@wsmeext.tg15.wsvalidate(int, int)
def multiply(self, a, b):
return a * b
@wsmeext.tg15.wsexpose(int)
@wsmeext.tg15.wsvalidate(int, int)
def divide(self, a, b):
if b == 0:
raise cherrypy.HTTPError(400, 'Cannot divide by zero!')
return a / b
from turbogears import testutil
class TestController(testutil.TGTest):
root = Root
# def setUp(self):
# "Tests the output of the index method"
# self.app = testutil.make_app(self.root)
# #print cherrypy.root
# testutil.start_server()
# def tearDown(self):
# # implementation copied from turbogears.testutil.stop_server.
# # The only change is that cherrypy.root is set to None
# # AFTER stopTurbogears has been called so that wsmeext.tg15
# # can correctly uninstall its filter.
# if config.get("cp_started"):
# cherrypy.server.stop()
# config.update({"cp_started": False})
#
# if config.get("server_started"):
# startup.stopTurboGears()
# config.update({"server_started": False})
def test_restcall(self):
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json'}
)
print response
assert simplejson.loads(response.body) == 50
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json', 'Accept': 'application/json'}
)
print response
assert simplejson.loads(response.body) == 50
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json', 'Accept': 'text/javascript'}
)
print response
assert simplejson.loads(response.body) == 50
response = self.app.post("/multiply",
simplejson.dumps({'a': 5, 'b': 10}),
{'Content-Type': 'application/json',
'Accept': 'text/xml'}
)
print response
assert response.body == "<result>50</result>"
def test_custom_clientside_error(self):
response = self.app.post(
"/divide",
simplejson.dumps({'a': 5, 'b': 0}),
{'Content-Type': 'application/json', 'Accept': 'application/json'},
expect_errors=True
)
assert response.status_int == 400
assert simplejson.loads(response.body) == {
"debuginfo": None,
"faultcode": "Client",
"faultstring": "(400, 'Cannot divide by zero!')"
}
response = self.app.post(
"/divide",
simplejson.dumps({'a': 5, 'b': 0}),
{'Content-Type': 'application/json', 'Accept': 'text/xml'},
expect_errors=True
)
assert response.status_int == 400
assert response.body == ("<error><faultcode>Client</faultcode>"
"<faultstring>(400, 'Cannot divide by zero!')"
"</faultstring><debuginfo /></error>")
def test_soap_wsdl(self):
wsdl = self.app.get('/ws/api.wsdl').body
print wsdl
assert 'multiply' in wsdl
def test_soap_call(self):
ts = test_soap.TestSOAP('test_wsdl')
ts.app = self.app
ts.ws_path = '/ws/'
print ts.ws_path
assert ts.call('multiply', a=5, b=10, _rt=int) == 50
def test_scan_api_loops(self):
class MyRoot(object):
pass
MyRoot.loop = MyRoot()
root = MyRoot()
api = list(wsmeext.tg1._scan_api(root))
print(api)
self.assertEqual(len(api), 0)
def test_scan_api_maxlen(self):
class ARoot(object):
pass
def make_subcontrollers(n):
c = type('Controller%s' % n, (object,), {})
return c
c = ARoot
for n in range(55):
subc = make_subcontrollers(n)
c.sub = subc()
c = subc
root = ARoot()
self.assertRaises(ValueError, list, wsmeext.tg1._scan_api(root))
def test_templates_content_type(self):
self.assertEqual(
"application/json",
wsmeext.tg1.AutoJSONTemplate().get_content_type('dummy')
)
self.assertEqual(
"text/xml",
wsmeext.tg1.AutoXMLTemplate().get_content_type('dummy')
)

View File

@ -1,173 +0,0 @@
try:
import json
except ImportError:
import simplejson as json # noqa
import functools
import sys
import cherrypy
import webob
from turbogears import expose, util
import turbogears.view
from wsme.rest import validate as wsvalidate
import wsme.api
import wsme.rest
import wsme.rest.args
import wsme.rest.json
from wsme.utils import is_valid_code
import inspect
APIPATH_MAXLEN = 50
__all__ = ['wsexpose', 'wsvalidate']
def wsexpose(*args, **kwargs):
tg_json_expose = expose(
'wsmejson:',
accept_format='application/json',
content_type='application/json',
tg_format='json'
)
tg_altjson_expose = expose(
'wsmejson:',
accept_format='text/javascript',
content_type='application/json'
)
tg_xml_expose = expose(
'wsmexml:',
accept_format='text/xml',
content_type='text/xml',
tg_format='xml'
)
sig = wsme.signature(*args, **kwargs)
def decorate(f):
sig(f)
funcdef = wsme.api.FunctionDefinition.get(f)
@functools.wraps(f)
def callfunction(self, *args, **kwargs):
args, kwargs = wsme.rest.args.get_args(
funcdef, args, kwargs,
cherrypy.request.params, None,
cherrypy.request.body,
cherrypy.request.headers['Content-Type']
)
if funcdef.pass_request:
kwargs[funcdef.pass_request] = cherrypy.request
try:
result = f(self, *args, **kwargs)
except Exception:
try:
exception_info = sys.exc_info()
orig_exception = exception_info[1]
if isinstance(orig_exception, cherrypy.HTTPError):
orig_code = getattr(orig_exception, 'status', None)
else:
orig_code = getattr(orig_exception, 'code', None)
data = wsme.api.format_exception(exception_info)
finally:
del exception_info
cherrypy.response.status = 500
if data['faultcode'] == 'client':
cherrypy.response.status = 400
elif orig_code and is_valid_code(orig_code):
cherrypy.response.status = orig_code
accept = cherrypy.request.headers.get('Accept', "").lower()
accept = util.simplify_http_accept_header(accept)
decorators = {'text/xml': wsme.rest.xml.encode_error}
return decorators.get(
accept,
wsme.rest.json.encode_error
)(None, data)
return dict(
datatype=funcdef.return_type,
result=result
)
callfunction = tg_xml_expose(callfunction)
callfunction = tg_altjson_expose(callfunction)
callfunction = tg_json_expose(callfunction)
callfunction._wsme_original_function = f
return callfunction
return decorate
class AutoJSONTemplate(object):
def __init__(self, extra_vars_func=None, options=None):
pass
def render(self, info, format="json", fragment=False, template=None):
"Renders the template to a string using the provided info."
return wsme.rest.json.encode_result(
info['result'], info['datatype']
)
def get_content_type(self, user_agent):
return "application/json"
class AutoXMLTemplate(object):
def __init__(self, extra_vars_func=None, options=None):
pass
def render(self, info, format="json", fragment=False, template=None):
"Renders the template to a string using the provided info."
return wsme.rest.xml.encode_result(
info['result'], info['datatype']
)
def get_content_type(self, user_agent):
return "text/xml"
turbogears.view.engines['wsmejson'] = AutoJSONTemplate(turbogears.view.stdvars)
turbogears.view.engines['wsmexml'] = AutoXMLTemplate(turbogears.view.stdvars)
class Controller(object):
def __init__(self, wsroot):
self._wsroot = wsroot
@expose()
def default(self, *args, **kw):
req = webob.Request(cherrypy.request.wsgi_environ)
res = self._wsroot._handle_request(req)
cherrypy.response.header_list = res.headerlist
cherrypy.response.status = res.status
return res.body
def _scan_api(controller, path=[], objects=[]):
"""
Recursively iterate a controller api entries.
"""
for name in dir(controller):
if name.startswith('_'):
continue
a = getattr(controller, name)
if a in objects:
continue
if inspect.ismethod(a):
if wsme.api.iswsmefunction(a):
yield path + [name], a._wsme_original_function, [controller]
elif inspect.isclass(a):
continue
else:
if len(path) > APIPATH_MAXLEN:
raise ValueError("Path is too long: " + str(path))
for i in _scan_api(a, path + [name], objects + [a]):
yield i
def scan_api(root=None):
return _scan_api(cherrypy.root)

View File

@ -1,40 +0,0 @@
from turbogears import config
import cherrypy
from cherrypy.filters.basefilter import BaseFilter
from turbogears.startup import call_on_startup, call_on_shutdown
from wsmeext.tg1 import wsexpose, wsvalidate
import wsmeext.tg1
__all__ = ['adapt', 'wsexpose', 'wsvalidate']
class WSMECherrypyFilter(BaseFilter):
def __init__(self, controller):
self.controller = controller
self.webpath = None
def on_start_resource(self):
path = cherrypy.request.path
if path.startswith(self.controller._wsroot._webpath):
cherrypy.request.processRequestBody = False
def adapt(wsroot):
wsroot._scan_api = wsmeext.tg1.scan_api
controller = wsmeext.tg1.Controller(wsroot)
filter_ = WSMECherrypyFilter(controller)
def install_filter():
filter_.webpath = config.get('server.webpath') or ''
controller._wsroot._webpath = \
filter_.webpath + controller._wsroot._webpath
cherrypy.root._cp_filters.append(filter_)
def uninstall_filter():
cherrypy.root._cp_filters.remove(filter_)
controller._wsroot._webpath = \
controller._wsroot._webpath[len(filter_.webpath):]
call_on_startup.append(install_filter)
call_on_shutdown.insert(0, uninstall_filter)
return controller

View File

@ -1,20 +0,0 @@
import cherrypy
from wsmeext.tg1 import wsexpose, wsvalidate
import wsmeext.tg1
__all__ = ['adapt', 'wsexpose', 'wsvalidate']
def scan_api(root=None):
for baseurl, instance in cherrypy.tree.apps.items():
path = [token for token in baseurl.split('/') if token]
for i in wsmeext.tg1._scan_api(instance.root, path):
yield i
def adapt(wsroot):
wsroot._scan_api = scan_api
controller = wsmeext.tg1.Controller(wsroot)
return controller