def __init__(self, collection): if is_str(collection): self.collection = collection.split() elif is_collection(collection): self.collection = collection else: # is scalar self.collection = {None, collection}
def get_field(cls, name, key=None, default=False): """Get Field Value Return value from the account given a field name and key. """ value = cls.__dict__.get(name) if value is None: if default is False: raise Error("not found.", culprit=(cls.get_name(), cls.combine_name(name, key))) else: return default if key is None: if is_collection(value): choices = [] for k, v in Collection(value).items(): try: choices.append(" %s: %s" % (k, v.get_key())) except AttributeError: choices.append(" %s:" % k) raise Error( "composite value found, need key. Choose from:", *choices, sep="\n", culprit=name, is_collection=True, collection=value ) else: try: if is_collection(value): value = value[key] else: warn("not a composite value, key ignored.", culprit=name) key = None except (IndexError, KeyError, TypeError): raise Error("not found.", culprit=cls.combine_name(name, key)) # generate the value if needed try: value.generate(name, key, cls) except AttributeError as err: pass return value
def extract(cls, value, name, key=None): "Generate all secrets in an account value" if not is_collection(value): if hasattr(value, 'initialize'): value.initialize(cls, name, key) return value try: return {k: cls.extract(v, name, k) for k, v in value.items()} except AttributeError: return [cls.extract(v, name, i) for i, v in enumerate(value)]
def __init__(self, collection, splitter=None): if is_str(collection): self.collection = collection.split(splitter) elif is_collection(collection): self.collection = collection elif collection is None: self.collection = [] else: # is scalar self.collection = {None: collection}
def flatten(collection, split=False): # if split is specified, create list from string by splitting at whitespace if split and is_str(collection): collection = collection.split() if is_collection(collection): for each in collection: for e in flatten(each): yield e else: yield collection
def write_summary(cls, all=False, sort=False): # present all account values that are not explicitly secret to the user label_color = get_setting('_label_color') highlight_color = get_setting('_highlight_color') def fmt_field(name, value='', key=None, level=0): hl = False # resolve values if isinstance(value, Script): hl = True value = value.script elif cls.is_secret(name, key): reveal = "reveal with: {}".format( highlight_color( join('avendesora', 'value', cls.get_name(), cls.combine_field(name, key)))) value = ', '.join(cull([value.get_description(), reveal])) elif isinstance(value, (GeneratedSecret, ObscuredSecret)): v = cls.get_scalar(name, key) value = ', '.join(cull([value.get_description(), str(v)])) else: value = str(value) # format values if '\n' in value: value = indent(dedent(value), get_setting('indent')).strip('\n') sep = '\n' elif value: sep = ' ' else: sep = '' if hl: value = highlight_color(value) name = str(name).replace('_', ' ') leader = level * get_setting('indent') return indent( label_color((name if key is None else str(key)) + ':') + sep + value, leader) # preload list with the names associated with this account names = [cls.get_name()] + getattr(cls, 'aliases', []) lines = [fmt_field('names', ', '.join(names))] for key, value in cls.items(all=all, sort=sort): if is_collection(value): lines.append(fmt_field(key)) for k, v in Collection(value).items(): lines.append(fmt_field(key, v, k, level=1)) else: lines.append(fmt_field(key, value)) output(*lines, sep='\n')
def extract(value, name, key=None): if not is_collection(value): if hasattr(value, "generate"): value.generate(name, key, cls) # value = 'Hidden(%s)' % Obscure.hide(str(value)) return value try: return {k: extract(v, name, k) for k, v in value.items()} except AttributeError: # still need to work out how to output the question. return [extract(v, name, i) for i, v in enumerate(value)]
def account_contains(cls, target): if cls.id_contains(target): return True target = target.lower() for key, value in cls.items(): try: if is_collection(value): for k, v in Collection(value).items(): if target in v.lower(): return True elif target in value.lower(): return True except AttributeError: pass return False
def __init__(self, collection, splitter=None, **kwargs): if is_str(collection): if callable(splitter): self.collection = splitter(collection, **kwargs) return elif splitter is not False: self.collection = collection.split(splitter) return if is_collection(collection): self.collection = collection return if collection is None: self.collection = [] return # is scalar self.collection = {None: collection}
def write_summary(cls): # present all account values that are not explicitly secret to the user def fmt_field(key, value="", level=0): if "\n" in value: value = indent(dedent(value), get_setting("indent")).strip("\n") sep = "\n" elif value: sep = " " else: sep = "" key = str(key).replace("_", " ") leader = level * get_setting("indent") return indent(LabelColor(key + ":") + sep + value, leader) def reveal(name, key=None): return "<reveal with 'avendesora value %s %s'>" % (cls.get_name(), cls.combine_name(name, key)) def extract_collection(name, collection): lines = [fmt_field(key)] for k, v in Collection(collection).items(): if hasattr(v, "generate"): # is a secret, get description if available try: v = "%s %s" % (v.get_key(), reveal(name, k)) except AttributeError: v = reveal(name, k) lines.append(fmt_field(k, v, level=1)) return lines # preload list with the names associated with this account names = [cls.get_name()] if hasattr(cls, "aliases"): names += Collection(cls.aliases) lines = [fmt_field("names", ", ".join(names))] for key, value in cls.items(): if key in TOOL_FIELDS: pass # is an Avendesora field elif is_collection(value): lines += extract_collection(key, value) elif hasattr(value, "generate"): lines.append(fmt_field(key, reveal(key))) else: lines.append(fmt_field(key, value)) output(*lines, sep="\n")
def get_composite(cls, name, default=None): """Get field value given a field name. A lower level interface than *get_value()* that given a name returns the value of the associated field, which may be a scalar (string or integer) or a composite (array of dictionary). Unlike *get_value()*, the actual value is returned, not a object that contains multiple facets of the value. Args: name (str): The name of the field. Returns: The requested value. """ value = getattr(cls, name, None) if value is None: if name == 'NAME': return cls.get_name() return default if is_collection(value): if is_mapping(value): # a dictionary or dictionary-like object result = {} for key in value.keys(): v = cls.get_scalar(name, key) if isinstance(v, GeneratedSecret) or isinstance( v, ObscuredSecret): v = str(v) result[key] = v else: result = [] for index in range(len(value)): v = cls.get_scalar(name, index) if isinstance(v, GeneratedSecret) or isinstance( v, ObscuredSecret): v = str(v) result.append(v) else: result = cls.get_scalar(name) if isinstance(result, GeneratedSecret) or isinstance( result, ObscuredSecret): result = str(result) return result
def account_contains(cls, target): if cls.id_contains(target): return True target = target.lower() for key, value in cls.fields(): if key in TOOL_FIELDS: continue try: if is_collection(value): for k, v in Collection(value).items(): if target in v.lower(): return True elif target in value.lower(): return True except AttributeError: # is not a string, and so pass return False
def get_fields(cls, all=False, sort=False): """Iterate through fields. Iterates through the field names. Example:: for name, keys in account.get_fields(): for key, value in account.get_values(name): display(indent( value.render(('{f} ({d}): {v}', '{f}: {v}')) )) Example:: fields = [ account.combine_field(name, key) for name, keys in account.get_fields() for key in keys ] for field in fields: value = account.get_value(field) display(f'{field}: {value!s}') Args: all (bool): If False, ignore the tool fields. sort (bool): If False, use natural sort order. Returns: A pair (2-tuple) that contains both field name and the key names. None is returned for the key names if the field holds a scalar value. """ for field in cls.fields(): value = getattr(cls, field) if is_collection(value): yield field, Collection(value).keys() else: yield field, [None]
def dumps(obj, *, width=0, sort_keys=False, indent=4, renderers=None, default=None, _level=0): """Recursively convert object to *NestedText* string. Args: obj: The object to convert to *NestedText*. width (int): Enables compact lists and dictionaries if greater than zero and if resulting line would be less than or equal to given width. sort_keys (bool or func): Dictionary items are sorted by their key if *sort_keys* is true. If a function is passed in, it is used as the key function. indent (int): The number of spaces to use to represent a single level of indentation. Must be one or greater. renderers (dict): A dictionary where the keys are types and the values are render functions (functions that take an object and convert it to a string). These will be used to convert values to strings during the conversion. default (func or 'strict'): The default renderer. Use to render otherwise unrecognized objects to strings. If not provided an error will be raised for unsupported data types. Typical values are *repr* or *str*. If 'strict' is specified then only dictionaries, lists, strings, and those types specified in *renderers* are allowed. If *default* is not specified then a broader collection of value types are supported, including *None*, *bool*, *int*, *float*, and *list*- and *dict*-like objects. In this case Booleans is rendered as 'True' and 'False' and None and empty lists and dictionaries are rendered as empty strings. _level (int): The number of indentation levels. When dumps is invoked recursively this is used to increment the level and so the indent. Should not be specified by the user. Returns: The *NestedText* content. Raises: NestedTextError: if there is a problem in the input data. Examples: .. code-block:: python >>> import nestedtext as nt >>> data = { ... 'name': 'Kristel Templeton', ... 'sex': 'female', ... 'age': '74', ... } >>> try: ... print(nt.dumps(data)) ... except nt.NestedTextError as e: ... print(str(e)) name: Kristel Templeton sex: female age: 74 The *NestedText* format only supports dictionaries, lists, and strings and all leaf values must be strings. By default, *dumps* is configured to be rather forgiving, so it will render many of the base Python data types, such as *None*, *bool*, *int*, *float* and list-like types such as *tuple* and *set* by converting them to the types supported by the format. This implies that a round trip through *dumps* and *loads* could result in the types of values being transformed. You can restrict *dumps* to only supporting the native types of *NestedText* by passing `default='strict'` to *dumps*. Doing so means that values that are not dictionaries, lists, or strings generate exceptions; as do empty dictionaries and lists. .. code-block:: python >>> data = {'key': 42, 'value': 3.1415926, 'valid': True} >>> try: ... print(nt.dumps(data)) ... except nt.NestedTextError as e: ... print(str(e)) key: 42 value: 3.1415926 valid: True >>> try: ... print(nt.dumps(data, default='strict')) ... except nt.NestedTextError as e: ... print(str(e)) 42: unsupported type. Alternatively, you can specify a function to *default*, which is used to convert values to strings. It is used if no other converter is available. Typical values are *str* and *repr*. .. code-block:: python >>> class Color: ... def __init__(self, color): ... self.color = color ... def __repr__(self): ... return f'Color({self.color!r})' ... def __str__(self): ... return self.color >>> data['house'] = Color('red') >>> print(nt.dumps(data, default=repr)) key: 42 value: 3.1415926 valid: True house: Color('red') >>> print(nt.dumps(data, default=str)) key: 42 value: 3.1415926 valid: True house: red You can also specify a dictionary of renderers. The dictionary maps the object type to a render function. .. code-block:: python >>> renderers = { ... bool: lambda b: 'yes' if b else 'no', ... int: hex, ... float: lambda f: f'{f:0.3}', ... Color: lambda c: c.color, ... } >>> try: ... print(nt.dumps(data, renderers=renderers)) ... except nt.NestedTextError as e: ... print(str(e)) key: 0x2a value: 3.14 valid: yes house: red If the dictionary maps a type to *None*, then the default behavior is used for that type. If it maps to *False*, then an exception is raised. .. code-block:: python >>> renderers = { ... bool: lambda b: 'yes' if b else 'no', ... int: hex, ... float: False, ... Color: lambda c: c.color, ... } >>> try: ... print(nt.dumps(data, renderers=renderers)) ... except nt.NestedTextError as e: ... print(str(e)) 3.1415926: unsupported type. Both *default* and *renderers* may be used together. *renderers* has priority over the built-in types and *default*. When a function is specified as *default*, it is always applied as a last resort. """ # define sort function {{{3 if sort_keys: def sort(keys): return sorted(keys, key=sort_keys if callable(sort_keys) else None) else: def sort(keys): return keys # render_dict_item {{{3 def render_dict_item(key, dictionary, indent, rdumps): value = dictionary[key] if is_a_scalar(key): key = str(key) if not is_a_str(key): raise NestedTextError(template='keys must be strings.', culprit=key) multiline_key_required = (not key or '\n' in key or key.strip(' ') != key or key[:1] == "#" or key[:2] in ["- ", "> ", ": "] or ': ' in key) if multiline_key_required: key = "\n".join(": " + l if l else ":" for l in key.split('\n')) if is_str(value): # force use of multiline value with multiline keys return key + "\n" + add_leader(value, indent * ' ' + '> ') else: return key + rdumps(value) else: return add_prefix(key + ":", rdumps(value)) # render_inline_dict {{{3 def render_inline_dict(obj): exclude = set('\n[]{}:,') items = [] for k in sort(obj): v = render_inline_value(obj[k], exclude=exclude) k = render_inline_scalar(k, exclude=exclude) items.append(f'{k}: {v}') return '{' + ', '.join(items) + '}' # render_inline_list {{{3 def render_inline_list(obj): items = [] for v in obj: v = render_inline_value(v, exclude=set('\n[]{},')) items.append(v) endcap = ', ]' if len(items) and items[-1] == '' else ']' return '[' + ', '.join(items) + endcap # render_inline_value {{{3 def render_inline_value(obj, exclude): if is_a_dict(obj): return render_inline_dict(obj) if is_a_list(obj): return render_inline_list(obj) return render_inline_scalar(obj, exclude) # render_inline_scalar {{{3 def render_inline_scalar(obj, exclude): render = renderers.get(type(obj)) if renderers else None if render is False: raise ValueError() elif render: value = render(obj) if "\n" in value: raise ValueError() elif is_a_str(obj): value = obj elif is_a_scalar(obj): value = '' if obj is None else str(obj) elif default and callable(default): value = default(obj) else: raise ValueError() if exclude & set(value): raise ValueError() if value.strip(' ') != value: raise ValueError() return value # define object type identification functions {{{3 if default == 'strict': is_a_dict = lambda obj: isinstance(obj, dict) is_a_list = lambda obj: isinstance(obj, list) is_a_str = lambda obj: isinstance(obj, str) is_a_scalar = lambda obj: False else: is_a_dict = is_mapping is_a_list = is_collection is_a_str = is_str is_a_scalar = lambda obj: obj is None or isinstance( obj, (bool, int, float)) if is_str(default): raise NotImplementedError(default) # define dumps function for recursion {{{3 def rdumps(v): return dumps(v, width=width - indent, sort_keys=sort_keys, indent=indent, renderers=renderers, default=default, _level=_level + 1) # render content {{{3 assert indent > 0 error = None need_indented_block = is_collection(obj) content = '' render = renderers.get(type(obj)) if renderers else None if render is False: error = "unsupported type." elif render: content = render(obj) if "\n" in content: need_indented_block = True elif is_a_dict(obj): try: if obj and not (width > 0 and len(obj) <= width / 6): raise ValueError content = render_inline_dict(obj) if obj and len(content) > width: raise ValueError except ValueError: content = "\n".join( render_dict_item(k, obj, indent, rdumps) for k in sort(obj)) elif is_a_list(obj): try: if obj and not (width > 0 and len(obj) <= width / 6): raise ValueError content = render_inline_list(obj) if obj and len(content) > width: raise ValueError except ValueError: content = "\n".join(add_prefix("-", rdumps(v)) for v in obj) elif is_a_str(obj): text = obj.replace('\r\n', '\n').replace('\r', '\n') if "\n" in text or _level == 0: content = add_leader(text, '> ') need_indented_block = True else: content = text elif is_a_scalar(obj): if obj is None: content = '' else: content = str(obj) elif default and callable(default): content = default(obj) else: error = 'unsupported type.' if need_indented_block and content and _level: content = "\n" + add_leader(content, indent * ' ') if error: raise NestedTextError(obj, template=error, culprit=repr(obj)) return content
def open_browser(cls, key=None, browser_name=None, list_urls=False): if not browser_name: browser_name = cls.get_scalar('browser', default=None) browser = StandardBrowser(browser_name) # get the urls from the urls attribute # this must be second so it overrides those from recognizers. primary_urls = getattr(cls, 'urls', []) if type(primary_urls) != dict: if is_str(primary_urls): primary_urls = primary_urls.split() primary_urls = {None: primary_urls} if primary_urls else {} # get the urls from the url recognizers discovery = getattr(cls, 'discovery', ()) urls = {} for each in Collection(discovery): urls.update(each.all_urls()) # combine, primary_urls must be added to urls, so they dominate urls.update(primary_urls) if list_urls: default = getattr(cls, 'default_url', None) for name, url in urls.items(): if is_collection(url): url = list(Collection(url))[0] if name == default: url += HighlightColor(' [default]') if not name: name = '' elif not name: continue output(LabelColor('{:>24s}:'.format(name)), url) return # select the urls keys = cull(list(urls.keys())) if not key: key = getattr(cls, 'default_url', None) if not key and keys and len(keys) == 1: key = keys[0] try: urls = urls[key] except KeyError: if keys: if key: msg = 'unknown key, choose from {}.' else: msg = 'key required, choose from {}.' raise PasswordError(msg.format(conjoin(repr(k) for k in keys)), culprit=key) else: if key: raise PasswordError( 'keys are not supported with urls on this account.', culprit=key) else: raise PasswordError('no url available.') # open the url urls = Collection(urls) url = list(urls)[0] # use the first url specified browser.run(url)
def get_scalar(cls, name, key=None, default=False): """Get field value given a field name and key. A lower level interface than *get_value()* that given a name and perhaps a key returns a scalar value. Also takes an optional default value that is returned if the value is not found. Unlike *get_value()*, the actual value is returned, not a object that contains multiple facets of the value. The *name* is the field name, and the *key* would identity which value is desired if the field is a composite. If default is False, an error is raised if the value is not present, otherwise the default value itself is returned. Args: name (str): The name of the field. key (str or int) The key for the desired value (should be None for scalar values). default: The value to return if the requested value is not available. Returns: The requested value. """ value = getattr(cls, name, None) if value is None: if name == 'NAME': return cls.get_name() if default is False: raise PasswordError('field not found.', culprit=(cls.get_name(), cls.combine_field(name, key))) return default if key is None: if is_collection(value): choices = {} for k, v in Collection(value).items(): try: desc = v.get_description() except AttributeError: desc = None if desc: choices[' %s: %s' % (k, desc)] = k else: choices[' %s:' % k] = k raise PasswordError( 'composite value found, need key. Choose from:', *sorted(choices.keys()), sep='\n', culprit=(cls.get_name(), cls.combine_field(name, key)), is_collection=True, collection=choices) else: try: if is_collection(value): value = value[key] else: warn('not a composite value, key ignored.', culprit=name) key = None except (IndexError, KeyError, TypeError): raise PasswordError('key not found.', culprit=(cls.get_name(), cls.combine_field(name, key))) # initialize the value if needed try: value.initialize(cls, name, key) # if Secret or Script, initialize otherwise raise exception except AttributeError: pass return value
def run(cls, command, args): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) # read archive file archive_path = get_setting('archive_file') f = PythonFile(archive_path) archive = f.run() import arrow created = archive.get('CREATED') if created: created = arrow.get(created).format('YYYY-MM-DD hh:mm:ss A ZZ') output('archive created: %s' % created) archive_accounts = archive.get('ACCOUNTS') if not archive_accounts: raise Error( 'corrupt archive, ACCOUNTS missing.', culprit=archive_path ) # run the generator generator = PasswordGenerator() # determine the account and open the URL current_accounts = {} for account in generator.all_accounts: entry = account.archive() if entry: current_accounts[account.get_name()] = entry # report any new or missing accounts new = current_accounts.keys() - archive_accounts.keys() missing = archive_accounts.keys() - current_accounts.keys() for each in sorted(new): output('new account:', each) for each in sorted(missing): output('missing account:', each) # for the common accounts, report any differences in the fields common = archive_accounts.keys() & current_accounts.keys() for account_name in sorted(common): archive_account = archive_accounts[account_name] current_account = current_accounts[account_name] # report any new or missing fields new = current_account.keys() - archive_account.keys() missing = archive_account.keys() - current_account.keys() for each in sorted(new): output(account_name, 'new field', each, sep=': ') for each in sorted(missing): output(account_name, 'new field', each, sep=': ') # for the common fields, report any differences in the values shared = archive_account.keys() & current_account.keys() for field_name in sorted(shared): try: archive_value = archive_account[field_name] current_value = current_account[field_name] if is_collection(current_value) != is_collection(archive_value): output(account_name, 'field dimension differs', field_name, sep=': ') elif is_collection(current_value): archive_items = Collection(archive_account[field_name]).items() current_items = Collection(current_account[field_name]).items() archive_keys = set(k for k, v in archive_items) current_keys = set(k for k, v in current_items) new = current_keys - archive_keys missing = archive_keys - current_keys for each in sorted(new): output(account_name, field_name, 'new member', each, sep=': ') for each in sorted(missing): output(account_name, field_name, 'missing member', each, sep=': ') for k in sorted(archive_keys & current_keys): if str(archive_value[k]) != str(current_value[k]): output(account_name, 'member differs', '%s[%s]' % (field_name, k), sep=': ') else: if dedent(str(archive_value)) != dedent(str(current_value)): output(account_name, 'field differs', field_name, sep=': ') except Exception: error( 'unanticipated situation.', culprit=(account_name, field_name) ) raise
def dumps(obj, *, sort_keys=False, indent=4, renderers=None, default=None, level=0): """Recursively convert object to *NestedText* string. Args: obj: The object to convert to *NestedText*. sort_keys (bool or func): Dictionary items are sorted by their key if *sort_keys* is true. If a function is passed in, it is used as the key function. indent (int): The number of spaces to use to represent a single level of indentation. Must be one or greater. renderers (dict): A dictionary where the keys are types and the values are render functions (functions that take an object and convert it to a string). These will be used to convert values to strings during the conversion. default (str or func): The default renderer. Use to render otherwise unrecognized objects to strings. If not provided an error will be raised for unsupported data types. Typical values are *repr* or *str*. If 'strict' is specified then only dictionaries, lists, strings, and those types specified in *renderers* are allowed. If *default* is not specified then a broader collection of value types are supported, including *None*, *bool*, *int*, *float*, and *list*- and *dict*-like objects. level (int): The number of indentation levels. When dumps is invoked recursively this is used to increment the level and so the indent. Generally not specified by the user, but can be useful in unusual situations to specify an initial indent. Returns: The *NestedText* content. Raises: NestedTextError: if there is a problem in the input data. Examples: This example writes to a string, but it is common to write to a file. The file name and extension are arbitrary. However, by convention a '.nt' suffix is generally used for *NestedText* files. .. code-block:: python >>> import nestedtext as nt >>> data = { ... 'name': 'Kristel Templeton', ... 'sex': 'female', ... 'age': '74', ... } >>> try: ... print(nt.dumps(data)) ... except nt.NestedTextError as e: ... print(str(e)) name: Kristel Templeton sex: female age: 74 The *NestedText* format only supports dictionaries, lists, and strings. By default, *dumps* is configured to be rather forgiving, so it will render many of the base Python data types, such as *None*, *bool*, *int*, *float* and list-like types such as *tuple* and *set* by converting them to the types supported by the format. This implies that a round trip through *dumps* and *loads* could result in the types of values being transformed. You can prevent this by passing `default='strict'` to *dumps*. Doing so means that values that are not dictionaries, lists, or strings generate exceptions. .. code-block:: python >>> data = {'key': 42, 'value': 3.1415926, 'valid': True} >>> try: ... print(nt.dumps(data)) ... except nt.NestedTextError as e: ... print(str(e)) key: 42 value: 3.1415926 valid: True >>> try: ... print(nt.dumps(data, default='strict')) ... except nt.NestedTextError as e: ... print(str(e)) 42: unsupported type. Alternatively, you can specify a function to *default*, which is used to convert values to strings. It is used if no other converter is available. Typical values are *str* and *repr*. .. code-block:: python >>> class Color: ... def __init__(self, color): ... self.color = color ... def __repr__(self): ... return f'Color({self.color!r})' ... def __str__(self): ... return self.color >>> data['house'] = Color('red') >>> print(nt.dumps(data, default=repr)) key: 42 value: 3.1415926 valid: True house: Color('red') >>> print(nt.dumps(data, default=str)) key: 42 value: 3.1415926 valid: True house: red You can also specify a dictionary of renderers. The dictionary maps the object type to a render function. .. code-block:: python >>> renderers = { ... bool: lambda b: 'yes' if b else 'no', ... int: hex, ... float: lambda f: f'{f:0.3}', ... Color: lambda c: c.color, ... } >>> try: ... print(nt.dumps(data, renderers=renderers)) ... except nt.NestedTextError as e: ... print(str(e)) key: 0x2a value: 3.14 valid: yes house: red If the dictionary maps a type to *None*, then the default behavior is used for that type. If it maps to *False*, then an exception is raised. .. code-block:: python >>> renderers = { ... bool: lambda b: 'yes' if b else 'no', ... int: hex, ... float: False, ... Color: lambda c: c.color, ... } >>> try: ... print(nt.dumps(data, renderers=renderers)) ... except nt.NestedTextError as e: ... print(str(e)) 3.1415926: unsupported type. Both *default* and *renderers* may be used together. *renderers* has priority over the built-in types and *default*. When a function is specified as *default*, it is always applied as a last resort. """ # define sort function if sort_keys: def sort(keys): return sorted(keys, key=sort_keys if callable(sort_keys) else None) else: def sort(keys): return keys # define object type identification functions if default == 'strict': is_a_dict = lambda obj: isinstance(obj, dict) is_a_list = lambda obj: isinstance(obj, list) is_a_str = lambda obj: isinstance(obj, str) is_a_scalar = lambda obj: False else: is_a_dict = is_mapping is_a_list = is_collection is_a_str = is_str is_a_scalar = lambda obj: obj is None or isinstance( obj, (bool, int, float)) if is_str(default): raise NotImplementedError(default) # define dumps function for recursion def rdumps(v): return dumps(v, sort_keys=sort_keys, indent=indent, renderers=renderers, default=default, level=level + 1) # render content assert indent > 0 error = None need_indented_block = is_collection(obj) content = '' render = renderers.get(type(obj)) if renderers else None if render is False: error = "unsupported type." elif render: content = render(obj) if "\n" in content or ('"' in content and "'" in content): need_indented_block = True elif is_a_dict(obj): content = "\n".join( add_prefix(render_key(k) + ":", rdumps(obj[k])) for k in sort(obj)) elif is_a_list(obj): content = "\n".join(add_prefix("-", rdumps(v)) for v in obj) elif is_a_str(obj): if "\n" in obj: content = add_leader(obj, '> ') need_indented_block = True else: content = obj if level == 0: content = add_leader(content, '> ').strip() elif is_a_scalar(obj): content = str(obj) elif default and callable(default): content = default(obj) else: error = "unsupported type." if need_indented_block and level != 0: content = "\n" + add_leader(content, indent * ' ') if error: raise NestedTextError(obj, template=error, culprit=repr(obj)) return content