pwr_tray 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pwr_tray/IniTool.py ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ TBD
5
+ """
6
+ # pylint: disable=invalid-name, broad-exception-caught
7
+ # pylint: disable=too-many-branches,too-many-statements
8
+ # pylint: disable=too-many-instance-attributes
9
+ # pylint: disable=consider-using-from-import
10
+ # pylint: disable=
11
+ # pylint: disable=
12
+
13
+ import os
14
+ import configparser
15
+ from types import SimpleNamespace
16
+ import copy
17
+ import json
18
+ from pwr_tray.Utils import prt
19
+
20
+ class IniTool:
21
+ """ Configued Params for this class"""
22
+ def __init__(self, paths_only=False):
23
+ self.defaults = {
24
+ 'Settings': {
25
+ 'i3lock_args': '-t -i ./lockpaper.png',
26
+ 'swaylock_args': '-i ./lockpaper.png',
27
+ 'debug_mode': False,
28
+ 'power_down': False,
29
+ 'turn_off_monitors': False,
30
+ 'lock_min_list': '[15, 30]',
31
+ 'sleep_min_list': '[5, 30]',
32
+ 'lo_battery_pct': 10,
33
+ 'gui_editor': 'geany',
34
+ # 'dim_pct_brightness': 100,
35
+ # 'dim_pct_lock_min': 100,
36
+
37
+ }, 'HiBattery': { #was OnBattery
38
+ 'power_down': False,
39
+ 'lock_min_list': '[10, 20]',
40
+ 'sleep_min_list': '[1, 10]',
41
+ # 'dim_pct_brightness': 50,
42
+ # 'dim_pct_lock_min': 70,
43
+
44
+ }, 'LoBattery': {
45
+ 'power_down': True,
46
+ 'lock_min_list': '[1]',
47
+ 'sleep_min_list': '[1]',
48
+ # 'dim_pct_brightness': 50,
49
+ # 'dim_pct_lock_min': 70,
50
+ }
51
+ }
52
+ self.folder = os.path.expanduser("~/.config/pwr-tray")
53
+ self.ini_path = os.path.join(self.folder, "config.ini")
54
+ self.log_path = os.path.join(self.folder, "debug.log")
55
+ self.picks_path = os.path.join(self.folder, "picks.json")
56
+ self.config = configparser.ConfigParser()
57
+ self.last_mod_time = None
58
+ self.section_params = {'Settings': {}, 'HiBattery': {}, 'LoBattery': {}, }
59
+ self.params_by_selector = {}
60
+ if not paths_only:
61
+ self.ensure_ini_file()
62
+ os.chdir(self.folder)
63
+
64
+ @staticmethod
65
+ def get_selectors():
66
+ """ Returns the in right "order" """
67
+ return 'Settings HiBattery LoBattery'.split()
68
+
69
+ def the_default(self, selector, key):
70
+ """ return the default value given the selector and key """
71
+ return self.defaults[selector][key]
72
+
73
+ def get_current_vals(self, selector, list_name):
74
+ """ Expecting a list of two or more non-zero ints """
75
+ if selector in self.params_by_selector and hasattr(self.params_by_selector[selector], list_name):
76
+ vals = getattr(self.params_by_selector[selector], list_name)
77
+ if isinstance(vals, list) and len(vals) >= 2:
78
+ return vals
79
+ return self.the_default(selector, list_name) # should not get here
80
+
81
+ def get_rotated_vals(self, selector, list_name, first):
82
+ """ TBD """
83
+ vals = self.get_current_vals(selector, list_name)
84
+ if first in vals:
85
+ while vals[0] != first:
86
+ vals = vals[1:] + vals[:1]
87
+ setattr(self.params_by_selector[selector], list_name, vals)
88
+ return vals
89
+
90
+ def ensure_ini_file(self):
91
+ """Check if the config file exists, create it if not."""
92
+ if not os.path.exists(self.folder):
93
+ os.makedirs(self.folder, exist_ok=True)
94
+ if not os.path.exists(self.ini_path):
95
+ self.config.read_dict(self.defaults)
96
+ with open(self.ini_path, 'w', encoding='utf-8') as configfile:
97
+ self.config.write(configfile)
98
+
99
+ def update_config(self):
100
+ """ Check if the file has been modified since the last read """
101
+ def to_array(val_str):
102
+ # Expecting string of form: "[1,2,...]" or just "20"
103
+ try:
104
+ vals = json.loads(val_str)
105
+ except Exception:
106
+ return None
107
+ if isinstance(vals, int):
108
+ vals = [vals]
109
+ if not isinstance(vals, list):
110
+ return None
111
+ rvs = []
112
+ for val in vals:
113
+ if isinstance(val, int) and val > 0:
114
+ rvs.append(val)
115
+ if not rvs:
116
+ return None
117
+ if len(rvs) == 1: # always want two
118
+ rvs.append(vals[0])
119
+ return rvs
120
+
121
+ current_mod_time = os.path.getmtime(self.ini_path)
122
+ if current_mod_time == self.last_mod_time:
123
+ return False # not updated
124
+ # Re-read the configuration file if it has changed
125
+ self.config.read(self.ini_path)
126
+ self.last_mod_time = current_mod_time
127
+
128
+ goldens = self.defaults['Settings']
129
+ running = goldens
130
+ all_params = {}
131
+
132
+ # Access the configuration values in order
133
+ prt('parsing config.ini...')
134
+ for selector in self.get_selectors():
135
+ all_params[selector] = params = copy.deepcopy(running)
136
+ if selector not in self.config:
137
+ all_params[selector] = SimpleNamespace(**params)
138
+ continue
139
+
140
+ # iterate the candidates
141
+ candidates = dict(self.config[selector])
142
+ for key, value in candidates.items():
143
+ if key not in goldens:
144
+ prt(f'skip {selector}.{key}: {value!r} [unknown key]')
145
+ continue
146
+
147
+ if key.endswith('_list'):
148
+ list_value = to_array(value)
149
+ if not value:
150
+ params[key] = self.the_default(selector, key)
151
+ prt(f'skip {selector}.{key}: {value!r} [bad list spec]')
152
+ else:
153
+ params[key] = list_value
154
+ continue
155
+
156
+ if isinstance(goldens[key], bool):
157
+ if isinstance(value, str):
158
+ if value.lower() == 'true':
159
+ value = True
160
+ elif value.lower() == 'false':
161
+ value = False
162
+ if isinstance(value, bool):
163
+ params[key] = value
164
+ else:
165
+ params[key] = self.the_default(selector, key)
166
+ prt(f'skip {selector}.{key}: {value!r} [expecting bool]')
167
+ continue
168
+
169
+ if isinstance(goldens[key], int):
170
+ try:
171
+ params[key] = int(value)
172
+ continue
173
+ except Exception:
174
+ params[key] = self.the_default(selector, key)
175
+ prt(f'skip {selector}.{key}: {value!r} [expecting int repr]')
176
+ continue
177
+
178
+ if isinstance(goldens[key], str):
179
+ if isinstance(value, str):
180
+ params[key] = value
181
+ else:
182
+ params[key] = self.the_default(selector, key)
183
+ prt(f'skip {selector}.{key}: {value!r} [expecting string]')
184
+ continue
185
+
186
+ assert False, f'unhandled goldens[{key}]: {value!r}'
187
+ all_params[selector] = SimpleNamespace(**params)
188
+
189
+ self.params_by_selector = all_params
190
+
191
+ prt('DONE parsing config.ini...')
192
+
193
+ return True # updated
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Sway does not provide a way to get the idle time, and so the fundamental design
5
+ of pwr-tray using idle time does nto work. Instead, for sway, we must basically
6
+ set up swayidle to run our config, and if there are special actions like
7
+ blank now, we have to kill the running swayidle and start one that does what
8
+ we want.
9
+ """
10
+ # pylint: disable=invalid-name,consider-using-with
11
+ import subprocess
12
+ import time
13
+ import os
14
+ import signal
15
+ from types import SimpleNamespace
16
+ from pwr_tray.Utils import prt
17
+
18
+ class SwayIdleManager:
19
+ """ Class to manage 'swayidle' """
20
+ def __init__(self, applet):
21
+ self.process = None
22
+ self.applet = applet
23
+ self.current_cmd = ''
24
+ # we construct the sway idle from these clauses which various
25
+ # substitutions.
26
+ self.clauses = SimpleNamespace(
27
+ leader="""exec swayidle""",
28
+ locker=""" timeout [lock_s] '[screenlock] [lockopts]'""",
29
+ blanker=""" timeout [blank_s] 'swaymsg "output * dpms off"'""",
30
+ sleeper=""" timeout [sleep_s] 'systemctl suspend'""",
31
+ # dimmer="""\\\n timeout [dim_s] 'brightnessctl set 50%'""", # perms?
32
+ before_sleep=""" before-sleep '[screenlock] [lockopts]'""",
33
+ after_resume=""" after-resume '[unblank]'""",
34
+ # + """ 'pgrep -x copyq || copyq --start-server hide;"""
35
+ # + """ pgrep -x nm-applet || nm-applet [undim][dpmsOn]'""",
36
+ # + """ pgrep -x nm-applet || nm-applet [unblank]'""",
37
+ # undim = """; brightnessctl set 100%""",
38
+ screenlock = """pkill swaylock ; exec swaylock --ignore-empty-password --show-failed-attempts""",
39
+ unblank='''; swaymsg "output * dpms on"''',
40
+ )
41
+ self.kill_other_swayidle()
42
+
43
+ @staticmethod
44
+ def kill_other_swayidle():
45
+ """ Kills any stray swayidles"""
46
+ try:
47
+ pids = subprocess.check_output(['pgrep', 'swayidle']).decode().split()
48
+ if pids:
49
+ subprocess.run(['pkill', 'swayidle'])
50
+ time.sleep(2) # Wait a moment to ensure processes are terminated
51
+
52
+ pids = subprocess.check_output(['pgrep', 'swayidle']).decode().split()
53
+ if pids:
54
+ prt(f"Force killing remaining swayidle processes... {pids}")
55
+ for pid in pids:
56
+ os.kill(int(pid), signal.SIGKILL)
57
+ return
58
+ except Exception:
59
+ return # none left
60
+
61
+ def build_cmd(self, mode=None):
62
+ """ Build the swayidle command line from the current statue. """
63
+
64
+ # lock_s, lockopts, sleep_s, blank_s, dim_s = None, '', None, None, None
65
+ lock_s, lockopts, sleep_s, blank_s = None, '', None, None
66
+ mode = mode if mode else self.applet.get_effective_mode()
67
+ til_sleep_s, sleeping = None, False
68
+
69
+ lockopts = self.applet.get_params().swaylock_args
70
+ quick = self.applet.quick
71
+ a_minute = 30 if quick else 60
72
+
73
+ if mode in ('LockOnly', 'SleepAfterLock'):
74
+ lock_s = self.applet.get_lock_min_list()[0] * a_minute
75
+ til_sleep_s = lock_s
76
+ if self.applet.get_params().turn_off_monitors:
77
+ blank_s = 20 + lock_s
78
+ if mode in ('SleepAfterLock', ):
79
+ sleep_s = self.applet.get_sleep_min_list()[0] * a_minute
80
+ til_sleep_s = sleep_s
81
+ sleeping = True
82
+
83
+ til_sleep_s, blanking = 0, False
84
+ cmd = self.clauses.leader
85
+ if isinstance(lock_s, (int,float)) and lock_s >= 0:
86
+ til_sleep_s += lock_s
87
+ cmd += self.clauses.locker.replace(
88
+ "[lock_s]", str(lock_s)).replace(
89
+ '[lockopts]', lockopts).replace(
90
+ '[screenlock]', self.clauses.screenlock)
91
+ if sleeping:
92
+ til_sleep_s += sleep_s
93
+ cmd += self.clauses.sleeper.replace("[sleep_s]", str(til_sleep_s))
94
+ if blank_s is not None:
95
+ blanking = True
96
+ cmd += self.clauses.blanker.replace('[blank_s]', str(blank_s))
97
+ # if isinstance(dim_s, (int,float)) and dim_s >= 0:
98
+ # dimming = True
99
+ # cmd += self.clauses.dimmer.replace('[dim_s]', str(dim_s))
100
+ cmd += self.clauses.before_sleep.replace(
101
+ "[sleep_s]", str(til_sleep_s)).replace(
102
+ '[lockopts]', lockopts).replace(
103
+ '[screenlock]', self.clauses.screenlock)
104
+ # cmd += self.clauses.after_resume.replace(
105
+ # "[undim]", self.clauses.undim if dimming else '')
106
+ if blanking:
107
+ cmd += self.clauses.after_resume.replace(
108
+ "[unblank]", self.clauses.unblank if blanking else '')
109
+
110
+ rv, self.current_cmd = bool(cmd != self.current_cmd), cmd
111
+ if rv:
112
+ prt('NEW-SWAYIDLE:', self.current_cmd)
113
+
114
+ return rv # whether updated
115
+
116
+ def start(self):
117
+ """ Build and start the command from the current state """
118
+ updated = self.build_cmd()
119
+ if self.process and updated:
120
+ self.stop()
121
+ if updated:
122
+ prt(f'SWAYIDLE: {self.current_cmd}')
123
+
124
+ if not self.process and self.current_cmd:
125
+ self.process = subprocess.Popen(self.current_cmd, shell=True)
126
+
127
+ return self.process
128
+
129
+ def stop(self):
130
+ """ Stop the current swayidle (normally to replace it) """
131
+ self.process.terminate()
132
+ self.process.wait()
133
+ self.process = None
134
+
135
+ def checkup(self):
136
+ """ Check whether swayidle is running, normally to restart it
137
+ with the current command. """
138
+ if self.process.poll() is not None:
139
+ self.start()
pwr_tray/Utils.py ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Utility functions
5
+ """
6
+ # pylint: disable=invalid-name,broad-exception-caught
7
+ # pylint: disable=consider-using-with,global-statement
8
+ # pylint: disable=too-few-public-methods
9
+
10
+ import os
11
+ import sys
12
+ import stat
13
+ import time
14
+ import signal
15
+ import shutil
16
+ import subprocess
17
+ import inspect
18
+ from datetime import datetime
19
+ from io import StringIO
20
+
21
+ import importlib.resources as pkg_resources
22
+ # from pathlib import Path
23
+
24
+ prt_kb = 512
25
+ prt_path = ''
26
+ prt_to_init = True
27
+
28
+ def copy_to_folder(resource_name, dest):
29
+ """ Get the path of a resource """
30
+ with pkg_resources.path('pwr_tray.resources', resource_name) as file_path:
31
+ shutil.copy(file_path, os.path.join(dest, resource_name))
32
+ return file_path
33
+
34
+
35
+ def where(above=0):
36
+ """Get the file and line of the caller. Arguments:
37
+ - above -- how many frames to go up (or down) from the reference
38
+ frame (which is 2 above). above=0 means the caller of the
39
+ caller of the function (which is the frame ofkjk interest usually).
40
+ Then keep going up until we get out of this file if possible
41
+ Returns:
42
+ [file:line]
43
+ """
44
+ stack, frameNo = inspect.stack(), 2 + above
45
+ if frameNo < 0 or len(stack) < frameNo + 2:
46
+ return '[n/a]'
47
+ filename, line_number = stack[frameNo][1:3]
48
+ # lg.db(f'YES {os.path.basename(filename)}:{line_number}')
49
+ return f'[{filename.split("/")[-1]}:{line_number}]'
50
+ # return f'[:{line_number}]'
51
+
52
+
53
+ def prt(*args, **kwargs):
54
+ """ Our custom print routine ...
55
+ - use instead of print() to get time stamps.
56
+ - unless stdout is a tty, say for debugging, ~/.config/pwr-tray/debug.log is used for stdout
57
+ - if we create a log file, its size is limited to 512K and then it is truncated
58
+ """
59
+ def check_stdout(use_stdout=None):
60
+ global prt_to_init
61
+ def is_tty():
62
+ try:
63
+ os.ttyname(sys.stdout.fileno())
64
+ return True
65
+ except Exception:
66
+ return False
67
+ def is_reg():
68
+ try:
69
+ return stat.S_ISREG(os.fstat(sys.stdout.fileno()).st_mode)
70
+ except Exception:
71
+ return False
72
+ def reopen():
73
+ global prt_to_init
74
+ sys.stdout = open(prt_path, "a+", encoding='utf-8')
75
+ sys.stderr = sys.stdout
76
+ os.dup2(sys.stdout.fileno(), 1)
77
+ os.dup2(sys.stderr.fileno(), 2)
78
+ prt_to_init = False
79
+
80
+ if prt_kb > 0 and prt_path: # non-positive disables stdout "tuning"
81
+ if use_stdout is False:
82
+ reopen()
83
+ elif prt_to_init and sys.stdout.closed: # Check if stdout is closed
84
+ reopen()
85
+ elif prt_to_init and not is_tty() and not is_reg():
86
+ reopen()
87
+ if os.fstat(sys.stdout.fileno()).st_size > prt_kb*1024:
88
+ shutil.move(prt_path, f'{prt_path}1')
89
+ reopen()
90
+
91
+ to_stdout = None
92
+ if 'to_stdout' in kwargs:
93
+ to_stdout = bool(kwargs['to_stdout'])
94
+ del kwargs['to_stdout']
95
+ check_stdout(to_stdout)
96
+
97
+ dt = datetime.now().strftime('%m-%d^%H:%M:%S')
98
+ s = StringIO()
99
+ print(dt, end=' ', file=s)
100
+ kwargs['end'] = ' '
101
+ kwargs['file'] = s
102
+ print(*args, **kwargs)
103
+ string = f'{s.getvalue()} {where()}'
104
+ print(string, flush=True)
105
+
106
+ def x_restart_self():
107
+ """ TBD """
108
+ # Retrieve the current script name and arguments
109
+ script_name = sys.argv[0]
110
+ script_args = sys.argv[1:]
111
+
112
+ # Use subprocess to run the script with the same arguments
113
+ subprocess.run([sys.executable, script_name] + script_args, check=True)
114
+
115
+ class PyKill:
116
+ """Class to kill python scripts and explain how; avoids suicide. """
117
+ def __init__(self):
118
+ self.last_sig = {} # keyed by target
119
+ self.alive = {} # ps line of found targets
120
+
121
+ def _kill_script(self, targets, sig):
122
+ """ one iteration through very instance of the remaining processes """
123
+ def kill_pid(pid, sig):
124
+ # terminate the process
125
+ alive = True
126
+ try:
127
+ os.kill(pid, sig)
128
+ except OSError:
129
+ alive = False
130
+ return alive
131
+ self.alive = {} # reset each time thru
132
+ for line in os.popen("ps -eo pid,args"):
133
+ words = line.split()
134
+ if len(words) < 3:
135
+ continue
136
+ pid, cmd = int(words[0]), os.path.basename(words[1])
137
+ if pid == os.getpid():
138
+ continue # no suicides
139
+ script = os.path.basename(words[2])
140
+ for target in targets:
141
+ if cmd in ('python', 'python3') and script in (target, f'{target}.py'):
142
+ self.last_sig[target] = sig
143
+ if kill_pid(pid, sig): # returns True if alive
144
+ self.alive[target] = line
145
+ return self.alive
146
+
147
+ def kill_loop(self, targets):
148
+ """Loops thru the remaining process until all or gone or we
149
+ become exhausted (e.g., unkillable processes)."""
150
+ targets = targets if isinstance(targets, (list, tuple)) else [targets]
151
+ for sig in [signal.SIGTERM, signal.SIGTERM, signal.SIGTERM,
152
+ signal.SIGKILL, signal.SIGKILL,]:
153
+ if not self._kill_script(targets, sig):
154
+ break
155
+ time.sleep(1)
156
+ for target, line in self.alive.items():
157
+ prt(f'ALERT: running: {line} [sig={self.last_sig[target]}]')
158
+ for target in targets:
159
+ if target in self.alive:
160
+ continue
161
+ prt(f'INFO: gone: {target} [sig={self.last_sig.get(target, None)}]')
162
+ return not bool(self.alive)
pwr_tray/YamlDump.py ADDED
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Dump (almost) any object in YAML format.
4
+ Supports mixing flow / non-flow nodes to handles cases where non-flow makes the
5
+ dump have an absurd line count with many trite lines.
6
+
7
+ This *may* require customization if types are used for which this does not work;
8
+ search for 'YourClassThatDoesNotWork' to see where to customize your outlier cases.
9
+
10
+ """
11
+ # pylint: disable=invalid-name
12
+
13
+ import threading
14
+ import textwrap
15
+ from io import StringIO
16
+ from collections import OrderedDict
17
+ from ruamel.yaml import YAML, comments as yaml_comments
18
+ from LibGen.CustLogger import CustLogger as lg
19
+ # import ToolBase as tb
20
+
21
+ #yaml = YAML(typ='safe') # NOTE: cannot have 'safe' and mixed block/flow style
22
+ yaml = YAML()
23
+ yaml.default_flow_style = False
24
+
25
+ flow = threading.local()
26
+ flow.flow_nodes = None
27
+
28
+ def yamlize(obj):
29
+ """Make a complex object (e.g., class objects) more capable of
30
+ being dumped in yaml by simplifying the types where possible."""
31
+ # pylint: disable=too-many-return-statements
32
+ if isinstance(obj, list):
33
+ rv = []
34
+ for value in obj:
35
+ rv.append(yamlize(value))
36
+ return rv
37
+
38
+ if isinstance(obj, dict):
39
+ rv = OrderedDict()
40
+ for key, value in obj.items():
41
+ rv[key] = yamlize(value)
42
+ return rv
43
+
44
+ # this handles some crazy types like:
45
+ # rumel.yaml.scalarfloat.ScalarFloat
46
+ # when the object was converted from yaml
47
+ if isinstance(obj, bool):
48
+ return bool(obj)
49
+ if isinstance(obj, int):
50
+ return int(obj)
51
+ if isinstance(obj, float):
52
+ return float(obj)
53
+ # NOTE: if there are objects not handled, then fix here
54
+ # if isinstance(obj, YourClassThatDoesNotWork):
55
+ # return str(obj) # or whatever type if str is not appropriate
56
+
57
+ # convert a potentially recursive object into a dictionary
58
+ # - ignore variables that start with '_'
59
+ if not hasattr(obj, '__dict__'):
60
+ return obj
61
+ raw_rv = vars(obj)
62
+ rv = {}
63
+ for key, val in raw_rv.items():
64
+ if key.startswith('_'):
65
+ continue
66
+ rv[key] = yamlize(val)
67
+ return rv
68
+
69
+ def set_flow(obj, flow_nodes=None):
70
+ """Set flow style for certain nodes...
71
+ Ref: https://stackoverflow.com/questions/63364894/
72
+ how-to-dump-only-lists-with-flow-style-with-pyyaml-or-ruamel-yaml
73
+ """
74
+ obj = yamlize(obj)
75
+ if flow_nodes:
76
+ flow.flow_nodes = flow_nodes
77
+ set_flow_recursive(obj)
78
+ return obj
79
+
80
+ def set_flow_recursive(obj):
81
+ """TBD"""
82
+ if isinstance(obj, dict):
83
+ # print('dict...')
84
+ for key, value in obj.items():
85
+ # print('key...', key)
86
+ if key in flow.flow_nodes:
87
+ if isinstance(value, dict):
88
+ # print('...', key, '/map')
89
+ value = yaml_comments.CommentedMap(value)
90
+ value.fa.set_flow_style()
91
+ obj[key] = value
92
+ elif isinstance(value, list):
93
+ # print('...', key, '/list')
94
+ value = yaml_comments.CommentedSeq(value)
95
+ value.fa.set_flow_style()
96
+ obj[key] = value
97
+ else:
98
+ set_flow_recursive(value)
99
+ elif isinstance(obj, list):
100
+ # print('list...')
101
+ for value in obj:
102
+ set_flow_recursive(value)
103
+
104
+ def yaml_to_file(obj, fileh, flow_nodes=None):
105
+ """Do a yaml-to-file conversion of an object (w/o indent)."""
106
+ obj = set_flow(obj, flow_nodes)
107
+ yaml.dump(obj, fileh)
108
+
109
+ def yaml_str(obj, indent=8, width=70, flow_nodes=None):
110
+ """Do a yaml-to-string conversion of an object with indent by default."""
111
+ outs = StringIO()
112
+ obj = set_flow(obj, flow_nodes)
113
+ old_width, yaml.width = yaml.width, width
114
+ yaml.dump(obj, outs)
115
+ yaml.width = old_width
116
+ return textwrap.indent(outs.getvalue(), prefix=' '*indent)
117
+
118
+ def yaml_dump(obj, indent=8, flow_nodes=None):
119
+ """Do a yaml dump of an object to stdout with indent by default1."""
120
+ lg.pr(yaml_str(obj, indent=indent, flow_nodes=flow_nodes).rstrip())
121
+
122
+
123
+ def runner(argv):
124
+ """Several simple tests for YamlDump including exercising flow_nodes."""
125
+ # pylint: disable=broad-except, import-outside-toplevel, using-constant-test
126
+ def tester_func():
127
+ """TBD"""
128
+ import sys
129
+ import copy
130
+
131
+ scrum_orig = {'flow_sec': 55, 'hash': 'cb217daa4aedf6ad8483a9333f6cc114f413cc7b',
132
+ 'name': 'Eche Palante - Refle', 'progress': 100, 'ratio': 0.8502833247184753,
133
+ 'samples': [[10.9, 10, 2.02], [15.7, 23, 1.14], [18.1, 32, 1.06],
134
+ [20.5, 42, 0.91], [22.9, 50, 0.93], [30.1, 65, 0.98], [34.9, 75, 0.91],
135
+ [39.7, 83, 0.88], [44.5, 90, 0.89], [54.1, 100, 0.85]],
136
+ 'scrum_form_sec': 8, 'scrum_sz': 12, 'time': 1621850682.8617778,
137
+ 'tor_sz': 249262127, 'up_client': 'qBittorrent/4.3.5', 'up_sec': 0}
138
+
139
+ if True:
140
+ scrum = copy.deepcopy(scrum_orig)
141
+ print('\n\n===== TEST 1:', 'yaml.dump(scrum, sys.stdout)')
142
+ yaml.dump(scrum, sys.stdout)
143
+
144
+ if True:
145
+ scrum = copy.deepcopy(scrum_orig)
146
+ print('\n\n===== TEST 2:', 'surgical CommentedSeq')
147
+ obj = yaml_comments.CommentedSeq(scrum['samples'])
148
+ obj.fa.set_flow_style()
149
+ scrum['samples'] = obj
150
+ yaml.dump(scrum, sys.stdout)
151
+
152
+ if True:
153
+ scrum = copy.deepcopy(scrum_orig)
154
+ print('\n\n===== TEST 3:', 'designed API')
155
+ yaml_dump(scrum, flow_nodes=('samples',))
156
+
157
+ import argparse
158
+ parser = argparse.ArgumentParser()
159
+ parser.add_argument('-V', '--log-level', choices=lg.choices,
160
+ default='INFO', help='set logging/verbosity level [dflt=INFO]')
161
+ opts = parser.parse_args(argv)
162
+ lg.setup(level=opts.log_level)
163
+ tester_func()
pwr_tray/__init__.py ADDED
File without changes