rework and use json for config

This commit is contained in:
Izalia Mae 2021-07-12 22:30:35 -04:00
parent 206032e797
commit 81d50cf827
4 changed files with 155 additions and 195 deletions

View file

@ -1,5 +1,5 @@
__script__ = 'PyReloader'
__version__ = (0, 3, 0)
__version__ = (0, 4, 0)
__year__ = 2021
__author__ = 'Zoey Mae'
__url__ = 'htts://git.barkshark.xyz/izaliamae/reload'
@ -13,100 +13,77 @@ from pathlib import Path
class DotDict(dict):
def __init__(self, value=None, **kwargs):
super().__init__()
self.__setattr__ = self.__setitem__
if type(value) in [str, bytes]:
self.fromJson(value)
if isinstance(value, (str, bytes)):
self.from_json(value)
elif type(value) in [dict, DotDict]:
elif isinstance(value, dict):
self.update(value)
elif value:
raise TypeError('The value must be a JSON string, dict, or another DotDict object, not', value.__class__)
raise TypeError(f'The value must be a JSON string, list, dict, or another DotDict object, not {value.__class__}')
if kwargs:
self.update(kwargs)
def __getattr__(self, key):
def __getattr__(self, k):
try:
val = super().__getattribute__(key)
return super().__getitem__(k)
except AttributeError:
val = self.get(key, KeyError)
if val == KeyError:
raise KeyError(f'Invalid key: {key}')
return DotDict(val) if type(val) == dict else val
except KeyError:
raise AttributeError(f'{self.__class__.__name__} object has no attribute {k}') from None
def __delattr__(self, key):
if self.get(key):
del self[key]
super().__delattr__(key)
def __setattr__(self, key, value):
if key.startswith('_'):
super().__setattr__(key, value)
else:
super().__setitem__(key, value)
def __parse_item__(self, k, v):
if type(v) == dict:
def __setitem__(self, k, v):
if isinstance(v, dict):
v = DotDict(v)
if not k.startswith('_'):
return (k, v)
super().__setitem__(k, v)
def get(self, key, default=None):
value = super().get(key, default)
return DotDict(value) if type(value) == dict else value
def __delattr__(self, k):
try:
dict.__delitem__(self, k)
except KeyError:
raise AttributeError(f'{self.__class__.__name__} object has no attribute {k}') from None
def items(self):
data = []
for k, v in super().items():
new = self.__parse_item__(k, v)
if new:
data.append(new)
return data
def copy(self):
return DotDict(self)
def values(self):
return list(super().values())
def update(self, data):
for k,v in data.items():
self.__setitem__(k, v)
def keys(self):
return list(super().keys())
def to_json(self, indent=None, **kwargs):
if 'cls' not in kwargs:
kwargs['cls'] = JsonEncoder
return json.dumps(dict(self), indent=indent, **kwargs)
def asDict(self):
return dict(self)
def toJson(self, indent=None, **kwargs):
data = {}
for k,v in self.items():
if type(k) in [DotDict, Path]:
k = str(k)
if type(v) in [DotDict, Path]:
v = str(v)
data[k] = v
return json.dumps(data, indent=indent, **kwargs)
def fromJson(self, string):
def from_json(self, string):
data = json.loads(string)
self.update(data)
def load_json(self, path):
with open(path) as fd:
self.update(json.load(fd))
def save_json(self, path, indent=None):
with open(path, 'w') as fp:
fp.write(self.to_json(indent=indent))
class JsonEncoder(json.JSONEncoder):
def default(self, obj):
if not any(map(isinstance, [obj], [str, int, float, dict])):
return str(obj)
return json.JSONEncoder.default(self, obj)

View file

