You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
10 KiB
299 lines
10 KiB
5 years ago
|
"""
|
||
|
Helper functions for CP apps
|
||
|
"""
|
||
|
|
||
|
import six
|
||
|
|
||
|
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
|
||
|
from cherrypy._cpcompat import text_or_bytes
|
||
|
|
||
|
import cherrypy
|
||
|
|
||
|
|
||
|
def expose(func=None, alias=None):
|
||
|
"""
|
||
|
Expose the function or class, optionally providing an alias or set of aliases.
|
||
|
"""
|
||
|
def expose_(func):
|
||
|
func.exposed = True
|
||
|
if alias is not None:
|
||
|
if isinstance(alias, text_or_bytes):
|
||
|
parents[alias.replace('.', '_')] = func
|
||
|
else:
|
||
|
for a in alias:
|
||
|
parents[a.replace('.', '_')] = func
|
||
|
return func
|
||
|
|
||
|
import sys
|
||
|
import types
|
||
|
decoratable_types = types.FunctionType, types.MethodType, type,
|
||
|
if six.PY2:
|
||
|
# Old-style classes are type types.ClassType.
|
||
|
decoratable_types += types.ClassType,
|
||
|
if isinstance(func, decoratable_types):
|
||
|
if alias is None:
|
||
|
# @expose
|
||
|
func.exposed = True
|
||
|
return func
|
||
|
else:
|
||
|
# func = expose(func, alias)
|
||
|
parents = sys._getframe(1).f_locals
|
||
|
return expose_(func)
|
||
|
elif func is None:
|
||
|
if alias is None:
|
||
|
# @expose()
|
||
|
parents = sys._getframe(1).f_locals
|
||
|
return expose_
|
||
|
else:
|
||
|
# @expose(alias="alias") or
|
||
|
# @expose(alias=["alias1", "alias2"])
|
||
|
parents = sys._getframe(1).f_locals
|
||
|
return expose_
|
||
|
else:
|
||
|
# @expose("alias") or
|
||
|
# @expose(["alias1", "alias2"])
|
||
|
parents = sys._getframe(1).f_locals
|
||
|
alias = func
|
||
|
return expose_
|
||
|
|
||
|
|
||
|
def popargs(*args, **kwargs):
|
||
|
"""A decorator for _cp_dispatch
|
||
|
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
|
||
|
|
||
|
Optional keyword argument: handler=(Object or Function)
|
||
|
|
||
|
Provides a _cp_dispatch function that pops off path segments into
|
||
|
cherrypy.request.params under the names specified. The dispatch
|
||
|
is then forwarded on to the next vpath element.
|
||
|
|
||
|
Note that any existing (and exposed) member function of the class that
|
||
|
popargs is applied to will override that value of the argument. For
|
||
|
instance, if you have a method named "list" on the class decorated with
|
||
|
popargs, then accessing "/list" will call that function instead of popping
|
||
|
it off as the requested parameter. This restriction applies to all
|
||
|
_cp_dispatch functions. The only way around this restriction is to create
|
||
|
a "blank class" whose only function is to provide _cp_dispatch.
|
||
|
|
||
|
If there are path elements after the arguments, or more arguments
|
||
|
are requested than are available in the vpath, then the 'handler'
|
||
|
keyword argument specifies the next object to handle the parameterized
|
||
|
request. If handler is not specified or is None, then self is used.
|
||
|
If handler is a function rather than an instance, then that function
|
||
|
will be called with the args specified and the return value from that
|
||
|
function used as the next object INSTEAD of adding the parameters to
|
||
|
cherrypy.request.args.
|
||
|
|
||
|
This decorator may be used in one of two ways:
|
||
|
|
||
|
As a class decorator:
|
||
|
@cherrypy.popargs('year', 'month', 'day')
|
||
|
class Blog:
|
||
|
def index(self, year=None, month=None, day=None):
|
||
|
#Process the parameters here; any url like
|
||
|
#/, /2009, /2009/12, or /2009/12/31
|
||
|
#will fill in the appropriate parameters.
|
||
|
|
||
|
def create(self):
|
||
|
#This link will still be available at /create. Defined functions
|
||
|
#take precedence over arguments.
|
||
|
|
||
|
Or as a member of a class:
|
||
|
class Blog:
|
||
|
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
|
||
|
#...
|
||
|
|
||
|
The handler argument may be used to mix arguments with built in functions.
|
||
|
For instance, the following setup allows different activities at the
|
||
|
day, month, and year level:
|
||
|
|
||
|
class DayHandler:
|
||
|
def index(self, year, month, day):
|
||
|
#Do something with this day; probably list entries
|
||
|
|
||
|
def delete(self, year, month, day):
|
||
|
#Delete all entries for this day
|
||
|
|
||
|
@cherrypy.popargs('day', handler=DayHandler())
|
||
|
class MonthHandler:
|
||
|
def index(self, year, month):
|
||
|
#Do something with this month; probably list entries
|
||
|
|
||
|
def delete(self, year, month):
|
||
|
#Delete all entries for this month
|
||
|
|
||
|
@cherrypy.popargs('month', handler=MonthHandler())
|
||
|
class YearHandler:
|
||
|
def index(self, year):
|
||
|
#Do something with this year
|
||
|
|
||
|
#...
|
||
|
|
||
|
@cherrypy.popargs('year', handler=YearHandler())
|
||
|
class Root:
|
||
|
def index(self):
|
||
|
#...
|
||
|
|
||
|
"""
|
||
|
|
||
|
# Since keyword arg comes after *args, we have to process it ourselves
|
||
|
# for lower versions of python.
|
||
|
|
||
|
handler = None
|
||
|
handler_call = False
|
||
|
for k, v in kwargs.items():
|
||
|
if k == 'handler':
|
||
|
handler = v
|
||
|
else:
|
||
|
raise TypeError(
|
||
|
"cherrypy.popargs() got an unexpected keyword argument '{0}'"
|
||
|
.format(k)
|
||
|
)
|
||
|
|
||
|
import inspect
|
||
|
|
||
|
if handler is not None \
|
||
|
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
|
||
|
handler_call = True
|
||
|
|
||
|
def decorated(cls_or_self=None, vpath=None):
|
||
|
if inspect.isclass(cls_or_self):
|
||
|
# cherrypy.popargs is a class decorator
|
||
|
cls = cls_or_self
|
||
|
setattr(cls, cherrypy.dispatch.Dispatcher.dispatch_method_name, decorated)
|
||
|
return cls
|
||
|
|
||
|
# We're in the actual function
|
||
|
self = cls_or_self
|
||
|
parms = {}
|
||
|
for arg in args:
|
||
|
if not vpath:
|
||
|
break
|
||
|
parms[arg] = vpath.pop(0)
|
||
|
|
||
|
if handler is not None:
|
||
|
if handler_call:
|
||
|
return handler(**parms)
|
||
|
else:
|
||
|
cherrypy.request.params.update(parms)
|
||
|
return handler
|
||
|
|
||
|
cherrypy.request.params.update(parms)
|
||
|
|
||
|
# If we are the ultimate handler, then to prevent our _cp_dispatch
|
||
|
# from being called again, we will resolve remaining elements through
|
||
|
# getattr() directly.
|
||
|
if vpath:
|
||
|
return getattr(self, vpath.pop(0), None)
|
||
|
else:
|
||
|
return self
|
||
|
|
||
|
return decorated
|
||
|
|
||
|
|
||
|
def url(path='', qs='', script_name=None, base=None, relative=None):
|
||
|
"""Create an absolute URL for the given path.
|
||
|
|
||
|
If 'path' starts with a slash ('/'), this will return
|
||
|
(base + script_name + path + qs).
|
||
|
If it does not start with a slash, this returns
|
||
|
(base + script_name [+ request.path_info] + path + qs).
|
||
|
|
||
|
If script_name is None, cherrypy.request will be used
|
||
|
to find a script_name, if available.
|
||
|
|
||
|
If base is None, cherrypy.request.base will be used (if available).
|
||
|
Note that you can use cherrypy.tools.proxy to change this.
|
||
|
|
||
|
Finally, note that this function can be used to obtain an absolute URL
|
||
|
for the current request path (minus the querystring) by passing no args.
|
||
|
If you call url(qs=cherrypy.request.query_string), you should get the
|
||
|
original browser URL (assuming no internal redirections).
|
||
|
|
||
|
If relative is None or not provided, request.app.relative_urls will
|
||
|
be used (if available, else False). If False, the output will be an
|
||
|
absolute URL (including the scheme, host, vhost, and script_name).
|
||
|
If True, the output will instead be a URL that is relative to the
|
||
|
current request path, perhaps including '..' atoms. If relative is
|
||
|
the string 'server', the output will instead be a URL that is
|
||
|
relative to the server root; i.e., it will start with a slash.
|
||
|
"""
|
||
|
if isinstance(qs, (tuple, list, dict)):
|
||
|
qs = _urlencode(qs)
|
||
|
if qs:
|
||
|
qs = '?' + qs
|
||
|
|
||
|
if cherrypy.request.app:
|
||
|
if not path.startswith('/'):
|
||
|
# Append/remove trailing slash from path_info as needed
|
||
|
# (this is to support mistyped URL's without redirecting;
|
||
|
# if you want to redirect, use tools.trailing_slash).
|
||
|
pi = cherrypy.request.path_info
|
||
|
if cherrypy.request.is_index is True:
|
||
|
if not pi.endswith('/'):
|
||
|
pi = pi + '/'
|
||
|
elif cherrypy.request.is_index is False:
|
||
|
if pi.endswith('/') and pi != '/':
|
||
|
pi = pi[:-1]
|
||
|
|
||
|
if path == '':
|
||
|
path = pi
|
||
|
else:
|
||
|
path = _urljoin(pi, path)
|
||
|
|
||
|
if script_name is None:
|
||
|
script_name = cherrypy.request.script_name
|
||
|
if base is None:
|
||
|
base = cherrypy.request.base
|
||
|
|
||
|
newurl = base + script_name + path + qs
|
||
|
else:
|
||
|
# No request.app (we're being called outside a request).
|
||
|
# We'll have to guess the base from server.* attributes.
|
||
|
# This will produce very different results from the above
|
||
|
# if you're using vhosts or tools.proxy.
|
||
|
if base is None:
|
||
|
base = cherrypy.server.base()
|
||
|
|
||
|
path = (script_name or '') + path
|
||
|
newurl = base + path + qs
|
||
|
|
||
|
if './' in newurl:
|
||
|
# Normalize the URL by removing ./ and ../
|
||
|
atoms = []
|
||
|
for atom in newurl.split('/'):
|
||
|
if atom == '.':
|
||
|
pass
|
||
|
elif atom == '..':
|
||
|
atoms.pop()
|
||
|
else:
|
||
|
atoms.append(atom)
|
||
|
newurl = '/'.join(atoms)
|
||
|
|
||
|
# At this point, we should have a fully-qualified absolute URL.
|
||
|
|
||
|
if relative is None:
|
||
|
relative = getattr(cherrypy.request.app, 'relative_urls', False)
|
||
|
|
||
|
# See http://www.ietf.org/rfc/rfc2396.txt
|
||
|
if relative == 'server':
|
||
|
# "A relative reference beginning with a single slash character is
|
||
|
# termed an absolute-path reference, as defined by <abs_path>..."
|
||
|
# This is also sometimes called "server-relative".
|
||
|
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
|
||
|
elif relative:
|
||
|
# "A relative reference that does not begin with a scheme name
|
||
|
# or a slash character is termed a relative-path reference."
|
||
|
old = url(relative=False).split('/')[:-1]
|
||
|
new = newurl.split('/')
|
||
|
while old and new:
|
||
|
a, b = old[0], new[0]
|
||
|
if a != b:
|
||
|
break
|
||
|
old.pop(0)
|
||
|
new.pop(0)
|
||
|
new = (['..'] * len(old)) + new
|
||
|
newurl = '/'.join(new)
|
||
|
|
||
|
return newurl
|