Check-in [0d6acc5aef]
Overview
Comment: | Split pluginconf from config module. |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA1: |
0d6acc5aef1fd791cee7f0e7edadfe2e |
User & Date: | mario on 2015-05-04 15:21:47 |
Other Links: | manifest | tags |
Context
2015-05-04
| ||
15:22 | Make configwin construction slightly more readable, use new pluginconf functions. check-in: 5f8afee363 user: mario tags: trunk | |
15:21 | Split pluginconf from config module. check-in: 0d6acc5aef user: mario tags: trunk | |
15:21 | Use file:// prefix for storing local paths. (Did break DND without "srv" export.) check-in: 3c7e820654 user: mario tags: trunk | |
Changes
Modified config.py from [8817626a3c] to [28c3c558fd].
|
| < > > | < | | > | > | > > | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | # encoding: UTF-8 # api: streamtuner2 # type: class # title: global config object # description: reads ~/.config/streamtuner/*.json files # config: # { arg: -d, type: str, name: disable[], description: Omit plugin from initialization. } # { arg: -e, type: str, name: enable[], description: Add channel plugin. } # { arg: --gtk3, type: boolean, name: gtk3, description: Start with Gtk3 interface. } # { arg: -D, type: boolean, name: debug, description: Enable debug messages on console } # { arg: action, type: str *, name: action[], description: CLI interface commands. } # { arg: -x, type: boolean, name: exit, hidden: 1 } # { arg: --nt, type: boolean, name: nothreads, description: Disable threading/gtk_idle UI. } # version: 2.7 # priority: core # depends: pluginconf >= 0.1, os, json, re, zlib, pkgutil # # Ties together the global conf.* object. It's typically used # in the main application and modules with: # # from config import * # # The underlying ConfigDict class is already instantiated and # imported as `conf` then. # # With .save() or .load() it handles storage as JSON. Both # utility functions are also used for other cache files. # More specific config stores are available per .netrc(), # and .init_args(). # # Whereas plugin utility code is available per plugin_meta(), # module_list(), and get_data(). There's a prepared function # for add_plugin_config() on initialization. # # Also provides a simple logging interface with log.TYPE(...), # which is also pre-instantiated. from __future__ import print_function import os import sys import json import gzip import platform import re from compat2and3 import gzip_decode, find_executable import zlib import zipfile import inspect import pkgutil import argparse from pluginconf import plugin_meta, module_list, get_data import pluginconf # export symbols __all__ = ["conf", "log", "plugin_meta", "module_list", "get_data", "find_executable"] #-- create a stub instance of config object conf = object() |
︙ | ︙ | |||
133 134 135 136 137 138 139 | self.playlist_asis = 0 self.google_homepage = 0 self.windows = platform.system()=="Windows" self.pyquery = 1 self.debug = 0 | | | | < < < < < < < < < | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | self.playlist_asis = 0 self.google_homepage = 0 self.windows = platform.system()=="Windows" self.pyquery = 1 self.debug = 0 # Add plugin names and default config: options from each .meta def add_plugin_defaults(self, meta, name): pluginconf.add_plugin_defaults(self, self.plugins, meta, name) # look at system binaries for standard audio players def find_player(self, typ="audio", default="xdg-open"): players = { "audio": ["audacious %m3u", "audacious2", "exaile %pls", "xmms2", "banshee", "amarok %pls", "clementine", "qmmp", "quodlibet", "aqualung", "mp3blaster %m3u", "vlc --one-instance", "totem"], "video": ["umplayer", "xnoise", "gxine", "totem", "vlc --one-instance", "parole", "smplayer", "gnome-media-player", "xine", "bangarang"], |
︙ | ︙ | |||
289 290 291 292 293 294 295 | if server in netrc: return netrc[server] # Use config:-style definitions for argv extraction, # such as: { arg: -D, name: debug, type: bool } def init_args(self, ap): | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | if server in netrc: return netrc[server] # Use config:-style definitions for argv extraction, # such as: { arg: -D, name: debug, type: bool } def init_args(self, ap): for opt in plugin_meta(frame=1).get("config"): kwargs = pluginconf.argparse_map(opt) if kwargs: #print kwargs ap.add_argument(*kwargs.pop("args"), **kwargs) return ap.parse_args() # Copy args fields into conf. dict def apply_args(self, args): self.debug = args.debug self.nothreads = args.nothreads if args.exit: sys.exit(1) for p_id in (args.disable or []): self.plugins[p_id] = 0 for p_id in (args.enable or []): self.plugins[p_id] = 1 # Simplified print wrapper: `log.err(...)` class log_printer(object): # Wrapper method = None |
︙ | ︙ | |||
534 535 536 537 538 539 540 541 | "STAT": "37m", # gray CONFIG STATE } # instantiate right away log = log_printer() | > > > > > > > > | < < < | 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | "STAT": "37m", # gray CONFIG STATE } # instantiate right away log = log_printer() # populate global conf instance conf = ConfigDict() log.PROC("ConfigDict() initialized") # tie in pluginconf.* pluginconf.log_WARN = log.WARN pluginconf.log_ERR = log.ERR pluginconf.module_base = "config" pluginconf.plugin_base = ["channels", conf.share + "/channels", conf.dir + "/plugins"] |
Added pluginconf.py version [8af6dbea92].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | # encoding: UTF-8 # api: python # type: handler # category: io # title: Plugin configuration # description: Read meta data, pyz/package contents, module locating # version: 0.5 # priority: core # config: - # # Provides plugin lookup and meta data extraction utility functions. # It's used to abstract module+option management in applications, # which consolidates internal and external tool reuse. # # The key:value format is language-agnostic, but in Python just uses # hash comments. It's basically YAML in the top-most script comment, # common field names, documentation, and a simple config: { .. } spec # for options and defaults. # # It neither imposes a specific module/plugin API, nor config storage, # and doesn't provide much for module loading. It's really just meant # to look up meta infos. # This approach avoids in-code values/inspection, externalized meta # descriptors, and any hodgepodge or premature module loading just to # uncover module description fields. # # plugin_meta() # ‾‾‾‾‾‾‾‾‾‾‾‾‾ # Is the primary function to extract a meta dictionary from files. # It either reads from a given module= name, a literal fn=, or just # src= code, and as fallback inspects the last stack frame= else. # # module_list() # ‾‾‾‾‾‾‾‾‾‾‾‾‾ # Returns basenames of available/installed plugins. It uses the # plugin_base=[] list for module relation. Which needs to be set up # beforehand, or injected. # # add_plugin_defaults() # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Populates a configval{} and pluginstate{} list. Used for initial # setup, when adding new plugins, etc. Both dicts might also be # ConfigParser stores, or implement magic __set__ handling to act # on changed states. # # get_data() # ‾‾‾‾‾‾‾‾‾‾ # Is mostly an alias for pkgutil.get_data(). It abstracts the main # base path, allows PYZ usage, and adds some convenience flags.‾ # It's somewhat off-scope for plugin management, but used internally. # # argparse_map() # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Converts a list of config: options with arg: attribute for use as # argparser parameters. # # dependency().valid/depends() # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Probes a new plugins` depends: list against installed base modules. # Very crude and tied to streamtuner2 base names. # # # from __future__ import print_function import sys import os import re import pkgutil import inspect try: from compat2and3 import gzip_decode except: from gzip import decompress as gzip_decode # Py3 only import zipfile import argparse __all__ = ["get_data", "module_list", "plugin_meta", "dependency", "add_plugin_defaults"] # Injectables # ‾‾‾‾‾‾‾‾‾‾‾ log_WARN = lambda *x:None log_ERR = lambda *x:None # File lookup relation for get_data(), should be a top-level package module module_base = "pluginconf" # Module lookups via module_list(), may contain names or paths plugin_base = ["plugins"] # [conf.share+"/channels", conf.dir+"/plugins"]) # Resource retrieval # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Retrieve file content from install path or from within # pyzip archive. This is just an alias and convenience # wrapper for pkgutil.get_data(). # Uses module_base / file_base as top-level reference. # def get_data(fn, decode=False, gz=False, file_base=None): try: bin = pkgutil.get_data(file_base or module_base, fn) if gz: bin = gzip_decode(bin) if decode: return bin.decode("utf-8", errors='ignore') else: return str(bin) except: log_WARN("get_data() didn't find:", fn) # Plugin name lookup # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Search through ./plugins/ (and other configured plugin_base # names or paths) and get module basenames. # def module_list(extra_base=[]): # Should list plugins within zips as well as local paths ls = pkgutil.iter_modules(plugin_base + extra_base) return [name for loader,name,ispkg in ls] # Plugin => meta dict # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # This is a trivial wrapper to assemble a complete dictionary # of available/installed plugins. It associates each plugin name # with a its meta{} fields. # def all_plugin_meta(): return { name: plugin_meta(module=name) for name in module_list() } # Plugin meta data extraction # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Can fetch infos from different sources: # # fn= read literal files, or .pyz contents # # src= from pre-available script code # # module= lookup per pkgutil, from plugin bases # or top-level modules # # frame= extract comment header of caller # (default) # def plugin_meta(fn=None, src=None, module=None, frame=1, extra_base=[module_base]): # Try via pkgutil first, # find any plugins.* modules, or main packages if module: fn = module for base in plugin_base + extra_base: try: src = get_data(fn+".py", decode=True, file_base=base) if src: break except: continue # plugin_meta_extract() will print a notice later # Real filename/path elif fn and os.path.exists(fn): src = open(fn).read(4096) # Else get source directly from caller elif not src and not fn: module = inspect.getmodule(sys._getframe(frame)) fn = inspect.getsourcefile(module) src = inspect.getcomments(module) # Assume it's a filename within a zip elif fn: intfn = "" while fn and len(fn) and not os.path.exists(fn): fn, add = os.path.split(fn) intfn = add + "/" + intfn if len(fn) >= 3 and intfn and zipfile.is_zipfile(fn): src = zipfile.ZipFile(fn, "r").read(intfn.strip("/")) # Extract source comment into meta dict if not src: src = "" if not isinstance(src, str): src = src.decode("utf-8", errors='replace') return plugin_meta_extract(src, fn) # Comment and field extraction logic # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Finds the first comment block. Splits key:value header # fields from comment. Turns everything into an dict, with # some stub fields if absent. # def plugin_meta_extract(src="", fn=None, literal=False): # Defaults meta = { "id": os.path.splitext(os.path.basename(fn or "")), "fn": fn, "api": "python", "type": "module", "category": None, "priority": None, "title": fn, "description": "no description", "config": [], "doc": "" } # Extract coherent comment block if not literal: src = rx.comment.search(src) if not src: log_ERR("Couldn't read source meta information:", fn) return meta src = src.group(0) src = rx.hash.sub("", src).strip() # Split comment block if src.find("\n\n") > 0: src, meta["doc"] = src.split("\n\n", 1) # Turn key:value lines into dictionary for field in rx.keyval.findall(src): meta[field[0]] = field[1].strip() meta["config"] = plugin_meta_config(meta.get("config") or "") return meta # Unpack config: structures # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Further breaks up the meta['config'] descriptor. # Creates an array from JSON/YAML option lists. # # config: # { name: 'var1', type: text, value: "default, ..." } # { name=option2, type=boolean, $value=1, etc. } # # Stubs out name, value, type, description if absent. # def plugin_meta_config(str): config = [] for entry in rx.config.findall(str): opt = { "type": None, "name": None, "description": "", "value": None } for field in rx.options.findall(entry): opt[field[0]] = (field[1] or field[2] or field[3] or "").strip() config.append(opt) return config # Comment extraction regexps # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Pretty crude comment splitting approach. But works # well enough already. Technically a YAML parser would # do better; but is likely overkill. # class rx: comment = re.compile(r"""(^ {0,4}#.*\n)+""", re.M) hash = re.compile(r"""(^ {0,4}# *)""", re.M) keyval = re.compile(r""" ^([\w-]+):(.*$(?:\n(?![\w-]+:).+$)*) # plain key:value lines """, re.M|re.X) config = re.compile(r""" [\{\<] (.+?) [\}\>] # JSOL/YAML scheme {...} dicts """, re.X) options = re.compile(r""" ["':$]? (\w*) ["']? # key or ":key" or '$key' \s* [:=] \s* # "=" or ":" (?: " ([^"]*) " | ' ([^']*) ' # "quoted" or 'singl' values | ([^,]*) # or unquoted literals ) """, re.X) # ArgumentParser options conversion # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # As variation of in-application config: options, this method converts # cmdline argument specifiers. # # config: # { arg: -i, name: input[], type: str, description: input files } # # Which allows to collect argumentparser options from different plugins. # The only difference to normal config entries is the `arg:` attribute. # # · It adds array arguments with a [] name suffix, or a `*` type suffix. # Else even a `?` or `+` and numeric counts after the type flag. # # · Understands the types `str`, `int` and `bool`. # # · Entries may carry a `hidden: 1` or `required: 1` attribute. # # · And `help:` is an alias to `description:` # And `default:` an alias for `value:` # # · While `type: select` utilizes the `select: a|b|c` format as uaual. # # ArgParsers const=, metavar= flag, or type=file are not aliased here. # # Basically returns a dictionary that can be fed per **kwargs directly # to an ArgumentParsers add_argument(). Iterate over each plugins # meta['config'][] options to convert them. # def argparse_map(opt): if not ("arg" in opt and opt["name"] and opt["type"]): return {} # Extract --flag names args = opt["arg"].split() + re.findall("-+\w+", opt["name"]) # Prepare mapping options typing = re.findall("bool|str|\[\]|const|false|true", opt["type"]) naming = re.findall("\[\]", opt["name"]) name = re.findall("(?<!-)\\b\\w+", opt["name"]) nargs = re.findall("\\b\d+\\b|[\?\*\+]", opt["type"]) or [None] is_arr = "[]" in (naming + typing) and nargs == [None] is_bool= "bool" in typing false_b = "false" in typing or opt["value"] in ("0", "false") #print "\nname=", name, "is_arr=", is_arr, "is_bool=", is_bool, "bool_d=", false_b, "naming=", naming, "typing=", typing # Populate partially - ArgumentParser has aversions to many parameter combinations kwargs = dict( args = args, dest = name[0] if not name[0] in args else None, action = is_arr and "append" or is_bool and false_b and "store_false" or is_bool and "store_true" or "store", nargs = nargs[0], default = opt.get("default") or opt["value"], type = None if is_bool else ("int" in typing and int or "bool" in typing and bool or str), choices = opt["select"].split("|") if "select" in opt else None, required = "required" in opt or None, help = opt["description"] if not "hidden" in opt else argparse.SUPPRESS ) return {k:w for k,w in kwargs.items() if w is not None} # Minimal depends: probing # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Now this definitely requires customization. Each plugin can carry # a list of (soft-) dependency names. # # depends: config, appcore >= 2.0, bin:wkhtmltoimage, python < 3.5 # # Here only in-application modules are honored, system references # ignored. Unknown plugin names are also skipped. A real install # helper might want to auto-tick them on, etc. This example is just # meant for probing downloadable plugins. # # The .valid() helper just asserts the api: string, or skips existing # modules. # While .depends() compares minimum versions of existing base plugins. # # In practice there's little need for full-blown dependency resolving # for in-app modules. # class dependency(object): # prepare list of known plugins and versions def __init__(self): self.have = all_plugin_meta() # dependencies on core modules are somewhat more interesting: self.have.update({ "streamtuner2": plugin_meta(module="st2", extra_base=["config"]), "uikit": plugin_meta(module="uikit", extra_base=["config"]), "config": plugin_meta(module="config", extra_base=["config"]), "action": plugin_meta(module="action", extra_base=["config"]), }) have = {} # depends: def depends(self, plugin): if plugin.get("depends"): d = self.deps(plugin["depends"]) if not self.cmp(d, self.have): return False return True # basic list pre-filtering (skip __init__, filter by api:, exclude installed & same-version plugins) def valid(self, newpl): id = newpl.get("$name") have_ver = self.have.get(id, {}).get("version", "0") return id.find("__") < 0 \ and newpl.get("api") == "streamtuner2" \ and have_ver < newpl.get("version", "0.0") # Split trivial "pkg, mod >= 1, uikit < 4.0" list def deps(self, dep_str): d = [] for dep in re.split(r"\s*[,;]+\s*", dep_str): # skip deb:pkg-name, rpm:name, bin:name etc. if not len(dep) or dep.find(":") >= 0: continue # find comparison and version num m = re.search(r"([\w.-]+)\s*([>=<!~]+)\s*([\d.]+([-~.]\w+)*)", dep + " >= 0") if m and m.group(2): d.append([m.group(i) for i in (1,2,3)]) return d # Do actual comparison def cmp(self, d, have): r = True for name, op, ver in d: # skip unknown plugins, might be python module references ("depends: re, json") if not have.get(name, {}).get("version"): continue curr = have[name]["version"] tbl = { ">=": curr >= ver, "<=": curr <= ver, "==": curr == ver, ">": curr > ver, "<": curr < ver, "!=": curr != ver, } r &= tbl.get(op, True) return r # Add plugin defaults to conf.* store # ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ # Utility function which applies plugin meta data to a config # store. Which in the case of streamtuner2 is really just a # dictionary `conf{}` and a plugin list in `conf.plugins{}`. # # Adds each default option value: to conf_options{}. And sets # first plugin state (enabled/disabled) in conf_plugins{} list, # depending on priority: classifier. # def add_plugin_defaults(conf_options, conf_plugins, meta={}, module=""): # Option defaults, if not yet defined for opt in meta.get("config", []): if "name" in opt and "value" in opt: if opt["name"] not in conf_options: conf_options[opt["name"]] = opt["value"] # should typemap "bool" and "int" here # Initial plugin activation status if module and module not in conf_plugins: conf_plugins[module] = meta.get("priority") in ("core", "builtin", "always", "default", "standard") |