# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['spelunk']

package_data = \
{'': ['*']}

setup_kwargs = {
    'name': 'spelunk',
    'version': '0.1.3',
    'description': 'Package with helpful object recursion utils',
    'long_description': '# spelunk\n`spelunk` is a module containing tools for recursively exploring python objects\n\n## Installation\n`spelunk` can be installed with `pip install spelunk`. See below for details on how to install the project for development.\n\n## Quick Use Guide\n### 1. Printing an object\'s tree\n\n\nEx:\n  ```python\nfrom spelunk import print_obj_tree\n  \nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\nprint_obj_tree(root_obj=obj)\n\n# ROOT -> {\'key\': [1, ...]}\n# ROOT[\'key\'] -> [1, ...]\n# ROOT[\'key\'][0] -> 1\n# ROOT[\'key\'][1] -> (2.0,)\n# ROOT[\'key\'][1][0] -> 2.0\n# ROOT[\'key\'][2] -> {3}\n# ROOT[\'key\'][2]{id=4431022448} -> 3\n# ROOT[\'key\'][3] -> frozenset({4})\n# ROOT[\'key\'][3]{id=4431022480} -> 4\n# ROOT[\'key\'][4] -> {\'subkey\': [(1,)]}\n# ROOT[\'key\'][4][\'subkey\'] -> [(1,)]\n# ROOT[\'key\'][4][\'subkey\'][0] -> (1,)\n# ROOT[\'key\'][4][\'subkey\'][0][0] -> 1\n  ```\n* The root object is referred to as `ROOT`. \n* Attributes are denoted with `ROOT.attr`.\n* Keys from mappings are denoted with `ROOT[\'key\']`.\n* Indices from sequences are denoted with `ROOT[idx]`.\n* Elements of sets and frozensets are indicated by their id in memory with `ROOT{id=10012}`. \n* Elements of a `ValuesView` are indicated by their id in memory with `ROOT{ValuesView_id=10012}`. (These are not common.)\n\nThe previous notations will be recursively chained together. For example, the path \n`ROOT[\'key\'][2]` indicates that in order to access the corresponding object `{3}`, we would \nuse `root_obj[\'key\'][2]`. For sets it is a bit more difficult due to the need to inspect by id. To\naccess `4` via `ROOT[\'key\'][3]{id=4431022480}` we would iterate through `root_obj[\'key\'][3]` until we found a\nmatching id:\n  ```python\nfor elem in root_obj[\'key\'][3]:\n    if id(elem) == 4431022480:\n      break\n      \nprint(elem)\n# 4\n  ```\n\nFortunately, for getting references and manipulating elements of `root_obj`, there are additional tools that \navoid needing to tediously address and iterate (see below). \n\n\nBefore moving on, it\'s worth pointing out you can also sort by element and/or by path name by supplying \ncallables `element_test` and `path_test` that determine whether an element or path is interesting \n(by default they always return True). `element_test` operates on the element itself and returns a bool. \n`path_test` operates on either the most recent string (for attributes, mapping keys) or integer \n(for sequence indices, memory ids of element of sets) of the current path and returns a bool.\nFor example, if you\'re at `root_obj[\'key\']` with path `ROOT[\'key\']`, it would pass `key` to the input of `path_test`\nand `[1, (2,), ...]` to `element_path`.\n\n  ```python\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\nprint_obj_tree(root_obj=obj, element_test=lambda x: isinstance(x, float))\n\n# ROOT[\'key\'][1][0] -> 2.0\n  ```\n  ```python\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\nprint_obj_tree(root_obj=obj, path_test=lambda x: x==\'subkey\')  \n\n# ROOT[\'key\'][4][\'subkey\'] -> [(1,)]\n  ```\n\n### 2. Getting the values and paths of objects\nTo get a dictionary of objects filtered by element/path and keyed by full path string, use `get_elements`:\n```python\nfrom spelunk import get_elements\n  \nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\nget_elements(root_obj=obj, element_test=lambda x: isinstance(x, frozenset))\n\n# {"ROOT[\'key\'][3]": frozenset({4})}\n\nget_elements(root_obj=obj, element_test=lambda x: isinstance(x, dict))\n# {\n#   \'ROOT\':           {\'key\': [1, (2.0,), {3}, frozenset({4}), {\'subkey\': [(1,)]}]}, \n#   "ROOT[\'key\'][4]": {\'subkey\': [(1,)]}\n# }\n```\n\n### 3. Overwriting elements \nTo overwrite elements use `overwrite_elements`:\n```python\nfrom spelunk import overwrite_elements\n\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\noverwrite_elements(\n    root_obj=obj, \n    overwrite_value=None, \n    element_test=lambda x: isinstance(x, tuple)\n)\nprint(obj)\n\n# {\'key\': [1, None, {3}, frozenset({4}), {\'subkey\': [None]}]}\n```\nOverwriting will fail if attempting to overwrite an immutable container. \n\n\nEx: \n```python\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\noverwrite_elements(\n    root_obj=obj, \n    overwrite_value=None, \n    element_test=lambda x: isinstance(x, int)\n)\nprint(obj)\n\n# Failed to overwrite [(<Address.MUTABLE_MAPPING_KEY: \'MutableMappingKey\'>, ...\n# Exception: Cannot overwrite immutable collections.\n# Traceback (most recent call last):\n# ...\n# TypeError: Cannot overwrite immutable collections.\n```\nError messages can be silenced with `silent=True` and exceptions can be dismissed with \n`raise_on_exception`.\n```python\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,)]}]}\noverwrite_elements(\n    root_obj=obj, \n    overwrite_value=None, \n    element_test=lambda x: isinstance(x, int),\n    silent=True,\n    raise_on_exception=False\n)\nprint(obj)\n\n# {\'key\': [None, (2.0,), {None}, frozenset({4}), {\'subkey\': [(1,)]}]}\n```\n\n### 4. Hot swapping\nIf you need to temporarily overwrite an object\'s contents with replacement \nvalues and then restore the original values, there is a context manager `hot_swap` that achieves this. \nAs an example, say you had an object that contained threading locks and you wanted to make a deepcopy in \norder to manipulate but preserve the original. The deepcopy will fail on the original object due to the fact \nthat thread locks are not serializable. With `hot_swap`, you can safely overwrite the non-serializable elements\nwith something safe, perform the deepcopy, and then restore the original elements.\n\n```python\nfrom spelunk import hot_swap\nfrom _thread import LockType\nfrom threading import Lock\nfrom copy import deepcopy\n\nlock_0 = Lock()\nlock_1 = Lock()\nobj = {\'key\': [1, lock_0, {3}, frozenset((4,)), {\'subkey\': [(1,)]}], \'other_lock\': lock_1}\n\nprint(obj)\n# {\n#   \'key\': [1, <unlocked _thread.lock object at 0x104a7b870>, {3}, frozenset({4}), {\'subkey\': [(1,)]}], \n#  \'other_lock\': <unlocked _thread.lock object at 0x104a7b840>\n# }\n\nobj_deepcopy = deepcopy(obj)\n# Traceback (most recent call last):\n# ...\n# TypeError: cannot pickle \'_thread.lock\' object\n\nwith hot_swap(root_obj=obj, overwrite_value=\'lock\', element_test=lambda x: isinstance(x, LockType)):\n    obj_deepcopy = deepcopy(obj)\n\nprint(obj_deepcopy)\n# {\'key\': [1, \'lock\', {3}, frozenset({4}), {\'subkey\': [(1,)]}], \'other_lock\': \'lock\'}\n\nprint(obj)\n# {\n#   \'key\': [1, <unlocked _thread.lock object at 0x104a7b870>, {3}, frozenset({4}), {\'subkey\': [(1,)]}], \n#  \'other_lock\': <unlocked _thread.lock object at 0x104a7b840>\n# }\n```\nIf performing a `hot_swap` on a `root_obj` would involve attempting to mutate an immutable collection, an exception\nwill be thrown before any modifications occur (even legal mutations) to leave `root_obj` unchanged. \nAdditionally, by default, it will throw an exception before any attempt to hot swap an element of a mutable set because \nthis cannot be performed reliably. Imagine swapping all `int` for `None` in `{1, 2, 3, None}` -> `{None}`. It is then ambiguous to determine which \nelements of the new set should be restored. By default, hot swapping is not allowed with sets, however,\nif you know it can be performed safely you can use the flag `allow_mutable_set_mutations`. For example,\nthe set `{1}` could be safely hot swapped to `{None}` and restored due to the fact that the cardinality is unchanged.\n\n## More Details\n### `__slots__`\n`spelunk` fully support objects that define `__slots__` (as well as `__dict__` simultaneously). For each\nobject that isn\'t an ignored type or an instance of a `Collection`, the object\'s MRO is looked up and \neach parent class is queried for possible contents of `__slots__` in order to capture those from inherited classes. \nThese attributes are collected together (along with the contents of the instance\'s `obj.__dict__`). Note that \nalthough we search for `__slots__` (a class attribute), we do not include the object `__slots__` in our exploration \nbecause this is a class attribute, not an instance attribute. This changes if we pass a class `cls` as `root_obj`. Here,\n`cls.__dict__` contains all of the attached methods and class attributes (including `__slots__` and the content within).\nHere, we never inherit `__slots__` contents from parent attributes because for any class `cls`, `cls.__class__` is `type` \nand `type.__mro__` is `(<class \'type\'>, <class \'object\'>)`. Neither `type` nor `object` define `__slots__`.\n\n\nEx:\n```python\nfrom spelunk import print_obj_tree\n\nclass A:\n    important = "important"\n    __slots__ = \'__dict__\', \'val\'\n    def __init__(self, val):\n        self.val = val\n        self.other = \'other\'\n\nprint_obj_tree(A(1))\n# ROOT -> <__main__.A object at 0x10a3dcdc0>\n# ROOT.other -> \'other\'\n# ROOT.__dict__ -> {\'other\': \'other\'}\n# ROOT.__dict__[\'other\'] -> \'other\'\n# ROOT.val -> 1\n# ...\n```\nWe can see that both the contents of `__slots__` (which containts `__dict__`) and `__dict__` attributes are captured but the \nclass attribute `important` is not. However, the class itself can be inspected:\n```python\nprint_obj_tree(A)\n# ROOT -> <class \'__main__.A\'>\n# ROOT.__module__ -> \'__main__\'\n# ROOT.important -> \'important\'\n# ROOT.__slots__ -> (\'__dict__\', ...)\n# ROOT.__slots__[0] -> \'__dict__\'\n# ROOT.__slots__[1] -> \'val\'\n# ...\n```\n\n### Memoization\n`spelunk` utilizes memoization by caching previously seen objects in a memoization dictionary during searches. It will not print new paths for \nobjects which refer to the same place in memory. This is not only important for speed but also to prevent potential infinite recursive loops. There\nis one important class of exceptions. In CPython, certain types of objects always share the same memory location (e.g. certain integers, strings)\nregardless of how they\'re initialized. For a conservative approach, all instances of `(Number, str, ByteString)` are prevented from caching\nso that each object\'s path is memorialized.\n\n### Ignored Collections\n`spelunk` intentionally ignores `Collections` that are instances of `(str, ByteString)`. This prevents string-like objects from being broken down by char which is usually not the preferred behavior.\n\n## Developing\n### Project Installation\nIf you prefer using `pyenv` and `Poetry` (or have no preference), the `Makefile` provides installation support. Make sure `conda` is deactivated fully (not even `base` active) and `pyenv` is not running a shell. \n1. Run `make install-python` to install `pyenv` (if not present) and then use `pyenv` to install the specific version of `python`.\n2. Run `make install-poetry` to install `Poetry` if not already present. \n3. Run `make install-repo` to create a virtual environment `spelunk` stored in `spelunk/.venv` and use `Poetry` to install all dependencies.\n4. To use the environment simply run `source .venv/bin/activate`.\n5. To deactivate simply run `deactivate`.\n\nIf you have a different package management system:\n1. Create a virtual environment.\n2. Either install using `Poetry` or use external tools to convert `poetry.lock` to a `requirements.txt` and `pip install`.\n\n### Tests\nFor contributors, kindly use the `Makefile` to perform formatting, linting, and unit testing locally.\n1. Run `make style-check` to dry-run `black` formatting changes.\n2. Run `make format` to format with `black`.\n3. Run `make lint` to lint with `flake8`.\n4. Run `make unit-test` to run `pytest` and check the coverage report. \n',
    'author': 'Spencer Tomarken',
    'author_email': 'stomarken@gmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/tomarken/spelunk',
    'packages': packages,
    'package_data': package_data,
    'python_requires': '>=3.9,<3.10',
}


setup(**setup_kwargs)
