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

⌈⌋ ⎇ branch:  streamtuner2


Check-in [1eea3140f8]

Overview
Comment:Move argv initialization to conf.apply_args(). Document config: format for argparse conversion. Enable file=sys.stderr for __print__/debug messages.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1: 1eea3140f86b5cfbbc17719027dc57938ccaa537
User & Date: mario on 2015-04-07 05:51:36
Other Links: manifest | tags
Context
2015-04-07
05:53
Fix a few CLI bugs (doesn't work yet with dynamic module list), stub_parent() implementations for non-GUI mode should be merged. check-in: a7c3f7336a user: mario tags: trunk
05:51
Move argv initialization to conf.apply_args(). Document config: format for argparse conversion. Enable file=sys.stderr for __print__/debug messages. check-in: 1eea3140f8 user: mario tags: trunk
2015-04-06
18:55
Add workaround for ArgumentParser, which tries to map config: descriptors onto AP.add_argument(*yikes) params. check-in: 24a5fe69a1 user: mario tags: trunk
Changes

Makefile became executable with contents [d3f05bad3b].

Modified config.py from [af23ce772a] to [ca9c88608a].

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
#
# encoding: UTF-8
# api: streamtuner2
# type: class
# title: global config object
# description: reads ~/.config/streamtuner/*.json files
# config:
#    { arg: -d,     type: str,      name: plugin[],  description: omit plugin from initialization  }

#    { arg: --gtk3, type: boolean,  name: gtk3,      description: use gtk3 interface }
#    { arg: -D,     type: boolean,  name: debug,     description: enable debug messages on console }
#    { arg: action, type: str*,     name: action[],  description: commandline actions }
#    { arg: -x,     type: boolean,  name: exit,      description: terminate right away }


#
# In the main application or module files which need access
# to a global conf.* object, just import this module as follows:
#
#   from config import *
#
# Here conf is already an instantiation of the underlying
# ConfigDoct class.
#
# Also provides the logging function __print__, and basic
# plugin handling code: plugin_meta() and module_list(),
# and the relative get_data() alias (files from pyzip/path).
#


import os
import sys
import json
import gzip
import platform
import re
from compat2and3 import gzip_decode, find_executable







|
>
|
|
|
|
>
>














|







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
#
# 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 }
# version: 2.5
# priority: core
#
# In the main application or module files which need access
# to a global conf.* object, just import this module as follows:
#
#   from config import *
#
# Here conf is already an instantiation of the underlying
# ConfigDoct class.
#
# Also provides the logging function __print__, and basic
# plugin handling code: plugin_meta() and module_list(),
# and the relative get_data() alias (files from pyzip/path).
#

from __future__ import print_function
import os
import sys
import json
import gzip
import platform
import re
from compat2and3 import gzip_decode, find_executable
83
84
85
86
87
88
89

90
91
92
93
94
95
96
        # store defaults in file
        else:
            self.save("settings")
            self.firstrun = 1
        
        # add argv
        self.args = self.init_args(argparse.ArgumentParser())



    # some defaults
    def defaults(self):
        self.play = {
           "audio/mpeg": self.find_player(),
           "audio/ogg": self.find_player(),







>







86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
        # store defaults in file
        else:
            self.save("settings")
            self.firstrun = 1
        
        # add argv
        self.args = self.init_args(argparse.ArgumentParser())
        self.apply_args(self.args)


    # some defaults
    def defaults(self):
        self.play = {
           "audio/mpeg": self.find_player(),
           "audio/ogg": self.find_player(),
167
168
169
170
171
172
173
174


175
176
177
178
179
180
181
            os.makedirs(self.dir)
       

    # store some configuration list/dict into a file                
    def save(self, name="settings", data=None, gz=0, nice=0):
        name = name + ".json"
        if (data is None):
            data = dict(self.__dict__)  # ANOTHER WORKAROUND: typecast to plain dict(), else json filter_data sees it as object and str()s it


            nice = 1
        # check for subdir
        if (name.find("/") > 0):
            subdir = name[0:name.find("/")]
            subdir = self.dir + "/" + subdir
            if (not os.path.exists(subdir)):
                os.mkdir(subdir)







|
>
>







171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
            os.makedirs(self.dir)
       

    # store some configuration list/dict into a file                
    def save(self, name="settings", data=None, gz=0, nice=0):
        name = name + ".json"
        if (data is None):
            data = vars(self)
            if "args" in data:
                data.pop("args")
            nice = 1
        # check for subdir
        if (name.find("/") > 0):
            subdir = name[0:name.find("/")]
            subdir = self.dir + "/" + subdir
            if (not os.path.exists(subdir)):
                os.mkdir(subdir)
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
                     netrc = parser(self.xdg() + "/netrc").hosts
            except:
                pass
        for server in varhosts:
            if server in netrc:
                return netrc[server]


    # Use config: definitions for argv extraction

    def init_args(self, ap):
        for opt in plugin_meta(frame=0).get("config"):
            kwargs = self.argparse_map(opt)
            #print kwargs
            if kwargs:
                args = kwargs["args"]
                del kwargs["args"]
                ap.add_argument(*args, **kwargs)
        return ap.parse_args()













    # Transform config: description into quirky ArgumentParser list












    def argparse_map(self, 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|\[\]|store|append|const", opt["type"])
        naming = re.findall("\[\]", opt["name"])
        name   = re.findall("(?<!-)\\b\\w+", opt["name"])
        nargs  = re.findall("\\b\d+\\b|\?|\*", opt["type"]) or [None]





        # Populate partially - ArgumentParser is highly fragile with combinations of named params
        kwargs = {
            "args": args,
            "dest": name[0] if not name[0] in args else None,
            "action": "append" if "[]" in (naming+typing) else ("store_true" if "bool" in typing else "store"),
            "nargs": nargs[0],
            "default": opt["value"],
           # "type":  int if "int" in typing else bool if "bool" in typing else str,
            "choices": opt["select"].split("|") if "select" in opt else None,
           # "required": "required" in opt,
            "help": opt["description"] or "",
        }
        return dict((k,w) for k,w in kwargs.items() if w is not None)










# Retrieve content from install path or pyzip archive (alias for pkgutil.get_data)
#
def get_data(fn, decode=False, gz=False, file_base="config"):
    try:
        bin = pkgutil.get_data(file_base, fn)







>
|
>


|
|
<
<
<
|


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








|


|
>
>
>
>

|



|

|
|

|
|

|

>
>
>
>
>
>
>







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
                     netrc = parser(self.xdg() + "/netrc").hosts
            except:
                pass
        for server in varhosts:
            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=0).get("config"):
            if [kwargs for kwargs in [self.argparse_map(opt)]]:
                #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
        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


    # Transform config: description into quirky ArgumentParser args.
    #
    # · An option entry requires an arg: parameter - unlike regular plugin options:
    #     { arg: -i, name: input[], type: str, description: input files }
    # · Where list elements are indicated by appending `[]` to names, or `*`onto type
    #   specifiers (alternatively `?`, `+` or a numeric count).
    # · Types `str` or `int` and `bool` are recognized (bool with false/true optionals).
    # · Entries can also carry a `hidden: 1` or `required: 1` attribute.
    # · And `help:` is an alias to `description:`
    # · Same for `default:` instead of the normal `value:`
    # · And `type: select` utilizes the `select: a|b|c` format as uaual.
    # · ArgParsers const=, metavar= flag, or type=file are not aliased here.
    #
    def argparse_map(self, 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 = {
            "args": args,
            "dest": name[0] if not name[0] in args else None,
            "action": self.sw( {is_arr: "append"}, {is_bool and false_b: "store_false"}, {is_bool: "store_true"}, {1: "store"} ),
            "nargs": nargs[0],
            "default": opt.get("default") or opt["value"],
            "type":  self.sw( {is_bool: None}, {"int" in typing: int}, {"bool" in typing: bool}, {1: 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}


    # Shorthand switch, returns first value for cond==true from list of {cond:val} arguments
    def sw(self, *args):
        for pair in args:
            [(cond,val)] = pair.items()
            if cond:
                return val


# Retrieve content from install path or pyzip archive (alias for pkgutil.get_data)
#
def get_data(fn, decode=False, gz=False, file_base="config"):
    try:
        bin = pkgutil.get_data(file_base, fn)
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446




# wrapper for all print statements
def __print__(*args):
    if "debug" in conf and conf.debug or args[0] == dbg.ERR:
        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







|







472
473
474
475
476
477
478
479
480
481
482
483
484
485
486




# wrapper for all print statements
def __print__(*args):
    if "debug" in conf and conf.debug or args[0] == dbg.ERR:
        print(" ".join([str(a) for a in args]), file=sys.stderr)


# error colorization
dbg = type('obj', (object,), {
    "ERR":  r"[ERR]",  # red    ERROR
    "INIT": r"[INIT]", # red    INIT ERROR
    "PROC": r"[PROC]", # green  PROCESS

Modified st2.py from [2915a6cb3f] to [cbc6a632be].

487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529




# startup procedure
def main():

    # process a few command line flags
    if conf.args.plugin:
        for p_id in conf.args.plugin:
            conf.plugins[p_id] = 0
    if conf.args.debug
        conf.debug = conf.args.debug
    if conf.args.exit:
        return

    # graphical
    if not len(conf.args.action[0]):

        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()

        # prepare main window
        main = StreamTunerTwo()

        # first invocation
        if (conf.get("firstrun")):
            main.configwin.open(None)
            del conf.firstrun

        # run
        gtk.main()
        __print__(dbg.PROC, r" gtk_main_quit ")
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI(conf.args.action[0])

# run
if __name__ == "__main__":
    main()








<
<
<
<
<
<
<
<
<

|



















|





487
488
489
490
491
492
493









494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520




# startup procedure
def main():










    # graphical
    if not len(conf.args.action):

        # prepare for threading in Gtk+ callbacks
        gobject.threads_init()

        # prepare main window
        main = StreamTunerTwo()

        # first invocation
        if (conf.get("firstrun")):
            main.configwin.open(None)
            del conf.firstrun

        # run
        gtk.main()
        __print__(dbg.PROC, r" gtk_main_quit ")
        
    # invoke command-line interface
    else:
        import cli
        cli.StreamTunerCLI(conf.args.action)

# run
if __name__ == "__main__":
    main()