Internet radio browser GUI for music/video streams from various directory services.

⌈⌋ branch:  streamtuner2


Check-in [70c2e437fd]

Overview
Comment:More complete plugin_meta() regex patterns. Now defaults to inspect.get_comments() on previous stack frame, src= or fn= only accepted as fallback. Complete default meta fields, splits doc block out, autostrips hash prefixes and indentation.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 70c2e437fd9755050b79f9bcc395e40391f4a525
User & Date: mario on 2015-03-29 03:06:37
Other Links: manifest | tags
Context
2015-03-29
03:08
Use .meta[title] instead of literal property. Channel list in config_dialog completed again with disabled plugins. New tooltop for comment doc section. check-in: ead195d25c user: mario tags: trunk
03:06
More complete plugin_meta() regex patterns. Now defaults to inspect.get_comments() on previous stack frame, src= or fn= only accepted as fallback. Complete default meta fields, splits doc block out, autostrips hash prefixes and indentation. check-in: 70c2e437fd user: mario tags: trunk
03:03
Removed static .title and .module properties. Plugin discovery now realized through pkgutil.iter_modules(). check-in: 2b3b50b6ad user: mario tags: trunk
Changes

Modified config.py from [cb67ede034] to [1c74200e99].

1
2
3
4
5
6
7
8
9
10
11
#
# encoding: UTF-8
# api: streamtuner2
#  .2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config: {type:var, name:z, description:v}
#
# In the main application or module files which need access
# to a global conf object, just import this module as follows:



<







1
2
3

4
5
6
7
8
9
10
#
# encoding: UTF-8
# api: streamtuner2

# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config: {type:var, name:z, description:v}
#
# In the main application or module files which need access
# to a global conf object, just import this module as follows:
27
28
29
30
31
32
33
34
35
36


37
38
39
40
41
42
43
import inspect


# export symbols
__all__ = ["conf", "__print__", "dbg", "plugin_meta"]



#-- create a single instance of config object
conf = object()




#-- global configuration data               ---------------------------------------------
class ConfigDict(dict):


        # start







<
|

>
>







26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42
43
import inspect


# export symbols
__all__ = ["conf", "__print__", "dbg", "plugin_meta"]



#-- create a stub instance of config object
conf = object()




#-- global configuration data               ---------------------------------------------
class ConfigDict(dict):


        # start
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
                else:
                    return # file not found
                # decode
                r = json.load(f)
                f.close()
                return r
            except Exception as e:
                print(dbg.ERR, "PSON parsing error (in "+name+")", e)
            

        # recursive dict update
        def update(self, with_new_data):
            for key,value in with_new_data.items():
                if type(value) == dict:
                    self[key].update(value)







|







177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
                else:
                    return # file not found
                # decode
                r = json.load(f)
                f.close()
                return r
            except Exception as e:
                print(dbg.ERR, "JSON parsing error (in "+name+")", e)
            

        # recursive dict update
        def update(self, with_new_data):
            for key,value in with_new_data.items():
                if type(value) == dict:
                    self[key].update(value)
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
                    return d+"/"+file



# Plugin meta data extraction
#
# Extremely crude version for Python and streamtuner2 plugin usage.
# Doesn't check top-level comment coherency.
# But supports plugins within python zip archives.
#
rx_zipfn  = re.compile(r"""^(.+\.(?:zip|pyz|pyzw|pyzip)(?:\.py)?)/(\w.*)$""")
rx_meta   = re.compile(r"""^ {0,4}# *([\w-]+): *(.+(\n *#  +(?![\w-]+:).+)*)$""", re.M)  # Python comments only
rx_lines  = re.compile(r"""\n *# """)        # strip multi-line prefixes
rx_config = re.compile(r"""[\{\<](.+?)[\}\>]""")     # extract only from JSOL/YAML scheme
rx_fields = re.compile(r"""["']?(\w+)["']?\s*[:=]\s*["']?([^,]+)(?<!["'])""")  # simple key: value entries
#
def plugin_meta(fn=None, frame=1, src=""):

    # filename of caller
    if not fn:
        fn = inspect.getfile(sys._getframe(frame))



    # within zip archive?

    zip = rx_zipfn.match(fn)
    if zip and zipfile.is_zipfile(zip.group(1)):
        src = zipfile.ZipFile(zip.group(1), "r").read(zip.group(2))
    else:
        src = open(fn).read(4096)

    # defaults
    meta = {

        "fn": fn,



        "id": os.path.basename(fn).replace(".py", "")









    }
    # extraction

    for field in rx_meta.findall(src):
        meta[field[0]] = rx_lines.sub("", field[1])




    # unpack config: structures

    meta["config"] = [


        dict([field for field in rx_fields.findall(entry)])
        for entry in rx_config.findall(meta.get("config", ""))
    ]
        

    return meta



























