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

packages = \
['spelunk']

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

setup_kwargs = {
    'name': 'spelunk',
    'version': '0.1.6',
    'description': 'Package with helpful object recursion utils',
    'long_description': '# Spelunk\nSpelunk is a module containing tools for recursively exploring and manipulating python objects.\n\n\n## Installation\nTo install spelunk, simply install with `pip install spelunk`. See below for details on how to install \nthe project for development.\n\n\n## Overview\nThis section will review the major utilities of spelunk.\n### 1. Printing an object\'s tree\n```python\nfrom spelunk import print_obj_tree\n\nclass A:\n   def __init__(self):\n      self.val = \'val\'\n    \n   def __repr__(self):\n      return f\'A(val={self.val})\'\n \nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,), A()]}]}\n\nprint_obj_tree(root_obj=obj)\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=4315240816} -> 3\n# ROOT[\'key\'][3] -> frozenset({4})\n# ROOT[\'key\'][3]{id=4315240848} -> 4\n# ROOT[\'key\'][4] -> {\'subkey\': [(1,), A(val=val)]}\n# ROOT[\'key\'][4][\'subkey\'] -> [(1,), A(val=val)]\n# ROOT[\'key\'][4][\'subkey\'][0] -> (1,)\n# ROOT[\'key\'][4][\'subkey\'][0][0] -> 1\n# ROOT[\'key\'][4][\'subkey\'][1] -> A(val=val)\n# ROOT[\'key\'][4][\'subkey\'][1].val -> \'val\'\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\nNote that `ValuesView` may seem odd here. This is included separately because this is a unique subclass\nof `Collection` not captured by `Mapping`, `Sequence`, or `Set`. In contrast, both `KeysView` and \n`ItemsView` are subclasses of `Set`.\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 also possible by iterating and inspecting by id. To\naccess `4` via `ROOT[\'key\'][3]{id=4315240848}` 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) == 4315240848:\n      break\n      \nprint(elem)\n# 4\n  ```\n\nFortunately, for accessing 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 select 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 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,), A()]}]}\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,), A()]}]}\nprint_obj_tree(root_obj=obj, path_test=lambda x: x==\'subkey\')  \n\n# ROOT[\'key\'][4][\'subkey\'] -> [(1,), A(val=val)]\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,), A()]}]}\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,), A(val=val)]}]},\n#   "ROOT[\'key\'][4]": {\'subkey\': [(1,), A(val=val)]}\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,), A()]}]}\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, A(val=val)]}]}\n```\nObjects can also be overwritten using a callable `overwrite_func`. If `overwrite_func` is not `None` and \ncallable, `overwrite_value` will be ignored.\n```python\nfrom spelunk import overwrite_elements\n\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,), A()]}]}\noverwrite_elements(\n    root_obj=obj, \n    overwrite_func=str, \n    element_test=lambda x: isinstance(x, tuple)\n)\nprint(obj)\n\n# {\'key\': [1, \'(2.0,)\', {3}, frozenset({4}), {\'subkey\': [\'(1,)\', A(val=val)]}]}\n```\nOverwriting will fail if attempting to overwrite an immutable container.\n```python\nobj = {\'key\': [1, (2.0,), {3}, frozenset((4,)), {\'subkey\': [(1,), A()]}]}\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=4315240848}.\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,), A()]}]}\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,), A(val=val)]}]}\n```\n\n### 4. Hot swapping\nOne helpful utility is the ability to safely and reversibly "hot swap" certain elements of an object.\nOne use-case is writing a non-serializable object to JSON. Say we have some root object `root_obj`\nthat needs to be serialized to JSON but some of its constituent elements are not\nserializable. It may be tedious to go through and null/convert the\nnon-serializable content. Furthermore, we may not want to permanently overwrite the non-serializable\ncontent. One tool in spelunk is a context manager `hot_swap` that can find elements to arbitrary \nspecification and at any depth in the root object, overwrite their values, and then restore the originals.\n\n```python\nfrom spelunk import hot_swap\nimport json\nfrom datetime import datetime\nfrom threading import Lock\nfrom _thread import LockType\nfrom typing import Any, Optional, Union\n\n\nroot_obj = {\n    \'date\': datetime.now(), \n    \'thread_lock\': Lock(), \n    \'data\': [1, 2, 3, 4], \n    \'other_locks\': [Lock(), Lock()]\n}\n\nprint(root_obj)\n# {\n#   \'date\': datetime.datetime(2022, 11, 9, 13, 48, 19, 969856), \n#   \'thread_lock\': <unlocked _thread.lock object at 0x105ff4600>, \n#   \'data\': [1, 2, 3, 4], \n#   \'other_locks\': [\n#       <unlocked _thread.lock object at 0x105ff4630>, \n#       <unlocked _thread.lock object at 0x105ff4690>\n#   ]\n# }\n```\nNeither `datetime` nor `_thread.lock` objects are serializable.\n```python\njson.dumps(root_obj)\n# Traceback (most recent call last):\n# ...\n# TypeError: Object of type datetime is not JSON serializable\n```\nWe can define callables to both capture the bad elements (`get_datetime_and_locks`) as well as \noverwrite intelligently (`overwrite_func`).\n```python\ndef get_datetime_and_locks(obj: Any) -> bool:\n    """Get the non-serializable content"""\n    return isinstance(obj, (datetime, LockType))\n\ndef overwrite_func(obj: Union[datetime, LockType]) -> Optional[str]:\n    """Overwrite the non-serializable content"""\n    if isinstance(obj, datetime):\n        return str(obj)\n    else:\n        return None\n```\nNote that `overwrite_func` will only get called on objects for which `get_datetime_and_locks` returns\n`True`. Now, we can use our context manager `hot_swap` to temporarily overwrite the non-serializable\ncontent and then restore on exit.\n\n```python\nwith hot_swap(root_obj, element_test=get_datetime_and_locks, overwrite_func=overwrite_func):\n    serialized_obj = json.dumps(root_obj)\n\nprint(serialized_obj)\n# {\n#   "date": "2022-11-09 13:48:19.969856", \n#   "thread_lock": null, \n#   "data": [1, 2, 3, 4], \n#   "other_locks": [null, null]\n# }\n```\n`root_obj` is restored to its original form, allowing the datetime and thread lock objects to\ncontinue to provide utility with further use.\n\n```python\nprint(root_obj)\n# {\n#   \'date\': datetime.datetime(2022, 11, 9, 13, 48, 19, 969856), \n#   \'thread_lock\': <unlocked _thread.lock object at 0x105ff4600>, \n#   \'data\': [1, 2, 3, 4], \n#   \'other_locks\': [\n#       <unlocked _thread.lock object at 0x105ff4630>, \n#       <unlocked _thread.lock object at 0x105ff4690>\n#   ]\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\nSpelunk fully support objects that define `__slots__`, `__dict__`, as well as `__slots__` and `__dict__`\nsimultaneously). In order to deal with instance attributes derived from `__slots__` defined on the \nclass itself (or which may be inherited from parent classes), the MRO is used. For each class \nin the object\'s MRO, the contents of `cls.__slots__` is collected (along with the contents of `obj.__dict__` if \n`__dict__` is defined). For a given object, its attributes are collected as follows:\n- `attrs = []`\n- If `obj.__dict__` exists, add all elements to `attrs`. \n- For `cls` in `obj.__class__.__mro__`:\n  - If `cls.__slots__` exists, add all elements to `attrs`.\n\nNote in the special case that both `__slots__` and `__dict__` are defined (such that `__dict__` is \na member of `__slots__`), `__dict__` itself will be independently added as an attribute to `attrs`\nin addition to the contents of `__dict__`.\n\nNote that any attributes accessible to `obj` outside of `__dict__` (such as attributes of the class), \nare not included by spelunk. However, if one wants to inspect class attribute, the class itself can be \npassed in as the `root_obj`. Here, `__slots__` as well as all methods and other attributes of the class \nwill be collected and explored 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    \n    def __repr__(self):\n       return f"A(val={self.val})"\n\nprint_obj_tree(A(1))\n# ROOT -> A(val=1)\n# ROOT.other -> \'other\'\n# ROOT.__dict__ -> {\'other\': \'other\'}\n# ROOT.__dict__[\'other\'] -> \'other\'\n# ROOT.val -> 1\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__\', \'val\')\n# ROOT.__slots__[0] -> \'__dict__\'\n# ROOT.__slots__[1] -> \'val\'\n# ...\n```\n\n### Memoization\nSpelunk 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 singleton in memory in CPython.\n\n### String unraveling\nSpelunk 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\n1. Install an appropriate version of python and create a virtual environment. [Pyenv](https://github.com/pyenv/pyenv) is recommended.\n   1. Set the shell variable `VENV_LOC` according to the location of the virtural environment. For example, if\n   the virtual environment is inside the repo home directory in a directory named `.venv` you would run `export VENV_LOC=.venv` (this is used by default\n   and isn\'t needed unless the virtual environment is located elsewhere).\n2. Install [Poetry](https://python-poetry.org/).\n3. Run `make install-repo` to activate the virtual environment and install the dependencies with Poetry.\n\nIf you have a different package management system (e.g. `conda`):\n1. Create and source/activate a virtual environment.\n2. Either install using `Poetry` or use external tools to convert the `poetry.lock` file to a \n`requirements.txt` and install with `pip install -r requirements.txt`.\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 reformat` 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)
