glom

repository·master·Indexed 23 days ago

https://github.com/mahmoud/glom

A Python library for restructuring and accessing deeply nested data structures. It provides path-based access, declarative transformations, and a command-line interface for data exploration. Key features include the T specifier for object-oriented access, Coalesce for handling missing data, Invoke for callables, and a dynamic Scope (S and A objects) for state management. Users can extend the library by creating custom Specifier Types or using register and register_op for new target types.

Tokens
10.4K
Snippets
24
Records
67
Agent score
79%

What's inside glom

  1. Overview of glom features

    master

    glom is designed for restructuring data in Python using a declarative approach. Key capabilities include:

    • Path-based access: Navigating nested structures using dot-notation or other paths.
    • Declarative data transformation: Using lightweight, Pythonic specifications to reshape data.
    • Deep assignment: Mutating nested structures.
    • Streaming: Processing data streams.
    • Data validation: Using matching logic to validate structures.
    • Debugging and Error Handling: Built-in debugging features and readable, meaningful error messages.
  2. Manage state using the glom Scope

    master

    The glom scope is a dictionary of extra values passed to the top-level glom call. These values can be accessed within the spec using the S object, which supports attribute-style dot-access for its keys.

    Basic Usage

    Pass a scope dictionary to glom and access its keys via S.

    from glom import glom, T, S
    
    count_spec = T.count(S.search)
    glom(['a', 'c', 'a', 'b'], count_spec, scope={'search': 'a'})
    # Output: 2

    Updating the scope with S() and A

    Scopes are dynamic. You can save values from the target into the scope using S or A:

    • S(key=spec): Evaluates spec and saves the result to key in the scope.
    • A.key: A shorthand to assign the current target to key in the scope.
    from glom import glom, S, A
    
    target = {'data': {'val': 9}}
    # Saving target value to scope
    spec = (S(value=T['data']['val']), {'val': S['value']})
    glom(target, spec)
    # Output: {'val': 9}
    
    # Using A as a shortcut for the current target
    spec_alt = ('data.val', A.value, {'val': S.value})
    glom(target, spec_alt)
    # Output: {'val': 9}

    Persistent state with Vars and S.globals

    By default, changes to the scope are local to the current spec. To persist state across different parts of a spec, use a Vars object.

    • S.globals: A pre-created Vars object available throughout the entire glom call.
    • Vars: A mutable namespace that allows child scopes to store state that persists beyond their local scope.
    from glom import glom, A, S
    
    # Using S.globals to persist a value
    last_spec = ([A.globals.last], S.globals.last)
    glom([3, 1, 4, 1, 5], last_spec)
    # Output: 5
  3. Choose between glom and remap

    master

    When deciding between glom and remap (the recursive map() utility), use the following criteria:

    • Use glom if you know the desired shape of the output ahead of time. Glom only traverses the paths explicitly defined in your spec.
    • Use remap if the output shape is determined by the input structure. Remap performs a full traversal of the nested data structure (walking it like a tree).
  4. Best practices for building Specifier Types

    master

    When implementing complex Specifier Types, follow these patterns to ensure compatibility and robustness:

    1. Use the Scope for Recursion: Instead of calling the global glom() function, access it via scope['glom']. This ensures your specifier works correctly even if the user has overridden the glom runtime.
    2. Leverage the TargetRegistry: To support new target types or operations, use scope[TargetRegistry].get_handler(operator_name, target, path=scope[Path]). Standard operators include "get", "iterate", "keys", "assign", and "delete".
    3. Implement Subspecs: Many specifiers use an __init__ method that accepts a subspec (often defaulting to T). This allows you to fetch a specific part of the data before applying your operation (e.g., Sum(subspec=T)).
    4. Error Handling: If a target does not support an operation, raise a descriptive error. You may consider inheriting from glom.GlomError for custom exceptions.
    from glom import glom, Path, T
    from glom.core import TargetRegistry, UnregisteredTarget
    
    class Sum(object):
        def __init__(self, subspec=T, init=int):
            self.subspec = subspec
            self.init = init
    
        def glomit(self, target, scope):
            # Use scope['glom'] for subspec execution
            if self.subspec is not T:
                target = scope['glom'](target, self.subspec, scope)
    
            # Use TargetRegistry to find the 'iterate' handler
            try:
                iterate = scope[TargetRegistry].get_handler('iterate', target, path=scope[Path])
            except UnregisteredTarget as ut:
                raise TypeError(f'can only sum on iterable targets, not {type(target).__name__}')
    
            iterator = iterate(target)
            return self._sum(iterator)
    
        def _sum(self, iterator):
            ret = self.init()
            for v in iterator:
                ret += v
            return ret
  5. Extend glom with custom spec types (glomit method)

    master

    Any class implementing a glomit(self, target, scope) method can be used as a custom glom spec. This allows for implementing logic like Lisp-style If expressions or parallel sequence evaluation.

    class If(object):
        def __init__(self, cond, if_, else_=None):
            self.cond, self.if_, self.else_ = cond, if_, else_
    
        def glomit(self, target, scope):
            g = lambda spec: scope[glom](target, spec, scope)
            if g(self.cond):
                return g(self.if_)
            elif self.else_:
                return g(self.else_)
            else:
                return None
    
    glom(1, If(bool, {'yes': T}, {'no': T}))
  6. When to use a Specifier Type vs a Lambda

    master

    While glom supports arbitrary callables (like lambda functions), you should use a formal Specifier Type when you need to:

    1. Perform validation at spec construction time.
    2. Enable users to interact with new target types and operations via the TargetRegistry.
    3. Improve readability and reusability of complex data transformations.
    4. Temporarily change the glom runtime behavior.

    If you are performing a simple, one-off transformation, a Python lambda or a standard function is preferred.

  7. Identify the glom public API

    master

    The primary entrypoint is the glom() function.

    Guidelines for determining if a feature is public:

    1. It must be in the top-level glom package.
    2. It must be documented in the official glom documentation.

    Functionality not in the top-level package or not documented may change or disappear without notice.

  8. Debug specs using Inspect

    master
    If error messages are insufficient to diagnose why a spec is behaving unexpectedly, use glom.Inspect. This tool is designed to help debug the specification logic itself when the data might be correct but the transformation is not what you intended.
  9. Understanding the glom Scope

    master

    The scope object passed to glomit provides access to the current execution context. It allows specifiers to interact with the target, the current path, and the glom engine itself.

    Commonly used keys in the scope dictionary include:

    • T: The current target (the data being operated on).
    • Spec: The current spec.
    • Path: The current path within the data structure.
    • TargetRegistry: Used to register and retrieve new operations and target types (imported from glom.core).
    • glom: The currently active glom() function (used instead of importing glom directly to ensure compatibility with overridden runtimes).
  10. Understand glom through mental models and analogies

    master

    If you are coming from other data manipulation tools, glom can be understood through several conceptual analogies:

    • List Comprehensions: Like Python list comprehensions, glom uses square brackets for list processing (e.g., [lambda x: x % 2]). However, glom requires a callable or another glom spec to enable deferred processing and can return glom.SKIP to exclude items from a list.
    • Object Templating: Similar to Jinja, Django, or Mustache, but instead of working on strings or HTML, glom works on Python objects (dicts, functions, primitives, etc.). It is essentially an "object templating" system.
    • Query Languages (SQL/GraphQL): glom acts as a Python query language for Python objects. Unlike SQL (which targets tables) or GraphQL (which targets graphs), glom can restructure the shape of the data while fetching and transforming it.
    • Data Validation (jsonschema/schema): While validation libraries focus on parsing and structuring, glom excels at the next step: translating valid, structured objects (like database models) into other formats, such as JSON-serializable objects.
    • jq: The glom CLI functions similarly to jq, but uses Python as the query language instead of a custom syntax.
    • XPath/XSLT: glom draws inspiration from the functional capabilities of XPath/XSLT but aims to be less verbose and less "pure."
  11. Read branched exceptions in Coalesce or Switch specs

    master

    For branching specs like Coalesce or Switch that attempt multiple paths, glom provides an error tree to visualize every attempt made before the final exception was raised.

    Visual indicators in the trace:

    • +: Indicates the start of a branching spec.
    • |: Indicates the current nesting level within a branch.
    • \: Indicates a new branch of the root branching spec.
    • X: Indicates an exception occurred within a specific branch.

    This allows you to see exactly which branches were attempted, which ones failed, and what the specific errors were for each attempt.

    >>> target = {'n': 'nope', 'xxx': {'z': {'v': 0}}}
    >>> glom(target, Coalesce(('xxx', 'z', 'n'), 'yyy'))
    # ... Traceback showing error tree with +, |, \, and X markers ...
  12. How the glom engine processes specs

    master

    The core engine of glom is a recursive loop that processes specs based on their type:

    • String or Path: Performs a deep-get on the target.
    • Callable: Calls the function on the target.
    • Dict: Creates a new dictionary where each key maps to the result of a glom() call on the corresponding subspec.
    • List: Runs the first element of the list (the subspec) on every element in the target's iterator.
    • Tuple: Chains the specs together, running each subsequent spec on the result of the previous one.
    def glom(target, spec):
        if isinstance(spec, (str, Path)):
            return _get_path(target, spec)
        elif callable(spec):
            return spec(target)
        elif isinstance(spec, dict):
            ret = {}
            for field, subspec in spec.items():
               ret[field] = glom(target, subspec)
            return ret
        elif isinstance(spec, list):
            subspec = spec[0]
            iterator = _get_iterator(target)
            return [glom(t, subspec) for t in iterator]
        elif isinstance(spec, tuple):
            res = target
            for subspec in spec:
                res = glom(res, subspec)
            return res
        else:
            raise TypeError('expected one of the above types')