Exemplo n.º 1
0
 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}
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
 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)]
Exemplo n.º 4
0
 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}
Exemplo n.º 5
0
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
Exemplo n.º 6
0
    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')
Exemplo n.º 7
0
 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)]
Exemplo n.º 8
0
 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
Exemplo n.º 9
0
 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}
Exemplo n.º 10
0
    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")
Exemplo n.º 11
0
    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
Exemplo n.º 12
0
 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
Exemplo n.º 13
0
    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]
Exemplo n.º 14
0
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
Exemplo n.º 15
0
    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)
Exemplo n.º 16
0
    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
Exemplo n.º 17
0
    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
Exemplo n.º 18
0
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