def save(self, ignore_readonly: bool = False) -> None: """Save the palette file into the specified location. If ignore_readonly is true, this will ignore the `readonly` property of the palette (allowing resaving those properties over old versions). Otherwise those palettes always create a new file. """ LOGGER.info('Saving "{}"!', self.name) props = Property.root( Property('Name', self.name), Property('TransName', self.trans_name), Property('Group', self.group), Property('ReadOnly', srctools.bool_as_int(self.readonly)), Property('UUID', self.uuid.hex), Property('Items', [ Property(item_id, str(subitem)) for item_id, subitem in self.pos ])) # If default, don't include in the palette file. # Remove the translated name, in case it's not going to write # properly to the file. if self.trans_name: props['Name'] = '' else: del props['TransName'] if self.settings is not None: self.settings.name = 'Settings' props.append(self.settings.copy()) # We need to write a new file, determine a valid path. # Use a hash to ensure it's a valid path (without '-' if negative) # If a conflict occurs, add ' ' and hash again to get a different # value. if self.filename is None or (self.readonly and not ignore_readonly): hash_src = self.name while True: hash_filename = str(abs(hash(hash_src))) + PAL_EXT if os.path.isfile(hash_filename): # Add a random character to iterate the hash. hash_src += chr(random.randrange(0x10ffff)) else: file = open(os.path.join(PAL_DIR, hash_filename), 'w', encoding='utf8') self.filename = os.path.join(PAL_DIR, hash_filename) break else: file = open(os.path.join(PAL_DIR, self.filename), 'w', encoding='utf8') with file: for line in props.export(): file.write(line)
def get_curr_settings(*, is_palette: bool) -> Property: """Return a property tree defining the current options.""" props = Property.root() for opt_id, opt_func in OPTION_SAVE.items(): # Skip if it opts out of being on the palette. if is_palette and not getattr(opt_func, 'to_palette', True): continue opt_prop = opt_func() opt_prop.name = opt_id.title() props.append(opt_prop) return props
def apply_replacements(conf: Property, item_id: str) -> Property: """Apply a set of replacement values to a config file, returning a new copy. The replacements are found in a 'Replacements' block in the property. These replace %values% starting and ending with percents. A double-percent allows literal percents. Unassigned values are an error. """ replace: dict[str, str] = {} new_conf = Property.root() if conf.is_root() else Property( conf.real_name, []) # Strip the replacement blocks from the config, and save the values. for prop in conf: if prop.name == 'replacements': for rep_prop in prop: replace[rep_prop.name.strip('%')] = rep_prop.value else: new_conf.append(prop) def rep_func(match: Match) -> str: """Does the replacement.""" var = match.group(1) if not var: # %% becomes %. return '%' try: return replace[var.casefold()] except KeyError: raise ValueError( f'Unresolved variable in "{item_id}": {var!r}\nValid vars: {replace}' ) for prop in new_conf.iter_tree(blocks=True): prop.name = RE_PERCENT_VAR.sub(rep_func, prop.real_name) if not prop.has_children(): prop.value = RE_PERCENT_VAR.sub(rep_func, prop.value) return new_conf
def concat_inner() -> Property: """Resolve then merge the configs.""" prop = Property.root() prop.extend(a()) prop.extend(b()) return prop
"""Implements callables which lazily parses and combines config files.""" from __future__ import annotations from typing import Callable, Pattern import functools from srctools import Property, logger, KeyValError from app import DEV_MODE import packages import utils LOGGER = logger.get_logger(__name__) LazyConf = Callable[[], Property] # Empty property. BLANK: LazyConf = lambda: Property.root() def raw_prop(block: Property, source: str= '') -> LazyConf: """Make an existing property conform to the interface.""" if block or block.name is not None: if source: def copier() -> Property: """Copy the config, then apply the source.""" copy = block.copy() packages.set_cond_source(copy, source) return copy return copier else: # We can just use the bound method. return block.copy else: # If empty, source is irrelevant, and we can use the constant. return BLANK