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

packages = \
['spelunk']

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

setup_kwargs = {
    'name': 'spelunk',
    'version': '0.1.5',
    '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 \nthe 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, (2.0,), ...]}\n# ROOT[\'key\'] -> [1, (2.0,), ...]\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}`.\n(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 \nwe found a matching 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 \ntools that avoid 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 \nsupplying callables `element_test` and `path_test` that determine whether an element or path is \ninteresting (by default they always return True). `element_test` operates on the element itself and \nreturns a bool. `path_test` operates on the most recent branch of the current path (either \nstring for attributes/mapping keys or integers for sequences and sets) and returns a bool. For \nexample, if you\'re at `root_obj[\'key\']` with path `ROOT[\'key\']`, it would pass `key` to the input of\n`path_test` and `[1, (2,), ...]` to `element_test`.\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, \nuse `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 4 at ROOT[\'key\'][3]{id=4424907152}.\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=False` kwargs. Be aware that it may be difficult to determine which objects \nfailed with these options.\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 \nthis. As an example, say you had an object that contained threading locks, and you wanted to make a \ndeepcopy in order to manipulate the copy but preserve the original. The deepcopy will fail on the \noriginal object due to the fact that threading locks are not serializable. With `hot_swap`, you can \nsafely overwrite the non-serializable elements with something safe, perform the deepcopy, and then \nrestore 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... 0x104a7b870>, {3}, frozenset({4}), {\'subkey\': [(1,)]}], \n#  \'other_lock\': <unlocked _thread.lock... 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... 0x104a7b870>, {3}, frozenset({4}), {\'subkey\': [(1,)]}], \n#  \'other_lock\': <unlocked _thread.lock... 0x104a7b840>\n# }\n```\nIf performing a `hot_swap` on a `root_obj` throws an exception, an attempt to restore`root_obj` to \nits original form is made. Additionally, by default, it will throw an exception before any attempt \nto hot swap an element of a mutable set because this cannot be performed reliably. Imagine swapping \nall `int` for `None` in `{1, 2, 3, None}` -> `{None}`. It is then ambiguous to determine which \nelements of the new set should be restored. It would be possible to copy the set `{1, 2, 3, None}` \nand restore this to the parent object, however, this copy would not share the same location in \nmemory as the original and it may break internal references as a result. By default, hot swapping is\nnot allowed with mutable sets, however, if you know it can be performed safely you can use the kwarg \n`allow_mutable_set_mutations=True`. For example, the set `{1}` could be safely hot swapped to \n`{None}` and restored due to the fact that the cardinality is unchanged.\n\n## More Details\n### `__slots__` and other class attributes\n`spelunk` fully support objects that define `__slots__` (as well as `__dict__` simultaneously). In \norder to deal with slot attributes that may be inherited from parent classes, the object\'s MRO is \nqueried. For each parent class, the contents of `__slots__` is collected (along with the object\'s \n`__dict__` contents if `__dict__` is defined). Note that although we are accessing the class \nattribute `__slots__`, we don\'t collect the object `__slots__` itself since this belongs to the \nclass and not the instance. Additionally, the attribute `__dict__` itself and any attributes stored \noutside `obj.__dict__` are not collected. To be clear, the contents of `obj.__dict__` are collected, \njust not `__dict__` itself. The one exception is when `__dict__` is defined as a member of \n`__slots__` to support both slot attributes and dynamically assigned attributes. \n\nIf one wants to inspect a class attribute, the class itself can be passed in as the `root_obj`. \nHere, `__slots__` as well as all methods and other attributes of the class will be collected and \nexplored since these are direct attributes of the root object.\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 contains `__dict__`) and `__dict__` \nare captured but the class attribute `important` is not. However, the class itself can be \ninspected:\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` optionally utilizes memoization to increase performance and to prevent reporting multiple \npaths which point to the same object in memory. By default, memoization is not used in order to \nretrieve and output the full hierarchy of the object. Memoization can be turned on and off with the \nkwarg `memoization=True`. Note that some objects cannot be memoized regardless of whether \nmemoization is turned on. Namely, any subclass of `Number`, `str`, or `ByteString`  (along with \n`None`) will not be memoized due to the fact that members of these classes may be interned and \nall instances will always refer to the same item in memory in CPython.\n\n### String unraveling\n`spelunk` by default assumes that all subclasses of `str` or `ByteString` refer to an atomic \ncollection that should not be recursed into character by character. If you do want to recurse \ninto a `str` or `ByteString` instance, use the kwarg `unravel_strings=True`.\n\n## Developing\n### Project Installation\nIf you prefer using `pyenv` and `Poetry` (or have no preference), the `Makefile` provides \ninstallation support. Make sure `conda` is deactivated fully (not even `base` active) and `pyenv` is\nnot running a shell. \n1. Run `make install-python` to install `pyenv` (if not present) and then use `pyenv` to install the \nspecific 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 \nuse `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 \n`requirements.txt` and `pip install`.\n\n### Tests\nFor contributors, kindly use the `Makefile` to perform formatting, linting, and unit testing \nlocally.\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)
