def get(self, *path: label.LabelInput) -> Nullable[Any]: ''' Regularizes `path` to a dotted list: Returns value in main document under (converted) `path`. - Returns Null() if `path` does not exist. ''' path = label.regularize(path) # Find out if there's anything at the end of the path. place = self._main() for key in path: # Check so we don't raise KeyError. if key not in place: return Null() # Update our place in the path and continue on to the next key. place = place[key] # We got here, so the path is either only a partial path, or we found a # leaf. We don't check for the difference right now, only return what # we ended up with. # # TODO: Figure out a way to return Null() if we're only a partial path? # - Does DataDict have recursive DataDicts for sub-entries or # something? return place
def exists(self, path: label.LabelInput) -> bool: ''' If `path` is a str: - Expects dotted string - converts to a list using `label.regularize()`. Else, uses list provided. Then checks for a value in the main document at the end of the (converted) `path`. Also checks for `check` in main document under 'alias' if that exists. ''' # First, check under our primary key. if super().exists(label.regularize(self._key_prime, path)): return True # Second, try the alias key, if it exists. if self.ALIAS in self: alias_exists = super().exists(label.regularize(self.ALIAS, path)) return alias_exists
def register(cls_or_func: RegisterType, dotted: Optional[label.LabelInput] = None, unit_test_only: Optional[bool] = False) -> None: ''' Register the `cls_or_func` with the `dotted` string to our registry. If `unit_test_only` is Truthy, the `cls_or_func` will be registered if we are running a unit test, or handed off to `ignore()` if we are not. ''' log_dotted = label.normalize(_DOTTED, 'register') # --- # Sanity # --- if not dotted: # Check for class's dotted. try: dotted = cls_or_func.dotted except AttributeError: pass # No dotted string is an error. if not dotted: msg = ("Config sub-classes must either have a `dotted` " "class attribute or be registered with a `dotted` " "argument.") error = ValueError(msg, cls_or_func, dotted) log.registration(log_dotted, msg + "Got '{}' for {}.", dotted, cls_or_func) raise log.exception(error, msg) # --- # Unit Testing? # --- # Unit-test registrees should continue on if in unit-testing mode, # or be diverted to ignore if not. if unit_test_only and not background.testing.get_unit_testing(): ignore(cls_or_func) return # --- # Register # --- # Registry should check if it is ignored already by someone previous, # if it cares. dotted_str = label.normalize(dotted) log.registration(log_dotted, "{}: Registering '{}' to '{}'...", config.klass, dotted_str, cls_or_func.__name__) dotted_args = label.regularize(dotted) config.register(cls_or_func, *dotted_args) log.registration(log_dotted, "{}: Registered '{}' to '{}'.", config.klass, dotted_str, cls_or_func.__name__)
def get_by_dotted(self, dotted: label.LabelInput, context: Optional[VerediContext]) -> 'RegisterType': ''' Get by dotted name. Returns a registered class/func from the dot-separated keys (e.g. "repository.player.file-tree"). Context just used for errors/exceptions. Raises: KeyError - dotted string not found in our registry. ''' registration = self._registry split_keys = label.regularize(dotted) # --- # Walk into our registry using the keys for our path. # --- i = 0 for key in split_keys: if registration is None: break # This can throw the KeyError... try: registration = registration[key] except KeyError as error: raise log.exception( RegistryError, "Registry has nothing at: {} (full path: {})", split_keys[:i + 1], split_keys, context=context) from error i += 1 # --- # Sanity Check - ended at leaf node? # --- if isinstance(registration, dict): raise log.exception(RegistryError, "Registry for '{}' is not at a leaf - " "still has entries to go: {}", label.normalize(dotted), registration, context=context) # Good; return the leaf value (a RegisterType). return registration
def create_from_config( self, *keychain: label.LabelInput, context: Optional['VerediContext'] = None, ) -> Nullable[Any]: ''' Gets value from these keychain in our config data, then tries to have our registry create that value. e.g. config.create_from_config('data', 'game', 'repository') -> from config file: 'veredi.repository.file-tree' -> from create_from_label('veredi.repository.file-tree', ...) -> FileTreeRepository object Will use provided context, or create a ConfigContext to use via `make_config_context()` if none provided. Returns thing created using keychain or None. ''' # Ensure the keychain is in good shape from whatever was passed in. keychain = label.regularize(*keychain) config_val = self.get(*keychain) if not isinstance(config_val, str): error_info = ("no config value" if not config_val else "incorrect config value of type " f"'{type(config_val)}' (need str)") log.debug( "Make requested for: {}. But we have {} " "for that. context: {}", error_info, keychain, context) return Null() if not context: context = self.make_config_context() context.add(ConfigLink.KEYCHAIN, list(keychain[:-1])) log.debug("Make requested for: {}. context: {}", keychain, context) # Assume their relevant data is one key higher up... # e.g. if we're making the thing under keychain (GAME, REPO, TYPE), # then the repository we're making will want (GAME, REPO) as its # root so it can get, say, DIRECTORY. ret_val = self.create_from_label(config_val, context=context) log.debug("Made: {} from {}. context: {}", ret_val, keychain, context) return ret_val
def ut_inject(self, value: Any, doc_type: Document, *keychain: label.LabelInput) -> None: # Ensure the keychain is in good shape from whatever was passed in. keychain = label.regularize(*keychain) # Get document type data first. doc_data = self._config.get(doc_type, None) data = doc_data if data is None: log.debug("No doc_type {} in our config data {}.", doc_type, self._config) return None # Now hunt for/create the keychain they wanted... for key in keychain[:-1]: data = data.setdefault(key, {}) # And set the key. data[keychain[-1]] = value
def _query_value(self, component: Component, entry: Union[str, Tuple[str, str]]) -> ValueMilieu: ''' `entry` string must be canonicalized. We'll get it from the component. Returns component query result. Also returns the canonicalized `entry` str, in case you need to call back into here for e.g.: _query_value(component, 'str.mod') -> '(${this.score} - 10) // 2', 'strength.modifier' _query_value(component, ('this.score', 'strength.modifier')) -> (20, 'strength.score') ''' if isinstance(entry, tuple): return self._query_this(component, *entry) entry = self._rule_defs.canonical(entry, None) return self._query_split(component, *label.regularize(entry))
def get_data(self, *keychain: label.LabelInput) -> Nullable[Any]: ''' Get a configuration thingy from us given some keychain use to walk into our config data in 'data' entry. Returns data found at end keychain. Returns None if couldn't find a key in our config data. ''' # Ensure the keychain is in good shape from whatever was passed in. keychain = label.regularize(*keychain) data = self.get('data', *keychain) log.data_processing(self.dotted, 'get_data: keychain: {} -> data: {}', keychain, data, log_minimum=log.Level.DEBUG) return data
def query(self, *dot_path: label.LabelInput) -> Nullable[Any]: ''' Query this component's data for something on either: - a dotted string path. - (dotted) string args. That is either: query('foo.bar') query('foo', 'bar') E.g. for an ability component with data: { 'ability': { 'strength': { 'score': 10, 'modifier': 'some math string', }, ... } } |------------------------+--------------------| | Query | Result | |------------------------+--------------------| | 'strength.modifier' | 'some math string' | | 'strength.score' | 10 | | 'strength' | 10 | | 'strength', 'modifier' | 'some math string' | | 'strength', 'score' | 10 | |------------------------+--------------------| ''' # Get our input sorted out. dot_path = label.regularize(*dot_path) data = self.persistent for each in dot_path: data = data.get(each, Null()) return data
class ConfigRegistration(enum.Enum): ''' Configuration settings keys for registration. ''' KEY = 'registration' ''' Registration is a list of entries for what to search for registration. ''' NAME = label.regularize('register.name') ''' Who is being registered. ''' DOTTED = label.regularize('register.dotted') ''' Who is being registered. ''' PATH_ROOT = label.regularize('path.root') ''' Path to resolve to get to the root of the file tree to be search for registration files. ''' PATH_REGISTRARS_RUN = label.regularize('path.registrars.run') ''' A list of filenames to look for when running (normal and testing). ''' PATH_REGISTRARS_TEST = label.regularize('path.registrars.test') ''' A list of filenames to look for when in testing mode. ''' PATH_REGISTREES_RUN = label.regularize('path.registrees.run') ''' A list of filenames to look for when running (normal and testing). ''' PATH_REGISTREES_TEST = label.regularize('path.registrees.test') ''' A list of filenames to look for when in testing mode. ''' PATH_IGNORE_FILES = label.regularize('path.ignore.files') ''' A list of strings and regexs of files to ignore during path searches. ''' PATH_IGNORE_DIRS = label.regularize('path.ignore.directories') ''' A list of strings and regexs of directories to ignore during path searches. ''' FORCE_TEST = label.regularize('unit-test') ''' A flag to force registration of unit-testing (or force skipping of it). Overrides auto-detection of unit-testing that registration does. ''' # ------------------------------ # Helpers # ------------------------------ def full_key(self) -> str: ''' Adds root key ('registration') to its value to form a full key (e.g. 'registration.path.ignores'). ''' return label.normalize(self.KEY.value, self.value) @classmethod def _get(klass: Type['ConfigRegistration'], path: 'ConfigRegistration', entry: Dict[str, Any]) -> Union[str, re.Pattern]: ''' Get value at end of `path` keys in `entry`. ''' if not path or not path.value: return Null() for node in path.value: entry = entry.get(node, Null()) return entry @classmethod def name(klass: Type['ConfigRegistration'], entry: Dict[str, Any]) -> str: ''' Returns the NAME entry of this registration entry. ''' value = klass._get(klass.NAME, entry) return value @classmethod def dotted(klass: Type['ConfigRegistration'], entry: Dict[str, Any]) -> label.DotStr: ''' Returns the DOTTED entry of this registration entry. ''' value = klass._get(klass.DOTTED, entry) return label.normalize(value) @classmethod def path_root(klass: Type['ConfigRegistration'], entry: Dict[str, Any], config: 'Configuration') -> Nullable[paths.Path]: ''' Returns the PATH_ROOT entry of this registration entry. PATH_ROOT is the resolved (absolute) path to the root of the file tree we should search for registration. ''' field = klass._get(klass.PATH_ROOT, entry) path = config.path(field) # Clean up the path if we found it. if path: if not path.is_absolute(): path = paths.cast(os.getcwd()) / path path = path.resolve() return path @classmethod def path_run(klass: Type['ConfigRegistration'], entry: Dict[str, Any], registrars: bool) -> Nullable[str]: ''' Returns either PATH_REGISTREES_RUN or PATH_REGISTRARS_RUN, depending on `registrars` bool. - True: PATH_REGISTRARS_RUN - False: PATH_REGISTREES_RUN This key's value should be a list of filenames to look for. ''' if registrars: return klass._get(klass.PATH_REGISTRARS_RUN, entry) return klass._get(klass.PATH_REGISTREES_RUN, entry) @classmethod def path_test(klass: Type['ConfigRegistration'], entry: Dict[str, Any], registrars: bool) -> Nullable[str]: ''' Returns either PATH_REGISTREES_TEST or PATH_REGISTRARS_TEST, depending on `registrars` bool. - True: PATH_REGISTRARS_TEST - False: PATH_REGISTREES_TEST This key's value should be a list of filenames to look for. ''' if registrars: return klass._get(klass.PATH_REGISTRARS_TEST, entry) return klass._get(klass.PATH_REGISTREES_TEST, entry) @classmethod def path_ignore_files(klass: Type['ConfigRegistration'], entry: Dict[str, Any]) -> Nullable[str]: ''' Returns the PATH_IGNORE_FILES entry of this registration entry. PATH_IGNORE_FILES should be a list of strings and regexes to match while checking file names. A matching file will be ignored. ''' return klass._get(klass.PATH_IGNORE_FILES, entry) @classmethod def path_ignore_dirs(klass: Type['ConfigRegistration'], entry: Dict[str, Any]) -> Nullable[str]: ''' Returns the PATH_IGNORE_DIRS entry of this registration entry. PATH_IGNORE_DIRS should be a list of strings and regexes to match while checking directory names. A matching dir will be ignored. ''' return klass._get(klass.PATH_IGNORE_DIRS, entry) @classmethod def force_test(klass: Type['ConfigRegistration'], entry: Dict[str, Any]) -> Nullable[bool]: ''' Returns the FORCE_TEST entry of this registration entry. FORCE_TEST, if it exists, should be true to force registration of testing classes or false to force skipping test class registration. Generally, not supplying is best choice - it will be auto-detected. ''' return klass._get(klass.FORCE_TEST, entry)
def register(klass: Type['Encodable'], dotted: Optional[label.LabelInput] = None, unit_test_only: Optional[bool] = False) -> None: ''' Register the `klass` with the `dotted` string to our registry. If `unit_test_only` is Truthy, the `klass` will be registered if we are running a unit test, or handed off to `ignore()` if we are not. ''' log_dotted = label.normalize(_DOTTED, 'register') # --- # Sanity # --- if not dotted: # Check for class's dotted. try: dotted = klass.dotted except AttributeError: pass # No dotted string is an error. if not dotted: msg = ("Encodable sub-classes must either have a `dotted` " "class attribute or be registered with a `dotted` " "argument.") error = ValueError(msg, klass, dotted) log.registration(log_dotted, msg + "Got '{}' for {}.", dotted, klass) raise log.exception(error, msg) if enum.needs_wrapped(klass): msg = ("Enum sub-classes must be wrapped in an EnumWrap for " "Encodable functionality. Call `register_enum()` " "instead of `register()`.") error = TypeError(msg, klass, dotted) log.registration(log_dotted, msg) raise log.exception(error, msg, data={ 'klass': klass, 'dotted': label.normalize(dotted), 'unit_test_only': unit_test_only, }) # --- # Unit Testing? # --- # Unit-test registrees should continue on if in unit-testing mode, # or be diverted to ignore if not. if unit_test_only and not background.testing.get_unit_testing(): ignore(klass) return # --- # Register # --- # Registry should check if it is ignored already by someone previous, # if it cares. dotted_str = label.normalize(dotted) log.registration(log_dotted, "{}: Registering '{}' to '{}'...", codec.klass, dotted_str, klass.klass) dotted_args = label.regularize(dotted) codec.register(klass, *dotted_args) log.registration(log_dotted, "{}: Registered '{}' to '{}'.", codec.klass, dotted_str, klass.klass)
def get_by_doc(self, doc_type: Document, *keychain: label.LabelInput) -> Nullable[Any]: ''' Get value of `keychain` from `doc_type`. Raises a ConfigError if invalid `doc_type` supplied. Returns Null() if `doc_type` doesn't exist or `keychain` isn't in it. ''' # Ensure the keychain is in good shape from whatever was passed in. keychain = label.regularize(*keychain) log.data_processing(self.dotted, 'get_by_doc: Getting doc: {}, keychain: {}...', doc_type, keychain, log_minimum=log.Level.DEBUG) hierarchy = Document.hierarchy(doc_type) if not hierarchy.valid(*keychain): log.data_processing(self.dotted, "get_by_doc: invalid document hierarchy for " "doc: {}, keychain: {}...", doc_type, keychain, log_minimum=log.Level.DEBUG) raise log.exception( ConfigError, "Invalid keychain '{}' for {} document type. See its " "Hierarchy class for proper layout.", keychain, doc_type) # Get document type data first. doc_data = self._config.get(doc_type, None) data = doc_data if data is None: log.data_processing(self.dotted, "get_by_doc: No document type '{}' in " "our config data: {}", doc_type, self._config, log_minimum=log.Level.DEBUG, log_success=False) return Null() # Now hunt for the keychain they wanted... for key in keychain: data = data.get(key, None) if data is None: log.data_processing(self.dotted, "get_by_doc: No data for key '{}' in " "keychain {} in our config " "document data: {}", key, keychain, doc_data, log_minimum=log.Level.DEBUG, log_success=False) return Null() log.data_processing(self.dotted, "get_by_doc: Got data for {} in " "keychain {}. Data: {}", doc_type, keychain, data, log_minimum=log.Level.DEBUG, log_success=True) return data
def add(self, cls_or_func: 'RegisterType', *dotted_label: label.LabelInput) -> None: ''' This function does the actual registration. ''' # Ignored? if self.ignored(cls_or_func): msg = (f"{cls_or_func} is in our set of ignored " "classes/functions that should not be registered.") error = RegistryError(msg, data={ 'registree': cls_or_func, 'dotted': label.normalize(dotted_label), 'ignored': self._ignore, }) raise log.exception(error, msg) # Do any initial steps. dotted_list = label.regularize(*dotted_label) if not self._init_register(cls_or_func, dotted_list): # Totally ignore if not successful. _init_register() should do # all the erroring itself. return # Pull final key off of list so we don't make too many # dictionaries. name = str(cls_or_func) try: # Final key where the registration will actually be stored. leaf_key = dotted_list[-1] except IndexError as error: kwargs = log.incr_stack_level(None) raise log.exception( RegistryError, "Need to know what to register this ({}) as. " "E.g. @register('jeff', 'geoff'). Got no dotted_list: {}", name, dotted_list, **kwargs) from error # Our register - full info saved here. registry_our = self._registry # Background register - just names saved here. registry_bg = background.registry.registry(self.dotted) # ------------------------------ # Get reg dicts to the leaf. # ------------------------------ length = len(dotted_list) # -1 as we've got our config name already from that final # dotted_list entry. for i in range(length - 1): # Walk down into both dicts, making new empty sub-entries as # necessary. registry_our = registry_our.setdefault(dotted_list[i], {}) registry_bg = registry_bg.setdefault(dotted_list[i], {}) # ------------------------------ # Register (warn if occupied). # ------------------------------ # Helpful messages - but registering either way. try: if leaf_key in registry_our: if background.testing.get_unit_testing(): msg = ("Something was already registered under this " f"registry_our key... keys: {dotted_list}, " f"replacing {str(registry_our[leaf_key])}' with " f"this '{name}'.") error = KeyError(leaf_key, msg, cls_or_func) log.exception(error, None, msg, stacklevel=3) else: log.warning( "Something was already registered under this " "registry_our key... keys: {}, replacing " "'{}' with this '{}'", dotted_list, str(registry_our[leaf_key]), name, stacklevel=3) else: log.debug("Registered: keys: {}, value '{}'", dotted_list, name, stacklevel=3) except TypeError as error: msg = (f"{self.klass}.add(): Our " "'registry_our' dict is the incorrect type? Expected " "something that can deal with 'in' operator. Have: " f"{type(registry_our)} -> {registry_our}. Trying to " f"register {cls_or_func} at " f"'{label.normalize(dotted_list)}'. " "Registry: \n{}") from veredi.base.strings import pretty log.exception(error, msg, pretty.indented(self._registry)) # Reraise it. Just want more info. raise # Register cls/func to our registry, save some info to our # background registry. self._register(cls_or_func, dotted_list, leaf_key, registry_our, registry_bg) # ------------------------------ # Finalize (if desired). # ------------------------------ self._finalize_register(cls_or_func, dotted_list, registry_our, registry_bg)
def _search(self, place: Dict[str, Any], dotted: Optional[label.DotStr], data: EncodedEither, data_type: Type[Encodable]) -> Optional[Encodable]: ''' Provide `self._registry` as starting point of search. Searches the registry. If 'data_type' is supplied, will restrict search for registered Encodable to use to just that or subclasses. If `dotted` is provided, walks that keypath and returns whatever is registered to that. Otherwise, recursively walk our registry dict to find something that will claim() `data`. Returns registree or None. ''' # Set data_type to any Encodable if not supplied. if data_type is None: data_type = Encodable # --- # Provided with a dotted key. Use that for explicit search. # --- # More like 'get' than search... if dotted and label.is_dotstr(dotted): keys = label.regularize(dotted) # Path shouldn't be long. Just let Null-pattern pretend to be a # dict if we hit a 'Does Not Exist'. for key in keys: place = place.get(key, Null()) # Done, return either the registree or None. place = null_to_none(place) # Place must be: # - An Encodable. # - The same class or a subclass of data_type. if (not place or (type(place) != data_type and not issubclass(place, data_type))): # Don't return some random halfway point in the registry tree. place = None return place # --- # Search for a claimer... # --- for key in place: node = place[key] # If we got a sub-tree/branch, recurse into it. if isinstance(node, dict): result = self._search(node, dotted, data, data_type) # Did that find it? if result: # Yes; return decoded result. return result else: # No; done with this - go to next node. continue # If we got a leaf node, check it. if not issubclass(node, Encodable): self._log_warning( "Unexpected node in registry... expect either " f"strings or Encodables, got: {node}") continue # Do they claim this? claiming, _, _ = node.claim(data) # claiming, claim, reason = node.claim(data) if claiming: # Yes; return decoded result. return node # Else, not this; continue looking. # --- # Nothing found. # --- return None
def __init__(self, dotted: label.LabelInput) -> None: ''' Initialize the LabelTaxon with the provided dotted label. ''' super().__init__(Rank.Domain.DEFINITIONS, *label.regularize(dotted))