@ -1,14 +1,15 @@
import os, shlex, sys, logging as logger
import json, os, shlex, sys, logging as logger
from configobj import ConfigObj
from pathlib import Path
from . import DotDict
args = ' '.join(sys.argv[1:])
defaults = {
'exec': 'insert command here' if not args else args,
'path': os.getcwd(),
'bin': None,
'args': [],
'env': {},
'path': Path.cwd(),
'watch_ext': ['py', 'env'],
'ignore_dirs': ['build', 'config', 'data'],
'ignore_files': ['reload.py', 'test.py'],
@ -16,73 +17,47 @@ defaults = {
}
configpath = Path.cwd().joinpath('reload.cfg')
configfile = ConfigObj(indent_type='\t')
configfile.filename = str(configpath)
def setup_config(configfile=None):
configfile = Path(configfile) if configfile else Path.cwd().joinpath('reload.json')
config = DotDict(**defaults)
if not configpath.exists():
msg = 'Creating config file and exiting...' if not args else 'Creating config file'
print('Cannot find "reload.cfg" in working directory. ' + msg)
if not configfile.exists():
print('Cannot find "reload.json" in working directory. Creating config file and exiting...')
for setting in defaults:
configfile[setting] = defaults[setting]
new_config = defaults.copy()
new_config['path'] = str(config.path)
try:
configfile.write()
except PermissionError:
print('Failed to write config file because of permission error:', configpath)
try:
config.save_json(configfile, indent=4)
except PermissionError:
print('Failed to write config file because of permission error')
if not args:
sys.exit()
configfile.reload()
config = DotDict()
config.load_json(configfile)
config.path = Path(config.path or os.getcwd())
config.log_level = getattr(logger, config.log_level.upper())
config.ignore_dirs = [s.strip('/') for s in config.ignore_dirs]
config.bin = Path(config.get('bin')).expanduser()
if not config.bin:
print('You forgot to specify a command')
sys.exit()
for k,v in defaults.items():
value = configfile.get(k)
key = k
if not config.path.exists():
print('Watch path doesn\'t exist:', config.path)
sys.exit()
if value == None:
if key == 'exec':
print('You forgot to specify a command')
sys.exit()
console.level = config.log_level
if key == 'watch_ext':
print('You forgot to specify a file extension to watch')
sys.exit()
if key == 'path':
value = os.getcwd()
if key == 'exec':
value = shlex.split(value if not args else args)
elif k == 'path':
value = Path(value).resolve()
elif type(v) != list and key != 'log_level':
value = [s.strip() for s in value.split(',')]
elif k == 'ignore_dirs':
value = [s.strip('/') for s in value]
config[key] = value
if not config.path.exists():
print('Watch path doesn\'t exist:', config.path)
sys.exit()
#if len(sys.argv) > 1:
#command += sys.argv[1:]
return config
logging = logger.getLogger('reloader')
logging.setLevel(logger.DEBUG)
console = logger.StreamHandler()
console.name = 'Console Log'
console.level = eval(f'logger.{config.log_level}')
console.level = logger.INFO
console.formatter = logger.Formatter('%(asctime)s WATCHER: %(message)s', '%H:%M:%S')
logging.addHandler(console)

View file

@ -3,107 +3,115 @@ import os, random, subprocess, sys, time
from datetime import datetime
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from watchdog.events import PatternMatchingEventHandler
from .config import config, logging
from .config import setup_config, logging
proc = None
last_restart = None
config = setup_config()
def not_in(var, string):
return string not in var
class WatchHandler(PatternMatchingEventHandler):
patterns = [f'*.{ext}' for ext in config.watch_ext]
def killProc(proc):
if proc.poll() != None:
return
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
logging.info(f'Terminating process {proc.pid}')
proc.terminate()
sec = 0
while proc.poll() == None:
time.sleep(1)
sec += 1
if sec >= 5:
logging.error('Failed to terminate. Killing process...')
proc.kill()
break
logging.info('Process terminated')
self.proc = None
self.last_restart = None
def run(restart=False):
global proc
global last_restart
timestamp = datetime.timestamp(datetime.now())
last_restart = timestamp if not last_restart else last_restart
if restart == True and proc.pid != '':
if timestamp - 1 < last_restart:
logging.debug('Process restarted less than 1 sec ago')
def kill_proc(self):
if self.proc.poll() != None:
return
killProc(proc)
logging.info(f'Terminating process {self.proc.pid}')
self.proc.terminate()
sec = 0
try:
proc = subprocess.Popen(config.exec, preexec_fn=os.setpgrp)
except FileNotFoundError:
logging.error(f'Cannot find executable:', config.exec.join(' '))
sys.exit()
while self.proc.poll() == None:
time.sleep(1)
sec += 1
last_restart = timestamp
logging.info(f'Started process with PID {proc.pid}')
if sec >= 5:
logging.error('Failed to terminate. Killing process...')
self.proc.kill()
break
logging.info('Process terminated')
def run_proc(self, restart=False):
timestamp = datetime.timestamp(datetime.now())
self.last_restart = timestamp if not self.last_restart else 0
if restart == True and self.proc.pid != '':
if timestamp - 3 < self.last_restart:
logging.debug('Process restarted less than 1 sec ago')
return
self.kill_proc()
cmd = [config.bin]
if config.env:
env = ['env']
for k,v in config.env.items():
env.append(f'{k}={v}')
cmd = [*env, *cmd]
try:
self.proc = subprocess.Popen([*cmd, *config.args], preexec_fn=os.setpgrp, stdin=subprocess.PIPE)
except FileNotFoundError:
logging.error(f'Cannot find executable: {config.bin}')
os.exit()
self.last_restart = timestamp
logging.info(f'Started process with PID {self.proc.pid}')
class MyHandler(FileSystemEventHandler):
def on_any_event(self, event):
if event.event_type not in ['modified', 'created', 'deleted']:
logging.debug(f'Ignored event: {event.event_type}')
return
path = Path(event.src_path)
name = path.name
directory = str(path.parent).replace(str(config.path), '')
ext = path.suffix.strip('.')
if ext or any(map(not_in, [directory, name], ['__pycache__'])):
logging.debug(f'{event.event_type}, {directory}, {name}, {ext}')
if event.event_type in ['modified', 'created'] and ext in config.watch_ext:
for direc in config.ignore_dirs:
if f'/{direc}/' in directory:
logging.debug(f'Ignored directory: {directory}')
return
if not set(config.ignore_dirs).intersection(directory.split('/')):
for filename in config.ignore_files:
if name == filename:
if path.name == filename:
logging.debug(f'Ignored file name: {filename}')
return
run(restart=True)
self.run_proc(restart=True)
def main():
logging.info('Starting process watcher')
logging.debug(f'Watch dir: {config.path}')
run()
handler = WatchHandler()
handler.run_proc()
observer = Observer()
observer.schedule(MyHandler(), str(config.path), recursive=True)
observer.start()
watcher = Observer()
watcher.schedule(handler, str(config.path), recursive=True)
watcher.start()
timeout = None
timer = 0
try:
while True:
time.sleep(random.randint(60 * 30, 60 * 60))
logging.info('*notices your running process* OwO what\'s this?')
for line in sys.stdin:
proc.stdin.write(line.encode('UTF-8'))
proc.stdin.flush()
except KeyboardInterrupt:
pass
killProc(proc)
print()
handler.kill_proc()
logging.debug('Stopping process watcher')
observer.stop()
observer.join()
watcher.stop()
watcher.join()

View file

@ -1,2 +1,2 @@
configobj>=5.0.6
watchdog>=0.8.3
watchdog>=2.1.3