Skip to content

bunches

Version PyPI Latest Release GitHub Latest Release
Status Build Status Code Coverage Development Status Project Stability
Documentation Hosted By
Tools Documentation Linter Type Checker Dependency Manager Pre-commit CI Editor Settings Repository Template Dependency Maintainer
Compatibility Compatible Python Versions Linux MacOS Windows
Stats PyPI Download Rate (per month) GitHub Stars GitHub Contributors GitHub Issues GitHub Forks

What is bunches?

bunches provides drop-in replacements for Python's built-in dict and list that add a consistent set of extra tools. Every class in the package:

  • stores its data in a plain Python collection in the contents attribute, so you can always drop back to the built-in type;
  • has an add method, the default way to put data in the collection;
  • has a delete method, the default way to remove data (it is also what del collection[item] calls);
  • has a subset method that returns a new collection containing only some of the data;
  • supports + (returns a new collection with an item added) and += (adds the item in place), both of which use add.

The package is fully typed, has no dependencies, and supports Python 3.11 and later.

Why use bunches?

The built-in collections are excellent, but they leave you to re-implement the same conveniences whenever you build a registry, a set of options, or an ordered group of named items. bunches supplies those conveniences once: wildcard lookups, keys inferred from the items you store, layered dictionaries, and lists that can be searched by name.

Classes

Mappings

  • Dictionary: drop-in replacement for a python dict with an add method for a default mechanism of adding data, a delete method for a default mechanism of deleting data, and a subset method for returning a subset of the key/value pairs in a new Dictionary. Its keys, values and items methods return tuples and it has defaultdict-like behavior when using get.
  • Catalog: wildcard-accepting dict which is intended for storing different options and strategies. It recognizes the special keys all, default and none and returns lists of matches if a list of keys is provided.
  • ChainDict: a Dictionary that stores several dictionaries and searches them in order, like collections.ChainMap.
  • Repository: a dictionary that automatically supplies key names for stored items. The overwrite argument determines if a unique key should always be created or whether entries may be overwritten.

Sequences

  • Listing: drop-in replacement for a python list with an add method for a default mechanism of adding data, a delete method for a default mechanism of deleting data, a prepend method, and a subset method for returning a subset of the items in a new Listing.
  • DictList: iterable with both dict and list interfaces. Stored items must be hashable or have a name attribute.

Getting started

Requirements

bunches requires Python 3.11 or later and runs on Linux, macOS and Windows. It has no third-party dependencies.

Installation

To install bunches, use pip:

pip install bunches

Usage

Dictionary

A Dictionary works like a dict. Its add method updates the stored data, subset returns a new Dictionary, and get falls back on default_factory (a value or a callable) when a key is missing.

from bunches import Dictionary

colors = Dictionary(contents = {'red': 1, 'green': 2}, default_factory = 0)
colors.add({'blue': 3})
colors['blue']  # 3
colors.get('purple')  # 0
colors.subset(include = ['red', 'blue']).contents  # {'red': 1, 'blue': 3}
colors.subset(exclude = 'red').keys()  # ('green', 'blue')
del colors['green']
colors.contents  # {'red': 1, 'blue': 3}
(colors + {'pink': 4}).contents  # {'red': 1, 'blue': 3, 'pink': 4}
colors.contents  # {'red': 1, 'blue': 3}

Catalog

A Catalog is meant for storing options. The all, default and none keys are special and a list of keys returns a list of values.

from bunches import Catalog

models = Catalog(contents = {'tree': 'DecisionTree', 'forest': 'RandomForest'})
models['tree']  # 'DecisionTree'
models[['tree', 'forest']]  # ['DecisionTree', 'RandomForest']
models['all']  # ['DecisionTree', 'RandomForest']
models.default = 'forest'
models['default']  # 'RandomForest'
models['none'] is None  # True

ChainDict

A ChainDict searches its stored dictionaries in order and returns the first match. Set return_first to False to get every match.

from bunches import ChainDict, Dictionary

chain = ChainDict(contents = [Dictionary({'a': 1}), Dictionary({'a': 2, 'b': 3})])
chain['a'], chain['b']  # (1, 3)
chain.return_first = False
chain['a']  # [1, 2]
chain.new_child(Dictionary({'c': 4}))
chain.keys()  # ('c', 'a', 'a', 'b')

Repository

A Repository infers the key for each item added: the item itself if it is a str, its name attribute, or the snake-cased name of its class. Unless overwrite is True, a counter is appended to avoid overwriting existing items.

from dataclasses import dataclass
from bunches import Repository


@dataclass
class Worker:
    name: str


class DataLoader:
    pass


repository = Repository()
repository.add(Worker('ann'))
repository.add(DataLoader())
repository.add(DataLoader())
repository.add(Worker('bob'), key = 'boss')
repository.keys()  # ('ann', 'data_loader', 'data_loader2', 'boss')

Listing

A Listing works like a list. add extends the list with a sequence and appends anything else, and prepend does the same at the beginning.

from bunches import Listing

letters = Listing(contents = ['b', 'c'])
letters.add('d')
letters.add(['e', 'f'])
letters.prepend('a')
letters.contents  # ['a', 'b', 'c', 'd', 'e', 'f']
letters.subset(include = ['a', 'b', 'c'], exclude = 'b').contents  # ['a', 'c']
letters.delete(0)
letters[0]  # 'b'

DictList

A DictList is a list you can also search by name. Items are found by their name attribute or, if they have none, by their own value. Duplicate names are allowed; a DictList of the matches is returned.

from bunches import DictList

workers = DictList(contents = [Worker('ann'), Worker('bob'), Worker('ann')])
workers.keys()  # ('ann', 'bob', 'ann')
workers['bob']  # Worker(name='bob')
len(workers['ann'])  # 2
workers[1]  # Worker(name='bob')
workers.delete('ann')
workers.keys()  # ('bob',)

Choosing what subset returns

Every subset method has a returns argument. 'class' (the default) returns a new instance of the same class, 'copy' returns a deep copy of the instance (keeping its other settings) and 'simple' returns the built-in type. To change the default for the whole package, use bunches.settings.set_subset_return.

colors = Dictionary({'red': 1, 'green': 2})
colors.subset(include = 'red', returns = 'simple')  # {'red': 1}

Naming items

Repository and DictList name items with a global function that you can replace using bunches.settings.set_key_namer.

from bunches import settings

settings.set_key_namer(lambda item: str(item).upper())
Repository({'a': 1}).add('b')
DictList(['x', 'y']).keys()  # ('X', 'Y')
settings.set_key_namer(settings.utilities._namify)

Contributing

Contributors are always welcome. Feel free to grab an issue to work on or make a suggested improvement. If you wish to contribute, please read the Contribution Guide and Code of Conduct.

Similar Projects

  • collections in the Python standard library provides UserDict, UserList, ChainMap and defaultdict, which inspired several of the classes in bunches.
  • sortedcontainers provides sorted drop-in replacements for list, dict and set.

License

Use of this repository is authorized under the Apache Software License 2.0.