File indexing completed on 2024-04-14 15:48:35

0001 # -*- coding: UTF-8 -*-
0002 
0003 """
0004 Fetch Pology modules, functions, data, etc. by various handles.
0005 
0006 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net>
0007 @license: GPLv3
0008 """
0009 
0010 import sys
0011 import os
0012 import re
0013 
0014 from pology import PologyError, _, n_
0015 from pology.report import error, warning
0016 
0017 
0018 def get_module (modpath, lang=None, proj=None, abort=False, wpath=False):
0019     """
0020     Import a Pology module.
0021 
0022     Module is specified by its dotted path in Pology's package structure
0023     relative to (optional) language and project.
0024     For example::
0025 
0026         get_module("remove")
0027 
0028     will try to import the L{pology.remove}, while::
0029 
0030         get_module("wconv", lang="sr")
0031 
0032     will try to import the C{pology.lang.sr.wconv} module, and::
0033 
0034         get_module("header", proj="kde")
0035 
0036     will try to import the C{pology.proj.kde.header} module.
0037 
0038     Elements of the relative path can also contain hyphens, which will
0039     be converted into underscores when looking for the module.
0040 
0041     If the module cannot be imported, if C{abort} is C{True} the execution
0042     will abort with an error message; otherwise an exception is raised.
0043 
0044     If C{wpath} is C{True}, the resolved module path is returned as well.
0045 
0046     @param modpath: relative module location
0047     @type modpath: string
0048     @param lang: language code
0049     @type lang: string
0050     @param proj: project code
0051     @type proj: string
0052     @param abort: whether to abort execution if the module cannot be imported
0053     @type abort: bool
0054     @param wpath: whether to also return resolve module path
0055     @type wpath: bool
0056 
0057     @returns: imported module, possibly with its path
0058     @rtype: module or (module, string)
0059     """
0060 
0061     modpath = modpath.replace("-", "_")
0062     if lang and proj:
0063         modpath = "pology.lang.%s.proj.%s" % (lang, proj, modpath)
0064     elif lang:
0065         modpath = "pology.lang.%s.%s" % (lang, modpath)
0066     elif proj:
0067         modpath = "pology.proj.%s.%s" % (proj, modpath)
0068     else:
0069         modpath = "pology.%s" % (modpath)
0070     try:
0071         module = __import__(modpath, globals(), locals(), [""])
0072     except ImportError:
0073         _raise_or_abort(_("@info",
0074                           "Cannot import module '%(mod)s'.",
0075                           mod=modpath), abort)
0076 
0077     # TODO: Make more detailed analysis why importing fails:
0078     # is there such a language or project, is there such a file, etc.
0079 
0080     return module if not wpath else (module, modpath)
0081 
0082 
0083 _valid_lang_rx = re.compile(r"^[a-z]{2,3}(_[A-Z]{2})?(@\w+)?$")
0084 _valid_proj_rx = re.compile(r"^[a-z_]+$")
0085 _valid_path_rx = re.compile(r"^([a-z][\w-]*(\.|$))+", re.I)
0086 _valid_item_rx = re.compile(r"^[a-z][\w-]+$", re.I)
0087 
0088 def split_ireq (ireq, abort=False):
0089     """
0090     Split item request string into distinct elements.
0091 
0092     The item request is a string of the form
0093     C{[lang:][proj%]path[/item][~args]} (or C{[proj%][lang:]...}),
0094     which this function parses into C{(path, lang, proj, item, args)} tuple.
0095     If language, project, item or argument strings are not not stated,
0096     their value in the tuple will be C{None}.
0097     The language should be a proper language code,
0098     the project an identifier-like string,
0099     the path a sequence of identifier-like strings connected by dots
0100     (though hyphens are accepted an taken as synonymous to underscores),
0101     item an identifier-like string,
0102     and arguments can be an arbitrary string.
0103 
0104     If the item request cannot be parsed,
0105     either the execution is aborted with an error message,
0106     or an exception is raised, depending on value of C{abort}.
0107 
0108     @param ireq: item request
0109     @type ireq: string
0110     @param abort: whether to abort execution or if the request cannot be parsed
0111     @type abort: bool
0112 
0113     @returns: parsed request elements
0114     @rtype: (string, string or C{None}, string or C{None}, string or C{None},
0115              string or C{None})
0116     """
0117 
0118     rest = ireq
0119 
0120     lst = rest.split("~", 1)
0121     if len(lst) == 1:
0122         rest, args = lst + [None]
0123     else:
0124         rest, args = lst
0125 
0126     lst = rest.split("/", 1)
0127     if len(lst) == 1:
0128         rest, item = lst + [None]
0129     else:
0130         rest, item = lst
0131 
0132     lang = None
0133     proj = None
0134     plang = rest.find(":")
0135     pproj = rest.find("%")
0136     if plang >= 0 and pproj >= 0:
0137         p1, p2 = min(plang, pproj), max(plang, pproj)
0138         c1, c2, rest = rest[:p1], rest[p1 + 1:p2], rest[p2 + 1:]
0139         if plang < pproj:
0140             lang, proj = c1, c2
0141         else:
0142             lang, proj = c2, c1
0143     elif plang >= 0:
0144         lang, rest = rest[:plang], rest[plang + 1:]
0145     elif pproj >= 0:
0146         proj, rest = rest[:pproj], rest[pproj + 1:]
0147 
0148     path = rest
0149 
0150     if not _valid_path_rx.search(path):
0151         _raise_or_abort(_("@info",
0152                           "Invalid path '%(path)s' in item request '%(req)s'.",
0153                           path=path, req=ireq), abort)
0154     if lang is not None and not _valid_lang_rx.search(lang):
0155         _raise_or_abort(_("@info",
0156                           "Invalid language code '%(code)s' "
0157                           "in item request '%(req)s.'",
0158                           code=lang, req=ireq), abort)
0159     if proj is not None and not _valid_proj_rx.search(proj):
0160         _raise_or_abort(_("@info",
0161                           "Invalid project code '%(code)s' "
0162                           "in item request '%(req)s.'",
0163                           code=proj, req=ireq), abort)
0164     if item is not None and not _valid_item_rx.search(item):
0165         _raise_or_abort(_("@info",
0166                           "Invalid item '%(item)s' in item request '%(req)s'.",
0167                           item=item, req=ireq), abort)
0168 
0169     path = path.replace("-", "_")
0170     if item:
0171         item = item.replace("-", "_")
0172 
0173     return path, lang, proj, item, args
0174 
0175 
0176 def get_hook (modpath, lang=None, proj=None, func=None, args=None, abort=False):
0177     """
0178     Fetch a hook function.
0179 
0180     Loads a hook function from a module obtained by applying L{get_module}
0181     to C{modpath}, C{lang}, and C{proj} parameters.
0182     If C{func} is C{None}, the function name defaults to module name;
0183     if C{func} is not C{None}, but function of that name is not found,
0184     then the function named C{<modname>_<func>} is additionally tried
0185     (where C{<modname>} is the last element in C{modpath}).
0186     If C{args} is not C{None}, then the loaded function is considered
0187     a hook factory, and the hook is created by calling it with C{args} string
0188     as argument list (it should have no surrounding parenthesis).
0189 
0190     @param modpath: hook module
0191     @type modpath: string
0192     @param lang: language code
0193     @type lang: string
0194     @param proj: project code
0195     @type proj: string
0196     @param func: function name of hook or hook factory
0197     @type func: string
0198     @param args: argument string to hook factory
0199     @type args: string
0200     @param abort: whether to abort execution or raise exception if the hook
0201         cannot be loaded
0202     @type abort: bool
0203 
0204     @returns: the hook
0205     """
0206 
0207     lmod, modpath = get_module(modpath, lang, proj, abort, wpath=True)
0208     modname = modpath.rsplit(".", 1)[-1]
0209     if func is None:
0210         func = modname
0211         func2 = "\0"
0212     else:
0213         func2 = "%s_%s" % (modname, func)
0214     call = getattr(lmod, func, None) or getattr(lmod, func2, None)
0215     if call is None:
0216         _raise_or_abort(_("@info",
0217                           "Module '%(mod)s' does not define "
0218                           "'%(func)s' function.",
0219                           mod=modpath, func=func), abort)
0220     if args is not None:
0221         try:
0222             call = eval("call(%s)" % args)
0223         except Exception as e:
0224             fspec = "%s/%s" % (modpath, func)
0225             _raise_or_abort(_("@info",
0226                               "Cannot create hook by applying function "
0227                               "'%(func)s' to argument list %(args)s; "
0228                               "reported error:\n%(msg)s",
0229                               func=fspec, args=repr(args), msg=e),
0230                             abort)
0231 
0232     return call
0233 
0234 
0235 def get_hook_ireq (ireq, abort=False):
0236     """
0237     Like L{get_hook}, but the hook is specified by
0238     L{item request<split_ireq>}.
0239 
0240     For a module C{pology.FOO} which defines the C{FOO()} hook function,
0241     the hook specification is simply C{FOO}.
0242     If the hook function is named C{BAR()} instead of C{FOO()},
0243     the hook specification is given as C{FOO/BAR};
0244     if the hook function is named C{FOO_BAR()}, i.e. the specification
0245     would be C{FOO/FOO_BAR}, it can be folded to C{FOO/BAR}.
0246     Language-specific hooks (C{pology.lang.LANG.FOO}) are aditionally
0247     preceded by the language code and colon, as C{LANG:FOO} or C{LANG:FOO/BAR}.
0248     Project-specific hooks (C{pology.proj.PROJ.FOO}) are aditionally
0249     preceded by the project code and percent, as C{PROJ%FOO} or C{LANG%FOO/BAR}.
0250     If the hook is both language- and project- specific, language and project
0251     qualifiers can both be added: C{LANG:PROJ%FOO} or C{LANG:PROJ%FOO/BAR};
0252     ordering, C{LANG:PROJ%...} or C{PROJ%LANG:...}, is not significant.
0253 
0254     If the hook is not a plain hook, but a hook factory function,
0255     the factory arguments are supplied after the basic hook specification,
0256     separated by tilde: C{LANG:PROJ%FOO/BAR~ARGLIST}
0257     (where C{LANG:}, C{PROJ%} and C{/BAR} may be omitted under previously
0258     listed conditions).
0259     Argument list is formatted just like it would be passed in Python code
0260     to the factory function, omitting the surrounding parenthesis.
0261     """
0262 
0263     return _by_ireq(ireq, get_hook, abort=abort)
0264 
0265 
0266 def _by_ireq (ireq, getter, abort=False):
0267     """
0268     Get item using C{getter(path, lang, proj, item, abort)}
0269     method, by applying it to parsed item request string.
0270     """
0271 
0272     path, lang, proj, item, args = split_ireq(ireq, abort)
0273     return getter(path, lang, proj, item, args, abort)
0274 
0275 
0276 def _raise_or_abort (errmsg, abort, exc=PologyError):
0277     """
0278     Raise an exception or abort execution with given error message,
0279     based on the value of C{abort}.
0280     """
0281 
0282     if abort:
0283         error(errmsg)
0284     else:
0285         raise exc(errmsg)
0286 
0287 
0288 def get_result (modpath, lang=None, proj=None, func=None, args="", abort=False):
0289     """
0290     Fetch the result of a function evaluation.
0291 
0292     Executes function from the module loaded by applying L{get_module}
0293     to C{modpath}, C{lang}, and C{proj} parameters.
0294     If C{func} is not given, the function name defaults to module name.
0295     C{args} is the string representing the argument list
0296     to the function call (without surrounding parenthesis).
0297 
0298     @param modpath: function module
0299     @type modpath: string
0300     @param lang: language code
0301     @type lang: string
0302     @param proj: project code
0303     @type proj: string
0304     @param func: function name within the module
0305     @type func: string
0306     @param args: argument string to function call
0307     @type args: string
0308     @param abort: if the function is not found, abort or report C{None}
0309     @type abort: bool
0310 
0311     @returns: the value returned by the function call
0312     """
0313 
0314     fmod, modpath = get_module(modpath, lang, proj, abort, wpath=True)
0315     modname = modpath.rsplit(".", 1)[-1]
0316     if func is None:
0317         func = modname
0318     call = getattr(fmod, func, None)
0319     if call is None:
0320         _raise_or_abort(_("@info",
0321                           "Module '%(mod)s' does not define "
0322                           "function '%(func)s'.",
0323                           mod=modpath, func=func), abort)
0324     try:
0325         res = eval("call(%s)" % args)
0326     except Exception as e:
0327         fspec = "%s/%s" % (modpath, func)
0328         _raise_or_abort(_("@info",
0329                           "Evaluating function '%(func)s' "
0330                           "with argument list %(args)s failed; "
0331                           "reported error:\n%(msg)s",
0332                           func=fspec, args=repr(args), msg=e),
0333                           abort)
0334 
0335     return res
0336 
0337 
0338 def get_result_ireq (ireq, abort=False):
0339     """
0340     Like L{get_result}, but the function is specified by
0341     L{item request<split_ireq>}.
0342     """
0343 
0344     return _by_ireq(ireq, get_result, abort=abort)
0345