diff --git a/gluon/compileapp.py b/gluon/compileapp.py index e7c2b7e8..7d190e30 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ @@ -14,93 +13,43 @@ Note: """ import re -import fnmatch -import os, sys -import copy import random -from gluon._compat import builtin, PY2, unicodeT, to_native, to_bytes, iteritems, integer_types, basestring, reduce, xrange, long, reload -from gluon.storage import Storage, List -from gluon.template import parse_template -from gluon.restricted import restricted, compile2 -from gluon.fileutils import mktree, listdir, read_file, write_file -from gluon.myregex import regex_expose, regex_longcomments -from gluon.languages import TranslatorFactory -from gluon.dal import DAL, Field -from gluon.validators import Validator -from pydal.base import BaseAdapter -from gluon.sqlhtml import SQLFORM, SQLTABLE -from gluon.cache import Cache -from gluon.globals import current, Response -from gluon import settings -from gluon.cfs import getcfs -from gluon import html -from gluon import validators -from gluon.http import HTTP, redirect -import marshal -import shutil -import imp -import logging import types +import copy from functools import reduce -from gluon import rewrite -from gluon.custom_import import custom_import_install import py_compile +import os +from os.path import join as pjoin, exists +import sys +import imp +import marshal +import fnmatch +import shutil -logger = logging.getLogger("web2py") +from gluon._compat import (builtin, PY2, unicodeT, to_native, to_bytes, + iteritems, integer_types, basestring, xrange, reload) +from gluon.storage import Storage, List +from gluon.globals import current, Response +from gluon import html, validators, rewrite +from gluon.http import HTTP, redirect +from gluon.dal import DAL, Field +from gluon.sqlhtml import SQLFORM, SQLTABLE +from gluon.languages import TranslatorFactory +from gluon.cache import Cache +from gluon.validators import Validator +from gluon.settings import global_settings +from pydal.base import BaseAdapter +from gluon.custom_import import custom_import_install +from gluon.fileutils import mktree, listdir, read_file, write_file, abspath +from gluon.template import parse_template +from gluon.cfs import getcfs +from gluon.restricted import restricted, compile2 +from gluon.admin import add_path_first -is_pypy = settings.global_settings.is_pypy -is_gae = settings.global_settings.web2py_runtime_gae -is_jython = settings.global_settings.is_jython -pjoin = os.path.join - -if PY2: - marshal_header_size = 8 -else: - marshal_header_size = 16 if sys.version_info[1] >= 7 else 12 - -TEST_CODE = \ - r""" -def _TEST(): - import doctest, sys, cStringIO, types, cgi, gluon.fileutils - if not gluon.fileutils.check_credentials(request): - raise HTTP(401, web2py_error='invalid credentials') - stdout = sys.stdout - html = '

Testing controller "%s.py" ... done.


\n' \ - % request.controller - for key in sorted([key for key in globals() if not key in __symbols__+['_TEST']]): - eval_key = eval(key) - if type(eval_key) == types.FunctionType: - number_doctests = sum([len(ds.examples) for ds in doctest.DocTestFinder().find(eval_key)]) - if number_doctests>0: - sys.stdout = cStringIO.StringIO() - name = '%s/controllers/%s.py in %s.__doc__' \ - % (request.folder, request.controller, key) - doctest.run_docstring_examples(eval_key, - globals(), False, name=name) - report = sys.stdout.getvalue().strip() - if report: - pf = 'failed' - else: - pf = 'passed' - html += '

Function %s [%s]

\n' \ - % (pf, key, pf) - if report: - html += CODE(report, language='web2py', \ - link='/examples/global/vars/').xml() - html += '
\n' - else: - html += \ - '

Function %s [no doctests]


