def add(data, target_path, value, fail_on_exists=True, update=False): # type errors can occur here ... # e.g. you try to go to a string if not [_ for _ in (list, tuple) if isinstance(target_path, _)]: raise TypeError(f'target_path is not a list or tuple! {type(target_path)}') target_prefixes = target_path[:-1] target_key = target_path[-1] target = data for target_name in target_prefixes: if target_name not in target: # TODO list indicies target[target_name] = {} target = target[target_name] if update: pass elif fail_on_exists and target_key in target: raise exc.TargetPathExistsError(f'A value already exists at path {target_path}\n' f'{lj(data)}') target[target_key] = value
def add(data, target_path, value, fail_on_exists=True, update=False): """ Note on semantics when target_path contains the type int. Normally when adding a path all the parents are added because we are expecting a direct path down. However, if the path contains int then it implicitly expects the list to alread exist. Therefore any failure on the way TO a list will immediately abort and not add the keys to the non-existent list. This is consistent with the approach where keys are not required but if their value is a list it must not be empty. Thus we abort so that we don't go around creating a bunch of empty lists that will show up later as errors when validating the schema. """ # type errors can occur here ... # e.g. you try to go to a string if not [_ for _ in (list, tuple) if isinstance(target_path, _)]: msg = f'target_path is not a list or tuple! {type(target_path)}' raise TypeError(msg) if False and target_path == ['@context', '@base']: # use to debug TargetPathExistsError issues if '@tracker' not in data: data['@tracker'] = [] try: raise BaseException('tracker') except BaseException as e: data['@tracker'].append(e) if '@context' in data and '@base' in data['@context']: log.critical(f'target present {data["id"]}') else: log.critical(f'target not present {data["id"]}') target_prefixes = target_path[:-1] target_key = target_path[-1] target = data is_subpath_add = int in target_path for i, target_name in enumerate(target_prefixes): if target_name is int: # add same value to all objects in list if not is_list_or_tuple(target): msg = (f'attempt to add to all elements of not a list ' f'{type(target)} target_path was {target_path} ' f'target_name was {target_name}') raise TypeError(msg) # LOL PYTHON namespaces [AtomicDictOperations.add(subtarget, target_path[i + 1:], value) for subtarget in target] return # int terminates this level of an add if target_name not in target: # TODO list indicies XXX that is really append though ... if is_subpath_add: # if we are targeting objects in a list for addition # abort the first time we would have to create a key # because we will eventually create an empty list # which we won't be able to add anything to and will # likely cause schema validation errors return target[target_name] = {} target = target[target_name] if update: pass elif fail_on_exists and target_key in target: msg = f'A value already exists at path {target_path} in\n{lj(data)}' raise exc.TargetPathExistsError(msg) target[target_key] = value