# wrapper for all print statements
def __print__(*args):
    if conf.debug:
        print(" ".join([str(a) for a in args]))


# error colorization
dbg = type('obj', (object,), {
    "ERR":  r"[ERR]",  # red    ERROR
    "INIT": r"[INIT]", # red    INIT ERROR
    "PROC": r"[PROC]", # green  PROCESS
    "CONF": r"[CONF]", # brown  CONFIG DATA
    "UI":   r"[UI]",   # blue   USER INTERFACE BEHAVIOUR
    "HTTP": r"[HTTP]", # magenta HTTP REQUEST
    "DATA": r"[DATA]", # cyan   DATA
    "INFO": r"[INFO]", # gray   INFO
    "STAT": r"[STATE]", # gray  CONFIG STATE
})


   



#-- actually fill global conf instance
conf = ConfigDict()
if conf:
    __print__(dbg.PROC, "ConfigDict() initialized")








|
<
<
<
<
<
<
<
<
|

|
|
|
>
>

|
>
|
|
|
|
|



>

>
>
>
|
>
>
>
>
>
>
>
>
>
|
|
>
|
|
>

>
>
|
>
|
>
>
|
<
<
|
>
|

|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>






|

















|
>
>
>
|

<
|
<
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

                    return d+"/"+file



# Plugin meta data extraction
#
# Extremely crude version for Python and streamtuner2 plugin usage.
# Fetches module source, or reads from filename / out of zip package.








def plugin_meta(fn=None, src=None, frame=1):

    # get source directly from caller
    if not src and not fn:
        module = inspect.getmodule(sys._getframe(frame))
        fn = inspect.getsourcefile(module)
        src = inspect.getcomments(module)

    # within zip archive or dir?
    elif fn:
        zip = rx.zipfn.match(fn)
        if zip and zipfile.is_zipfile(zip.group(1)):
            src = zipfile.ZipFile(zip.group(1), "r").read(zip.group(2))
        else:
            src = open(fn).read(4096)

    # defaults
    meta = {
        "id": re.sub("\.\w+$", "", os.path.basename(fn or "")),
        "fn": fn,
        "title": fn, "description": "no description", "config": [],
        "type": "module", "api": "python", "doc": ""
    }

    # extract coherent comment block, split doc section
    src = rx.comment.search(src)
    if not src:
        __print__(dbg.ERR, "Couldn't read source meta information", fn)
        return meta
    src = src.group(0)
    src = rx.hash.sub("", src).strip()
    if src.find("\n\n") > 0:
        src, meta["doc"] = src.split("\n\n", 1)
    

    # split into dict
    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
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
class rx:
    zipfn   = re.compile(r"""
        ^ (.+  \.(?:zip|pyz|pyzw|pyzip)        # zip-wrapping extensions
        (?:\.py)? ) /(\w.*) $
    """, re.X)
    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)




# wrapper for all print statements
def __print__(*args):
    if "debug" in conf:
        print(" ".join([str(a) for a in args]))


# error colorization
dbg = type('obj', (object,), {
    "ERR":  r"[ERR]",  # red    ERROR
    "INIT": r"[INIT]", # red    INIT ERROR
    "PROC": r"[PROC]", # green  PROCESS
    "CONF": r"[CONF]", # brown  CONFIG DATA
    "UI":   r"[UI]",   # blue   USER INTERFACE BEHAVIOUR
    "HTTP": r"[HTTP]", # magenta HTTP REQUEST
    "DATA": r"[DATA]", # cyan   DATA
    "INFO": r"[INFO]", # gray   INFO
    "STAT": r"[STATE]", # gray  CONFIG STATE
})




   

#-- populate global conf instance
conf = ConfigDict()

__print__(dbg.PROC, "ConfigDict() initialized")