\n' \ - % (key) - response._vars = html - sys.stdout = stdout -_TEST() -""" CACHED_REGEXES = {} CACHED_REGEXES_MAX_SIZE = 1000 - def re_compile(regex): try: return CACHED_REGEXES[regex] @@ -111,22 +60,6 @@ def re_compile(regex): return compiled_regex -class mybuiltin(object): - """ - NOTE could simple use a dict and populate it, - NOTE not sure if this changes things though if monkey patching import..... - """ - # __builtins__ - def __getitem__(self, key): - try: - return getattr(builtin, key) - except AttributeError: - raise KeyError(key) - - def __setitem__(self, key, value): - setattr(self, key, value) - - def LOAD(c=None, f='index', args=None, vars=None, extension=None, target=None, ajax=False, ajax_trap=False, url=None, user_signature=False, timeout=None, times=1, @@ -140,10 +73,10 @@ def LOAD(c=None, f='index', args=None, vars=None, vars(dict): vars extension(str): extension target(str): id of the target - ajax(bool): True to enable AJAX bahaviour + ajax(bool): True to enable AJAX behaviour ajax_trap(bool): True if `ajax` is set to `True`, traps both links and forms "inside" the target - url(str): overrides `c`,`f`,`args` and `vars` + url(str): overrides `c`, `f`, `args` and `vars` user_signature(bool): adds hmac signature to all links with a key that is different for every user timeout(int): in milliseconds, specifies the time to wait before @@ -153,7 +86,6 @@ def LOAD(c=None, f='index', args=None, vars=None, "infinity" or "continuous" are accepted to reload indefinitely the component """ - from gluon.html import TAG, DIV, URL, SCRIPT, XML if args is None: args = [] vars = Storage(vars or {}) @@ -163,19 +95,21 @@ def LOAD(c=None, f='index', args=None, vars=None, if '.' in f: f, extension = f.rsplit('.', 1) if url or ajax: - url = url or URL(request.application, c, f, r=request, - args=args, vars=vars, extension=extension, - user_signature=user_signature) + url = url or html.URL(request.application, c, f, r=request, + args=args, vars=vars, extension=extension, + user_signature=user_signature) # timing options if isinstance(times, basestring): if times.upper() in ("INFINITY", "CONTINUOUS"): times = "Infinity" else: + # FIXME: should be a ValueError raise TypeError("Unsupported times argument %s" % times) elif isinstance(times, int): if times <= 0: raise ValueError("Times argument must be greater than zero, 'Infinity' or None") else: + # NOTE: why do not use ValueError only? raise TypeError("Unsupported times argument type %s" % type(times)) if timeout is not None: if not isinstance(timeout, integer_types): @@ -191,7 +125,7 @@ def LOAD(c=None, f='index', args=None, vars=None, statement = "$.web2py.component('%s','%s');" % (url, target) attr['_data-w2p_remote'] = url if target is not None: - return DIV(content, **attr) + return html.DIV(content, **attr) else: if not isinstance(args, (list, tuple)): @@ -211,7 +145,7 @@ def LOAD(c=None, f='index', args=None, vars=None, '/'.join([request.application, c, f] + [str(a) for a in other_request.args]) other_request.env.query_string = \ - vars and URL(vars=vars).split('?')[1] or '' + vars and html.URL(vars=vars).split('?')[1] or '' other_request.env.http_web2py_component_location = \ request.env.path_info other_request.cid = target @@ -220,7 +154,7 @@ def LOAD(c=None, f='index', args=None, vars=None, # A bit nasty but needed to use LOAD on action decorates with @request.restful() other_response.view = '%s/%s.%s' % (c, f, other_request.extension) - other_environment = copy.copy(current.globalenv) # NASTY + other_environment = copy.copy(current.globalenv) # FIXME: NASTY other_response._view_environment = other_environment other_response.generic_patterns = \ @@ -241,12 +175,12 @@ def LOAD(c=None, f='index', args=None, vars=None, current.request, current.response = original_request, original_response js = None if ajax_trap: - link = URL(request.application, c, f, r=request, - args=args, vars=vars, extension=extension, - user_signature=user_signature) + link = html.URL(request.application, c, f, r=request, + args=args, vars=vars, extension=extension, + user_signature=user_signature) js = "$.web2py.trap_form('%s','%s');" % (link, target) - script = js and SCRIPT(js, _type="text/javascript") or '' - return TAG[''](DIV(XML(page), **attr), script) + script = js and html.SCRIPT(js, _type="text/javascript") or '' + return html.TAG[''](html.DIV(html.XML(page), **attr), script) class LoadFactory(object): @@ -262,7 +196,6 @@ class LoadFactory(object): if args is None: args = [] vars = Storage(vars or {}) - from . import globals target = target or 'c' + str(random.random())[2:] attr['_id'] = target request = current.request @@ -289,7 +222,7 @@ class LoadFactory(object): other_request.vars = vars other_request.get_vars = vars other_request.post_vars = Storage() - other_response = globals.Response() + other_response = Response() other_request.env.path_info = '/' + \ '/'.join([request.application, c, f] + [str(a) for a in other_request.args]) @@ -354,37 +287,36 @@ def local_import_aux(name, reload_force=False, app='welcome'): if reload_force: reload(module) return module +# +# OLD IMPLEMENTATION: +# +# items = name.replace('/','.').split('.') +# filename, modulepath = items[-1], pjoin(apath,'modules',*items[:-1]) +# imp.acquire_lock() +# try: +# file=None +# (file,path,desc) = imp.find_module(filename,[modulepath]+sys.path) +# if not path in sys.modules or reload: +# if is_gae: +# module={} +# execfile(path,{},module) +# module=Storage(module) +# else: +# module = imp.load_module(path,file,path,desc) +# sys.modules[path] = module +# else: +# module = sys.modules[path] +# except Exception, e: +# module = None +# if file: +# file.close() +# imp.release_lock() +# if not module: +# raise ImportError, "cannot find module %s in %s" % ( +# filename, modulepath) +# return module -""" -OLD IMPLEMENTATION: - items = name.replace('/','.').split('.') - filename, modulepath = items[-1], pjoin(apath,'modules',*items[:-1]) - imp.acquire_lock() - try: - file=None - (file,path,desc) = imp.find_module(filename,[modulepath]+sys.path) - if not path in sys.modules or reload: - if is_gae: - module={} - execfile(path,{},module) - module=Storage(module) - else: - module = imp.load_module(path,file,path,desc) - sys.modules[path] = module - else: - module = sys.modules[path] - except Exception, e: - module = None - if file: - file.close() - imp.release_lock() - if not module: - raise ImportError, "cannot find module %s in %s" % ( - filename, modulepath) - return module -""" - _base_environment_ = dict((k, getattr(html, k)) for k in html.__all__) _base_environment_.update( (k, getattr(validators, k)) for k in validators.__all__) @@ -424,7 +356,7 @@ def build_environment(request, response, session, store_current=True): r'^%s/%s/\w+\.py$' % (request.controller, request.function) ] - T = environment['T'] = TranslatorFactory(os.path.join(request.folder, 'languages'), + T = environment['T'] = TranslatorFactory(pjoin(request.folder, 'languages'), request.env.http_accept_language) c = environment['cache'] = Cache(request) @@ -439,7 +371,23 @@ def build_environment(request, response, session, store_current=True): current.T = T current.cache = c - if is_jython: # jython hack + if global_settings.is_jython: + # jython hack + class mybuiltin(object): + """ + NOTE could simple use a dict and populate it, + NOTE not sure if this changes things though if monkey patching import..... + """ + # __builtins__ + def __getitem__(self, key): + try: + return getattr(builtin, key) + except AttributeError: + raise KeyError(key) + + def __setitem__(self, key, value): + setattr(self, key, value) + global __builtins__ __builtins__ = mybuiltin() @@ -462,6 +410,11 @@ def save_pyc(filename): py_compile.compile(filename, cfile=cfile) +if PY2: + MARSHAL_HEADER_SIZE = 8 +else: + MARSHAL_HEADER_SIZE = 16 if sys.version_info[1] >= 7 else 12 + def read_pyc(filename): """ Read the code inside a bytecode compiled file if the MAGIC number is @@ -471,44 +424,49 @@ def read_pyc(filename): a code object """ data = read_file(filename, 'rb') - if not is_gae and data[:4] != imp.get_magic(): + if not global_settings.web2py_runtime_gae and \ + not data.startswith(imp.get_magic()): raise SystemError('compiled code is incompatible') - return marshal.loads(data[marshal_header_size:]) + return marshal.loads(data[MARSHAL_HEADER_SIZE:]) +REGEX_PATH_CLASS = r"[\w%s-]" % os.sep if os.sep != '\\' else r'[\w\\-]' +REGEX_VIEW_PATH = r"%s+(?:\.\w+)*$" % REGEX_PATH_CLASS + def compile_views(folder, skip_failed_views=False): """ Compiles all the views in the application specified by `folder` """ - path = pjoin(folder, 'views') failed_views = [] - for fname in listdir(path, '^[\w/\-]+(\.\w+)*$'): + for fname in listdir(path, REGEX_VIEW_PATH): try: data = parse_template(fname, path) except Exception as e: if skip_failed_views: failed_views.append(fname) else: + # FIXME: should raise something more specific than Exception raise Exception("%s in %s" % (e, fname)) else: - filename = 'views.%s.py' % fname.replace(os.path.sep, '.') + filename = 'views.%s.py' % fname.replace(os.sep, '.') filename = pjoin(folder, 'compiled', filename) write_file(filename, data) save_pyc(filename) os.unlink(filename) - return failed_views if failed_views else None + return failed_views or None +REGEX_MODEL_PATH = r"%s+\.py$" % REGEX_PATH_CLASS + def compile_models(folder): """ Compiles all the models in the application specified by `folder` """ - path = pjoin(folder, 'models') - for fname in listdir(path, '.+\.py$'): + for fname in listdir(path, REGEX_MODEL_PATH): data = read_file(pjoin(path, fname)) - modelfile = 'models.'+fname.replace(os.path.sep, '.') + modelfile = 'models.'+fname.replace(os.sep, '.') filename = pjoin(folder, 'compiled', modelfile) mktree(filename) write_file(filename, data) @@ -516,19 +474,22 @@ def compile_models(folder): os.unlink(filename) -def find_exposed_functions(data): - data = regex_longcomments.sub('', data) - return regex_expose.findall(data) +REGEX_LONG_STRING = re.compile('(""".*?"""|' "'''.*?''')", re.DOTALL) +REGEX_EXPOSED = re.compile(r'^def\s+(_?[a-zA-Z0-9]\w*)\( *\)\s*:', re.MULTILINE) +def find_exposed_functions(data): + data = REGEX_LONG_STRING.sub('', data) + return REGEX_EXPOSED.findall(data) + + +REGEX_CONTROLLER = r'[\w-]+\.py$' def compile_controllers(folder): """ Compiles all the controllers in the application specified by `folder` """ - path = pjoin(folder, 'controllers') - for fname in listdir(path, '.+\.py$'): - ### why is this here? save_pyc(pjoin(path, file)) + for fname in listdir(path, REGEX_CONTROLLER): data = read_file(pjoin(path, fname)) exposed = find_exposed_functions(data) for function in exposed: @@ -541,20 +502,22 @@ def compile_controllers(folder): os.unlink(filename) -def model_cmp(a, b, sep='.'): - return cmp(a.count(sep), b.count(sep)) or cmp(a, b) +if PY2: + def model_cmp(a, b, sep='.'): + return cmp(a.count(sep), b.count(sep)) or cmp(a, b) + + def model_cmp_sep(a, b, sep=os.sep): + return model_cmp(a, b, sep) -def model_cmp_sep(a, b, sep=os.path.sep): - return model_cmp(a, b, sep) - +REGEX_COMPILED_MODEL = r'models[_.][\w.-]+\.pyc$' +REGEX_MODEL = r'[\w-]+\.py$' def run_models_in(environment): """ Runs all models (in the app specified by the current folder) It tries pre-compiled models first before compiling them. """ - request = current.request folder = request.folder c = request.controller @@ -563,19 +526,21 @@ def run_models_in(environment): path = pjoin(folder, 'models') cpath = pjoin(folder, 'compiled') - compiled = os.path.exists(cpath) + compiled = exists(cpath) if PY2: if compiled: - models = sorted(listdir(cpath, '^models[_.][\w.]+\.pyc$', 0), model_cmp) + models = sorted(listdir(cpath, REGEX_COMPILED_MODEL, 0), + model_cmp) else: - models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), model_cmp_sep) + models = sorted(listdir(path, REGEX_MODEL, 0, sort=False), + model_cmp_sep) else: if compiled: - models = sorted(listdir(cpath, '^models[_.][\w.]+\.pyc$', 0), + models = sorted(listdir(cpath, REGEX_COMPILED_MODEL, 0), key=lambda f: '{0:03d}'.format(f.count('.')) + f) else: - models = sorted(listdir(path, '^\w+\.py$', 0, sort=False), - key=lambda f: '{0:03d}'.format(f.count(os.path.sep)) + f) + models = sorted(listdir(path, REGEX_MODEL, 0, sort=False), + key=lambda f: '{0:03d}'.format(f.count(os.sep)) + f) models_to_run = None for model in models: @@ -589,7 +554,7 @@ def run_models_in(environment): fname = model[n:-4].replace('.', '/')+'.py' else: n = len(path)+1 - fname = model[n:].replace(os.path.sep, '/') + fname = model[n:].replace(os.sep, '/') if not regex.search(fname) and c != 'appadmin': continue elif compiled: @@ -600,19 +565,56 @@ def run_models_in(environment): restricted(ccode, environment, layer=model) +TEST_CODE = r""" +def _TEST(): + import doctest, sys, cStringIO, types, cgi, gluon.fileutils + if not gluon.fileutils.check_credentials(request): + raise HTTP(401, web2py_error='invalid credentials') + stdout = sys.stdout + html = '

Testing controller "%s.py" ... done.


\n' \ + % request.controller + for key in sorted([key for key in globals() if not key in __symbols__+['_TEST']]): + eval_key = eval(key) + if type(eval_key) == types.FunctionType: + number_doctests = sum([len(ds.examples) for ds in doctest.DocTestFinder().find(eval_key)]) + if number_doctests>0: + sys.stdout = cStringIO.StringIO() + name = '%s/controllers/%s.py in %s.__doc__' \ + % (request.folder, request.controller, key) + doctest.run_docstring_examples(eval_key, + globals(), False, name=name) + report = sys.stdout.getvalue().strip() + if report: + pf = 'failed' + else: + pf = 'passed' + html += '

Function %s [%s]

\n' \ + % (pf, key, pf) + if report: + html += CODE(report, language='web2py', \ + link='/examples/global/vars/').xml() + html += '
\n' + else: + html += \ + '

Function %s [no doctests]


\n' \ + % (key) + response._vars = html + sys.stdout = stdout +_TEST() +""" + def run_controller_in(controller, function, environment): """ Runs the controller.function() (for the app specified by the current folder). It tries pre-compiled controller.function.pyc first before compiling it. """ - # if compiled should run compiled! folder = current.request.folder cpath = pjoin(folder, 'compiled') badc = 'invalid controller (%s/%s)' % (controller, function) badf = 'invalid function (%s/%s)' % (controller, function) - if os.path.exists(cpath): + if exists(cpath): filename = pjoin(cpath, 'controllers.%s.%s.pyc' % (controller, function)) try: ccode = getcfs(filename, filename, lambda: read_pyc(filename)) @@ -622,8 +624,6 @@ def run_controller_in(controller, function, environment): web2py_error=badf) elif function == '_TEST': # TESTING: adjust the path to include site packages - from gluon.settings import global_settings - from gluon.admin import abspath, add_path_first paths = (global_settings.gluon_parent, abspath( 'site-packages', gluon=True), abspath('gluon', gluon=True), '') [add_path_first(path) for path in paths] @@ -631,7 +631,7 @@ def run_controller_in(controller, function, environment): filename = pjoin(folder, 'controllers/%s.py' % controller) - if not os.path.exists(filename): + if not exists(filename): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badc, web2py_error=badc) @@ -696,12 +696,12 @@ def run_view_in(environment): layer = 'file stream' else: filename = pjoin(folder, 'views', view) - if os.path.exists(cpath): # compiled views + if exists(cpath): # compiled views x = view.replace('/', '.') files = ['views.%s.pyc' % x] - is_compiled = os.path.exists(pjoin(cpath, files[0])) + is_compiled = exists(pjoin(cpath, files[0])) # Don't use a generic view if the non-compiled view exists. - if is_compiled or (not is_compiled and not os.path.exists(filename)): + if is_compiled or (not is_compiled and not exists(filename)): if allow_generic: files.append('views.generic.%s.pyc' % request.extension) # for backward compatibility @@ -712,16 +712,16 @@ def run_view_in(environment): # end backward compatibility code for f in files: compiled = pjoin(cpath, f) - if os.path.exists(compiled): + if exists(compiled): ccode = getcfs(compiled, compiled, lambda: read_pyc(compiled)) layer = compiled break # if the view is not compiled if not layer: - if not os.path.exists(filename) and allow_generic: + if not exists(filename) and allow_generic: view = 'generic.' + request.extension filename = pjoin(folder, 'views', view) - if not os.path.exists(filename): + if not exists(filename): raise HTTP(404, rewrite.THREAD_LOCAL.routes.error_message % badv, web2py_error=badv) @@ -737,6 +737,8 @@ def run_view_in(environment): return environment['response'].body.getvalue() +REGEX_COMPILED_CONTROLLER = r'[\w-]+\.pyc$' + def remove_compiled_application(folder): """ Deletes the folder `compiled` containing the compiled application. @@ -744,7 +746,7 @@ def remove_compiled_application(folder): try: shutil.rmtree(pjoin(folder, 'compiled')) path = pjoin(folder, 'controllers') - for file in listdir(path, '.*\.pyc$', drop=False): + for file in listdir(path, REGEX_COMPILED_CONTROLLER, drop=False): os.unlink(file) except OSError: pass diff --git a/gluon/console.py b/gluon/console.py index e51693c8..57604362 100644 --- a/gluon/console.py +++ b/gluon/console.py @@ -531,7 +531,7 @@ def load_config(config_file, opt_map): """ REGEX_ASSIGN_EXP = re.compile(r'\s*=\s*(.+)') map_items = opt_map.items() - # preserve the order of loaded options even if this is not needed + # preserve the order of loaded options even though this is not needed pl = OrderedDict() config_encoding = get_pep263_encoding(config_file) # NOTE: assume 'ascii' encoding when not explicitly stated (Python 2), diff --git a/gluon/packages/dal b/gluon/packages/dal index 2dca2fd7..cd1b2ed6 160000 --- a/gluon/packages/dal +++ b/gluon/packages/dal @@ -1 +1 @@ -Subproject commit 2dca2fd72bfdac22c894d1bb1e0abdadd43fe41b +Subproject commit cd1b2ed6d9b03cd837660f64b2528aff6413f8f8 diff --git a/gluon/packages/yatl b/gluon/packages/yatl index 97ce196d..dd969365 160000 --- a/gluon/packages/yatl +++ b/gluon/packages/yatl @@ -1 +1 @@ -Subproject commit 97ce196d2c248163254be15e4b22b1b855cc17e2 +Subproject commit dd969365265bf329c73784d989bdaa6de23d9633