cemysf / safebag

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

safebag

Stable Version tests

Safebag is a little python package implementing optional chaining.

Installation

pip install safebag

Usage

Code we want to avoid

if (
    obj is not None
    and obj.attr is not None
    and obj.attr.attr is not None
    and obj.attr.attr.attr is not None
    and obj.attr.attr.attr.attr is not None
):
    # Do something useful with obj.attr.attr.attr.attr
    ...

Pythonic solution

try:
    print(obj.attr.attr.attr.attr)
    # Do something useful with obj.attr.attr.attr.attr
except(NameError, AttributeError) as e:
    # Do something useful with an error

Still it's not very clean way in case of multiple attribute handling in one place

try:
    print(obj.attr.attr.attr.attr)
    # Do something useful with obj.attr.attr.attr.attr
except(NameError, AttributeError) as e:
    ...

try:
    print(obj.attr.attr)
    # Do something useful with obj.attr.attr
except(NameError, AttributeError) as e:
    ...

try:
    print(obj.attr)
    # Do something useful with obj.attr
except(NameError, AttributeError) as e:
    ...

Usage example:

from safebag import chain, get_value

if attr := chain(obj).attr.attr.attr.attr:
    # Do something useful with obj.attr.attr.attr.attr
    print(get_value(attr))

if attr := chain(obj).attr.attr:
    # Do something useful with obj.attr.attr
    print(get_value(attr))

if attr := chain(obj).attr:
    # Do something useful with obj.attr
    print(get_value(attr))

Examples

chain [source]

Optional chain constructor, may be constructed from any object

Chain is used for building sequence of null-safe attribute calls

from __future__ import annotations

import dataclasses as dt
import typing

from safebag import chain


@dt.dataclass
class Node:
    data: int
    node: typing.Optional[Node]


nodes = Node(data=1, node=Node(data=2, node=None))

third_node_proxy = chain(nodes).node.node.node
print(third_node_proxy)  # ChainProxy(data_object=None, bool_hook=False)

get_value [source]

Final value getter for optional chain.

Optional chain constructed from any object. Chain is used for building sequence of null-safe attribute calls.

from __future__ import annotations

import dataclasses as dt
import typing


@dt.dataclass
class Node:
    data: int
    node: typing.Optional[Node]


nodes = Node(data=1, node=Node(data=2, node=None))

from safebag import chain, get_value

third_node_proxy = chain(nodes).node.node.node
value = get_value(third_node_proxy)
assert value is None

next_node = chain(nodes).node
value = get_value(next_node)  # Node(data=2, node=None)

Possible way of getting value

if next_node := chain(nodes).node:
    print(next_node.get_value())  # Node(data=2, node=None)

Default can be passed as argument

if next_node := chain(nodes).node.node:
    print(next_node.get_value(default='Default')) # 'Default'

Useful in combination with walrus operator:

if next_node := chain(nodes).node.node:
    print(get_value(next_node))

if next_node := chain(nodes).node:
    print(get_value(next_node))  # Node(data=2, node=None)

ChainProxy [source]

ChainProxy container:

  • stores data_object
  • proxying data_object attribute value into new ChainProxy instance when attribute is invoked. If attribute does not exist or attribute value is None. ChainProxy instance data_object will be None and bool_hook will be False.
  • ChainProxy instance always returning when attribute is invoked.

Release: 0.2.0

Performance update

Increase performance by adding empty_proxy instead of real ChainProxy

For case below Numbers: # 0.100 -> 0.046

import timeit
from dataclasses import dataclass

@dataclass
class Node:
    data: int
    node: typing.Optional[Node]

node = Node(1, None)
executable = lambda: get_value(chain(node).node.node.node.node.node.node.node.node.node.node)
perf = timeit.timeit(executable, number=10000)

About

License:MIT License


Languages

Language:Python 100.0%