366 lines
12 KiB
Python
366 lines
12 KiB
Python
# config.py - deals with loading configuration information.
|
|
|
|
# Loads config data from a .cfg file. Also caches the compiled
|
|
# data back into a .cfc file.
|
|
|
|
# If you are wondering how to avoid needing .cfg files (eg,
|
|
# if you are freezing Pythonwin etc) I suggest you create a
|
|
# .py file, and put the config info in a docstring. Then
|
|
# pass a CStringIO file (rather than a filename) to the
|
|
# config manager.
|
|
import glob
|
|
import importlib.util
|
|
import marshal
|
|
import os
|
|
import stat
|
|
import sys
|
|
import traceback
|
|
import types
|
|
|
|
import pywin
|
|
import win32api
|
|
|
|
from . import keycodes
|
|
|
|
debugging = 0
|
|
if debugging:
|
|
import win32traceutil # Some trace statements fire before the interactive window is open.
|
|
|
|
def trace(*args):
|
|
sys.stderr.write(" ".join(map(str, args)) + "\n")
|
|
|
|
else:
|
|
trace = lambda *args: None
|
|
|
|
compiled_config_version = 3
|
|
|
|
|
|
def split_line(line, lineno):
|
|
comment_pos = line.find("#")
|
|
if comment_pos >= 0:
|
|
line = line[:comment_pos]
|
|
sep_pos = line.rfind("=")
|
|
if sep_pos == -1:
|
|
if line.strip():
|
|
print("Warning: Line %d: %s is an invalid entry" % (lineno, repr(line)))
|
|
return None, None
|
|
return "", ""
|
|
return line[:sep_pos].strip(), line[sep_pos + 1 :].strip()
|
|
|
|
|
|
def get_section_header(line):
|
|
# Returns the section if the line is a section header, else None
|
|
if line[0] == "[":
|
|
end = line.find("]")
|
|
if end == -1:
|
|
end = len(line)
|
|
rc = line[1:end].lower()
|
|
try:
|
|
i = rc.index(":")
|
|
return rc[:i], rc[i + 1 :]
|
|
except ValueError:
|
|
return rc, ""
|
|
return None, None
|
|
|
|
|
|
def find_config_file(f):
|
|
return os.path.join(pywin.__path__[0], f + ".cfg")
|
|
|
|
|
|
def find_config_files():
|
|
return [
|
|
os.path.split(x)[1]
|
|
for x in [
|
|
os.path.splitext(x)[0]
|
|
for x in glob.glob(os.path.join(pywin.__path__[0], "*.cfg"))
|
|
]
|
|
]
|
|
|
|
|
|
class ConfigManager:
|
|
def __init__(self, f):
|
|
self.filename = "unknown"
|
|
self.last_error = None
|
|
self.key_to_events = {}
|
|
b_close = False
|
|
if hasattr(f, "readline"):
|
|
fp = f
|
|
self.filename = "<config string>"
|
|
compiled_name = None
|
|
else:
|
|
try:
|
|
f = find_config_file(f)
|
|
src_stat = os.stat(f)
|
|
except os.error:
|
|
self.report_error("Config file '%s' not found" % f)
|
|
return
|
|
self.filename = f
|
|
self.basename = os.path.basename(f)
|
|
trace("Loading configuration", self.basename)
|
|
compiled_name = os.path.splitext(f)[0] + ".cfc"
|
|
try:
|
|
cf = open(compiled_name, "rb")
|
|
try:
|
|
ver = marshal.load(cf)
|
|
ok = compiled_config_version == ver
|
|
if ok:
|
|
kblayoutname = marshal.load(cf)
|
|
magic = marshal.load(cf)
|
|
size = marshal.load(cf)
|
|
mtime = marshal.load(cf)
|
|
if (
|
|
magic == importlib.util.MAGIC_NUMBER
|
|
and win32api.GetKeyboardLayoutName() == kblayoutname
|
|
and src_stat[stat.ST_MTIME] == mtime
|
|
and src_stat[stat.ST_SIZE] == size
|
|
):
|
|
self.cache = marshal.load(cf)
|
|
trace("Configuration loaded cached", compiled_name)
|
|
return # We are ready to roll!
|
|
finally:
|
|
cf.close()
|
|
except (os.error, IOError, EOFError):
|
|
pass
|
|
fp = open(f)
|
|
b_close = True
|
|
self.cache = {}
|
|
lineno = 1
|
|
line = fp.readline()
|
|
while line:
|
|
# Skip to the next section (maybe already there!)
|
|
section, subsection = get_section_header(line)
|
|
while line and section is None:
|
|
line = fp.readline()
|
|
if not line:
|
|
break
|
|
lineno = lineno + 1
|
|
section, subsection = get_section_header(line)
|
|
if not line:
|
|
break
|
|
|
|
if section == "keys":
|
|
line, lineno = self._load_keys(subsection, fp, lineno)
|
|
elif section == "extensions":
|
|
line, lineno = self._load_extensions(subsection, fp, lineno)
|
|
elif section == "idle extensions":
|
|
line, lineno = self._load_idle_extensions(subsection, fp, lineno)
|
|
elif section == "general":
|
|
line, lineno = self._load_general(subsection, fp, lineno)
|
|
else:
|
|
self.report_error(
|
|
"Unrecognised section header '%s:%s'" % (section, subsection)
|
|
)
|
|
line = fp.readline()
|
|
lineno = lineno + 1
|
|
if b_close:
|
|
fp.close()
|
|
# Check critical data.
|
|
if not self.cache.get("keys"):
|
|
self.report_error("No keyboard definitions were loaded")
|
|
if not self.last_error and compiled_name:
|
|
try:
|
|
cf = open(compiled_name, "wb")
|
|
marshal.dump(compiled_config_version, cf)
|
|
marshal.dump(win32api.GetKeyboardLayoutName(), cf)
|
|
marshal.dump(importlib.util.MAGIC_NUMBER, cf)
|
|
marshal.dump(src_stat[stat.ST_SIZE], cf)
|
|
marshal.dump(src_stat[stat.ST_MTIME], cf)
|
|
marshal.dump(self.cache, cf)
|
|
cf.close()
|
|
except (IOError, EOFError):
|
|
pass # Ignore errors - may be read only.
|
|
|
|
def configure(self, editor, subsections=None):
|
|
# Execute the extension code, and find any events.
|
|
# First, we "recursively" connect any we are based on.
|
|
if subsections is None:
|
|
subsections = []
|
|
subsections = [""] + subsections
|
|
general = self.get_data("general")
|
|
if general:
|
|
parents = general.get("based on", [])
|
|
for parent in parents:
|
|
trace("Configuration based on", parent, "- loading.")
|
|
parent = self.__class__(parent)
|
|
parent.configure(editor, subsections)
|
|
if parent.last_error:
|
|
self.report_error(parent.last_error)
|
|
|
|
bindings = editor.bindings
|
|
codeob = self.get_data("extension code")
|
|
if codeob is not None:
|
|
ns = {}
|
|
try:
|
|
exec(codeob, ns)
|
|
except:
|
|
traceback.print_exc()
|
|
self.report_error("Executing extension code failed")
|
|
ns = None
|
|
if ns:
|
|
num = 0
|
|
for name, func in list(ns.items()):
|
|
if type(func) == types.FunctionType and name[:1] != "_":
|
|
bindings.bind(name, func)
|
|
num = num + 1
|
|
trace("Configuration Extension code loaded", num, "events")
|
|
# Load the idle extensions
|
|
for subsection in subsections:
|
|
for ext in self.get_data("idle extensions", {}).get(subsection, []):
|
|
try:
|
|
editor.idle.IDLEExtension(ext)
|
|
trace("Loaded IDLE extension", ext)
|
|
except:
|
|
self.report_error("Can not load the IDLE extension '%s'" % ext)
|
|
|
|
# Now bind up the key-map (remembering a reverse map
|
|
subsection_keymap = self.get_data("keys")
|
|
num_bound = 0
|
|
for subsection in subsections:
|
|
keymap = subsection_keymap.get(subsection, {})
|
|
bindings.update_keymap(keymap)
|
|
num_bound = num_bound + len(keymap)
|
|
trace("Configuration bound", num_bound, "keys")
|
|
|
|
def get_key_binding(self, event, subsections=None):
|
|
if subsections is None:
|
|
subsections = []
|
|
subsections = [""] + subsections
|
|
|
|
subsection_keymap = self.get_data("keys")
|
|
for subsection in subsections:
|
|
map = self.key_to_events.get(subsection)
|
|
if map is None: # Build it
|
|
map = {}
|
|
keymap = subsection_keymap.get(subsection, {})
|
|
for key_info, map_event in list(keymap.items()):
|
|
map[map_event] = key_info
|
|
self.key_to_events[subsection] = map
|
|
|
|
info = map.get(event)
|
|
if info is not None:
|
|
return keycodes.make_key_name(info[0], info[1])
|
|
return None
|
|
|
|
def report_error(self, msg):
|
|
self.last_error = msg
|
|
print("Error in %s: %s" % (self.filename, msg))
|
|
|
|
def report_warning(self, msg):
|
|
print("Warning in %s: %s" % (self.filename, msg))
|
|
|
|
def _readline(self, fp, lineno, bStripComments=1):
|
|
line = fp.readline()
|
|
lineno = lineno + 1
|
|
if line:
|
|
bBreak = (
|
|
get_section_header(line)[0] is not None
|
|
) # A new section is starting
|
|
if bStripComments and not bBreak:
|
|
pos = line.find("#")
|
|
if pos >= 0:
|
|
line = line[:pos] + "\n"
|
|
else:
|
|
bBreak = 1
|
|
return line, lineno, bBreak
|
|
|
|
def get_data(self, name, default=None):
|
|
return self.cache.get(name, default)
|
|
|
|
def _save_data(self, name, data):
|
|
self.cache[name] = data
|
|
return data
|
|
|
|
def _load_general(self, sub_section, fp, lineno):
|
|
map = {}
|
|
while 1:
|
|
line, lineno, bBreak = self._readline(fp, lineno)
|
|
if bBreak:
|
|
break
|
|
|
|
key, val = split_line(line, lineno)
|
|
if not key:
|
|
continue
|
|
key = key.lower()
|
|
l = map.get(key, [])
|
|
l.append(val)
|
|
map[key] = l
|
|
self._save_data("general", map)
|
|
return line, lineno
|
|
|
|
def _load_keys(self, sub_section, fp, lineno):
|
|
# Builds a nested dictionary of
|
|
# (scancode, flags) = event_name
|
|
main_map = self.get_data("keys", {})
|
|
map = main_map.get(sub_section, {})
|
|
while 1:
|
|
line, lineno, bBreak = self._readline(fp, lineno)
|
|
if bBreak:
|
|
break
|
|
|
|
key, event = split_line(line, lineno)
|
|
if not event:
|
|
continue
|
|
sc, flag = keycodes.parse_key_name(key)
|
|
if sc is None:
|
|
self.report_warning("Line %d: Invalid key name '%s'" % (lineno, key))
|
|
else:
|
|
map[sc, flag] = event
|
|
main_map[sub_section] = map
|
|
self._save_data("keys", main_map)
|
|
return line, lineno
|
|
|
|
def _load_extensions(self, sub_section, fp, lineno):
|
|
start_lineno = lineno
|
|
lines = []
|
|
while 1:
|
|
line, lineno, bBreak = self._readline(fp, lineno, 0)
|
|
if bBreak:
|
|
break
|
|
lines.append(line)
|
|
try:
|
|
c = compile(
|
|
"\n" * start_lineno + "".join(lines), # produces correct tracebacks
|
|
self.filename,
|
|
"exec",
|
|
)
|
|
self._save_data("extension code", c)
|
|
except SyntaxError as details:
|
|
errlineno = details.lineno + start_lineno
|
|
# Should handle syntax errors better here, and offset the lineno.
|
|
self.report_error(
|
|
"Compiling extension code failed:\r\nFile: %s\r\nLine %d\r\n%s"
|
|
% (details.filename, errlineno, details.msg)
|
|
)
|
|
return line, lineno
|
|
|
|
def _load_idle_extensions(self, sub_section, fp, lineno):
|
|
extension_map = self.get_data("idle extensions")
|
|
if extension_map is None:
|
|
extension_map = {}
|
|
extensions = []
|
|
while 1:
|
|
line, lineno, bBreak = self._readline(fp, lineno)
|
|
if bBreak:
|
|
break
|
|
line = line.strip()
|
|
if line:
|
|
extensions.append(line)
|
|
extension_map[sub_section] = extensions
|
|
self._save_data("idle extensions", extension_map)
|
|
return line, lineno
|
|
|
|
|
|
def test():
|
|
import time
|
|
|
|
start = time.clock()
|
|
f = "default"
|
|
cm = ConfigManager(f)
|
|
map = cm.get_data("keys")
|
|
took = time.clock() - start
|
|
print("Loaded %s items in %.4f secs" % (len(map), took))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test()
|