[
  {
    "path": ".github/tag-changelog-config.js",
    "content": "module.exports = {\n  types: [\n    { types: [\"other\"], label: \"Commits\" },\n  ],\n\n  renderTypeSection: function (label, commits) {\n    let text = `\\n## ${label}\\n`;\n\n    commits.forEach((commit) => {\n      text += `- ${commit.subject}\\n`;\n    });\n\n    return text;\n  },\n\n  renderChangelog: function (release, changes) {\n    const now = new Date();\n    return `# ${release} - ${now.toISOString().substr(0, 10)}\\n` + changes + \"\\n\\n\";\n  },\n};\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy and Release\n\n# Controls when the workflow will run\non:\n  # Triggers the workflow on version change\n  push:\n    branches: \n      - master\n    paths:\n      - dpath/version.py\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n  # This workflow contains a single job called \"deploy\"\n  deploy:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it\n      - uses: actions/checkout@v2\n\n      - name: Get Version\n        id: get-version\n        run: |\n          python -c \"from dpath.version import VERSION; print(f'::set-output name=version::v{VERSION}');\"\n\n      - name: Check Tag\n        uses: mukunku/tag-exists-action@v1.0.0\n        id: check-tag\n        with:\n          tag: ${{ steps.get-version.outputs.version }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Create Tag\n        if: steps.check-tag.outputs.exists == 'false'\n        uses: negz/create-tag@v1\n        with:\n          version: ${{ steps.get-version.outputs.version }}\n          token: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Generate Changelog\n        id: generate-changelog\n        uses: loopwerk/tag-changelog@v1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          config_file: .github/tag-changelog-config.js\n\n      - name: PyPI Deployment\n        uses: casperdcl/deploy-pypi@v2\n        with:\n          # PyPI username\n          user: ${{ secrets.PYPI_USER }}\n          # PyPI password or API token\n          password: ${{ secrets.PYPI_PASS }}\n          # `setup.py` command to run (\"true\" is a shortcut for \"clean sdist -d <dist_dir> bdist_wheel -d <dist_dir>\")\n          build: clean sdist -d dist/\n          # `pip` command to run (\"true\" is a shortcut for \"wheel -w <dist_dir> --no-deps .\")\n          pip: true\n\n      - name: Github Release\n        uses: softprops/action-gh-release@v1\n        with:\n          tag_name: ${{ steps.get-version.outputs.version }}\n          body: ${{ steps.generate-changelog.outputs.changes }}\n          files: dist/*\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Run tests\n\n# Controls when the workflow will run\non:\n  # Triggers the workflow on push or pull request events but only for important files\n  push:\n    branches:\n      - master\n    paths:\n      - \"dpath/\"\n      - \"**.py\"\n      - \"tox.ini\"\n  pull_request:\n    paths:\n      - \"dpath/\"\n      - \"**.py\"\n      - \"tox.ini\"\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# A workflow run is made up of one or more jobs that can run sequentially or in parallel\njobs:\n\n  # Run flake8 linter\n  flake8:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out code\n        uses: actions/checkout@main\n\n      - name: Set up Python 3.12\n        uses: actions/setup-python@main\n        with:\n          python-version: \"3.12\"\n\n      - name: Setup flake8 annotations\n        uses: TrueBrain/actions-flake8@v2.3\n        with:\n          path: setup.py dpath/ tests/\n\n  # Generate a common hashseed for all tests\n  generate-hashseed:\n    runs-on: ubuntu-latest\n\n    outputs:\n      hashseed: ${{ steps.generate.outputs.hashseed }}\n\n    steps:\n      - name: Generate Hashseed\n        id: generate\n        run: |\n          python -c \"import os\n          from random import randint\n          hashseed = randint(0, 4294967295)\n          print(f'{hashseed=}')\n          open(os.environ['GITHUB_OUTPUT'], 'a').write(f'hashseed={hashseed}')\"\n\n  # Tests job\n  tests:\n    # The type of runner that the job will run on\n    runs-on: ubuntu-latest\n\n    needs: [generate-hashseed, flake8]\n\n    strategy:\n      matrix:\n        # Match versions specified in tox.ini\n        python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.7', '3.12']\n\n    # Steps represent a sequence of tasks that will be executed as part of the job\n    steps:\n      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it\n    - name: Check out code\n      uses: actions/checkout@main\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@main\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Run tox with tox-gh-actions\n      uses: ymyzk/run-tox-gh-actions@main\n      with:\n        tox-args: -vv --hashseed=${{ needs.generate-hashseed.outputs.hashseed }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/MANIFEST\n/.tox\n/build\n/env\n.hypothesis\n*.pyc\n.vscode\nvenv_39\n.idea/\ndpath.egg-info/\ndist/\ntests/.hypothesis"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Andrew Kesterson <andrew@aklabs.net>, Caleb Case <caleb.case@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "Who Maintains DPATH\n===================\n\ndpath was created by and originally maintained by Andrew Kesterson <andrew@aklabs.net> and Caleb Case <calebcase@gmail.com>. In July\nof 2020 they put out a call for new maintainers. [@bigsablept](https://github.com/bigsablept) and \n[@moomoohk](https://github.com/moomoohk) stepped up to become the new maintainers.\n\nThere are several individuals in the community who have taken an active role in helping to maintain the project and submit fixes. Those individuals are shown in the git changelog.\n\nWhere and How do we communicate\n===============================\n\nThe dpath maintainers communicate in 3 primary ways:\n\n1. Email, directly to each other.\n2. Github via issue and pull request comments\n3. A monthly maintainers meeting via Zoom\n\nThe remainder of this document is subject to change after further discussion among the new maintainers.\n\nWhat is the roadmap\n===================\n\ndpath has 3 major series: 1.x, 2.x, and 3.x.\n\n1.x is the original dpath release from way way back. It has a util library with a C-like calling convention, lots of assumptions about how it would be used (it was built originally to solve a somewhat narrow use case), and very bad unicode support.\n\n2.x is a transitional branch that intends to fix the unicode support and to introduce some newer concepts (such as the segments library) while still being backwards compatible with 1.x.\n\n3.x is a total reconstruction of the library that does not guarantee backwards compatibility with 1.x.\n\nFinding and Prioritizing Work\n=============================\n\nThere are GitHub project boards which show the work to be done for a given series:\n\nhttps://github.com/akesterson/dpath-python/projects/\n\nEach series has a board with 4 columns:\n\n* Backlog. New work for this series appears here.\n* To Do. This column represents work that has been prioritized and someone has agreed to do the work when they have an available time slot. Each maintainer should never have more than 1 or 2 things in To Do.\n* In Progress. Maintainers are actively working on these issues.\n* Done. These issues have been recently completed.\n\nWork is prioritized depending on:\n\n1. The type of work. Bugs almost always get worked before features.\n2. The versions impacted by the work. Versions which are already in use get worked first (so 1.x before 2.x before 3.x etc)\n3. The relative importance/usefulness of the work. \"Really useful\" tends to get worked before \"nice to have\".\n4. The amount of time to complete the work. Quick issues tend to get worked sooner than issues that will take a long time to resolve.\n\nThere is no specific SLA around dpath, for features or bugs. However, generally speaking:\n\n* All issues get triaged within 1 calendar month\n* High priority bugs get addressed on the monthly maintainers call\n* Very severe bugs are often fixed out of cycle in less than 30 days\n\nNote that we have not always had anything remotely resembling a rigorous process around this, so there are some bugs that have lingered for several years. This is not something we intend to repeat.\n\nTaking and Completing Work\n==========================\n\nAnyone who wants to is welcome to submit a pull request against a given issue. You do not need any special maintainer permissions to say \"hey, I know how to solve that, let me send up a PR\".\n\nThe more complete process goes:\n\n1. Decide what issue(s) you will be working on\n2. On the Projects tab on Github, move those items to the To Do column on the appropriate board\n3. For the item you are ACTIVELY WORKING, move that item to \"In Progress\"\n4. Create a fork of dpath-python, and name your branch for the work. We name bugfixes as \"bugfix/ISSUENUMBER_shortname\"; features are named \"feature/ISSUENUMBER_shortname\".\n5. Complete and push your work on your fork. Use tox to test your work against the test suites. Features MUST ship with at least one new unit test that covers the new functionality. Bugfixes MUST ship with one new test (or an updated old test) that guards against regression.\n6. Send your pull request\n7. If accepted, the maintainers will merge your pull request and close the issue.\n\nBranching Strategy\n==================\n\nWe run a clean bleeding edge master. Long term support for major version numbers are broken out into version branches.\n\n* master : Current 3.x (bleeding edge) development\n* version/1.x : 1.x series bugfixes\n* version/2.x : 2.x series features and bugfixes\n\nWe name bugfixes as \"bugfix/ISSUENUMBER_shortname\"; features are named \"feature/ISSUENUMBER_shortname\". All branches representing work against an issue must have the issue number in the branch name.\n\nCutting a New Release\n=====================\n\nReleases for dpath occur automatically from Github Actions based on version changes on the master branch.\n\nDue to legacy reasons older tag names do not follow a uniform format:\n\n    akesterson@akesterson:~/dpath-python$ git tag\n    1.0-0\n    1.1\n    1.2-66\n    1.2-68\n    1.2-70\n    build,1.2,70\n    build,1.2,71\n    build,1.2,72\n    build,1.3,0\n    build,1.3,1\n    build,1.3,2\n    build,1.3,3\n    build,1.4,0\n    build,1.4,1\n    build,1.4,3\n    build,1.5,0\n    build,2.0,0\n\nMoving forward version numbers and tag names will be identical and follow the standard semver format.\n\nThe version string is stored in `dpath/version.py` and tag names/release versions are generated using this string.\n\n    akesterson@akesterson:~/dpath-python$ cat dpath/version.py\n    VERSION = \"2.0.0\"\n\nTo cut a new release, follow this procedure:\n\n1. Commit a new `dpath/version.py` on the appropriate branch with the format \"MAJOR.MINOR.RELEASE\".\n2. Github Actions SHOULD push the new release to PyPI on merge to `master`.\n\nSee `.github/workflows/deploy.yml` for more information.\n\nIf the Github workflow fails to update pypi, follow the instructions on manually creating a release, here:\n\nhttps://packaging.python.org/tutorials/packaging-projects/#uploading-the-distribution-archives\n\nDeployment CI was previously implemented using [Travis CI](https://travis-ci.org/github/akesterson/dpath-python).\n\nRunning Tests\n=============\n\nTests are managed using [tox](https://tox.readthedocs.io/en/latest/).\n\nEnvironment creation and dependency installation is managed by this tool, all one has to do is install it with `pip` and run `tox` in this repo's root directory.\n\nTests can also be run with Github Actions via the [tests.yml](https://github.com/dpath-maintainers/dpath-python/actions/workflows/tests.yml) workflow.\n\nThis workflow will run automatically on pretty much any commit to any branch of this repo but manual runs are also available.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.md\ninclude README.rst\nrecursive-include tests *\n"
  },
  {
    "path": "README.rst",
    "content": "dpath-python\n============\n\n|PyPI|\n|Python Version|\n|Build Status|\n|Gitter|\n\nA python library for accessing and searching dictionaries via\n/slashed/paths ala xpath\n\nBasically it lets you glob over a dictionary as if it were a filesystem.\nIt allows you to specify globs (ala the bash eglob syntax, through some\nadvanced fnmatch.fnmatch magic) to access dictionary elements, and\nprovides some facility for filtering those results.\n\nsdists are available on pypi: http://pypi.python.org/pypi/dpath\n\nInstalling\n==========\n\nThe best way to install dpath is via easy\\_install or pip.\n\n::\n\n    easy_install dpath\n    pip install dpath\n\nUsing Dpath\n===========\n\n.. code-block:: python\n\n    import dpath\n\nSeparators\n==========\n\nAll of the functions in this library (except 'merge') accept a\n'separator' argument, which is the character that should separate path\ncomponents. The default is '/', but you can set it to whatever you want.\n\nSearching\n=========\n\nSuppose we have a dictionary like this:\n\n.. code-block:: python\n\n    x = {\n        \"a\": {\n            \"b\": {\n                \"3\": 2,\n                \"43\": 30,\n                \"c\": [],\n                \"d\": ['red', 'buggy', 'bumpers'],\n            }\n        }\n    }\n\n... And we want to ask a simple question, like \"Get me the value of the\nkey '43' in the 'b' hash which is in the 'a' hash\". That's easy.\n\n.. code-block:: pycon\n\n    >>> help(dpath.get)\n    Help on function get in module dpath:\n\n    get(obj, glob, separator='/')\n        Given an object which contains only one possible match for the given glob,\n        return the value for the leaf matching the given glob.\n\n        If more than one leaf matches the glob, ValueError is raised. If the glob is\n        not found, KeyError is raised.\n\n    >>> dpath.get(x, '/a/b/43')\n    30\n\nOr you could say \"Give me a new dictionary with the values of all\nelements in ``x['a']['b']`` where the key is equal to the glob ``'[cd]'``. Okay.\n\n.. code-block:: pycon\n\n    >>> help(dpath.search)\n    Help on function search in module dpath:\n\n    search(obj, glob, yielded=False)\n    Given a path glob, return a dictionary containing all keys\n    that matched the given glob.\n\n    If 'yielded' is true, then a dictionary will not be returned.\n    Instead tuples will be yielded in the form of (path, value) for\n    every element in the document that matched the glob.\n\n... Sounds easy!\n\n.. code-block:: pycon\n\n    >>> result = dpath.search(x, \"a/b/[cd]\")\n    >>> print(json.dumps(result, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"c\": [],\n                \"d\": [\n                    \"red\",\n                    \"buggy\",\n                    \"bumpers\"\n                ]\n            }\n        }\n    }\n\n... Wow that was easy. What if I want to iterate over the results, and\nnot get a merged view?\n\n.. code-block:: pycon\n\n    >>> for x in dpath.search(x, \"a/b/[cd]\", yielded=True): print(x)\n    ...\n    ('a/b/c', [])\n    ('a/b/d', ['red', 'buggy', 'bumpers'])\n\n... Or what if I want to just get all the values back for the glob? I\ndon't care about the paths they were found at:\n\n.. code-block:: pycon\n\n    >>> help(dpath.values)\n    Help on function values in module dpath:\n\n    values(obj, glob, separator='/', afilter=None, dirs=True)\n    Given an object and a path glob, return an array of all values which match\n    the glob. The arguments to this function are identical to those of search(),\n    and it is primarily a shorthand for a list comprehension over a yielded\n    search call.\n\n    >>> dpath.values(x, '/a/b/d/*')\n    ['red', 'buggy', 'bumpers']\n\nExample: Setting existing keys\n==============================\n\nLet's use that same dictionary, and set keys like 'a/b/[cd]' to the\nvalue 'Waffles'.\n\n.. code-block:: pycon\n\n    >>> help(dpath.set)\n    Help on function set in module dpath:\n\n    set(obj, glob, value)\n    Given a path glob, set all existing elements in the document\n    to the given value. Returns the number of elements changed.\n\n    >>> dpath.set(x, 'a/b/[cd]', 'Waffles')\n    2\n    >>> print(json.dumps(x, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"3\": 2,\n                \"43\": 30,\n                \"c\": \"Waffles\",\n                \"d\": \"Waffles\"\n            }\n        }\n    }\n\nExample: Adding new keys\n========================\n\nLet's make a new key with the path 'a/b/e/f/g', set it to \"Roffle\". This\nbehaves like 'mkdir -p' in that it makes all the intermediate paths\nnecessary to get to the terminus.\n\n.. code-block:: pycon\n\n    >>> help(dpath.new)\n    Help on function new in module dpath:\n\n    new(obj, path, value)\n    Set the element at the terminus of path to value, and create\n    it if it does not exist (as opposed to 'set' that can only\n    change existing keys).\n\n    path will NOT be treated like a glob. If it has globbing\n    characters in it, they will become part of the resulting\n    keys\n\n    >>> dpath.new(x, 'a/b/e/f/g', \"Roffle\")\n    >>> print(json.dumps(x, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"3\": 2,\n                \"43\": 30,\n                \"c\": \"Waffles\",\n                \"d\": \"Waffles\",\n                \"e\": {\n                    \"f\": {\n                        \"g\": \"Roffle\"\n                    }\n                }\n            }\n        }\n    }\n\nThis works the way we expect with lists, as well. If you have a list\nobject and set index 10 of that list object, it will grow the list\nobject with None entries in order to make it big enough:\n\n.. code-block:: pycon\n\n    >>> dpath.new(x, 'a/b/e/f/h', [])\n    >>> dpath.new(x, 'a/b/e/f/h/13', 'Wow this is a big array, it sure is lonely in here by myself')\n    >>> print(json.dumps(x, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"3\": 2,\n                \"43\": 30,\n                \"c\": \"Waffles\",\n                \"d\": \"Waffles\",\n                \"e\": {\n                    \"f\": {\n                        \"g\": \"Roffle\",\n                        \"h\": [\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            \"Wow this is a big array, it sure is lonely in here by myself\"\n                        ]\n                    }\n                }\n            }\n        }\n    }\n\nHandy!\n\nExample: Deleting Existing Keys\n===============================\n\nTo delete keys in an object, use dpath.delete, which accepts the same globbing syntax as the other methods.\n\n.. code-block:: pycon\n\n    >>> help(dpath.delete)\n\n    delete(obj, glob, separator='/', afilter=None):\n        Given a path glob, delete all elements that match the glob.\n\n        Returns the number of deleted objects. Raises PathNotFound if\n        no paths are found to delete.\n\nExample: Merging\n================\n\nAlso, check out dpath.merge. The python dict update() method is\ngreat and all but doesn't handle merging dictionaries deeply. This one\ndoes.\n\n.. code-block:: pycon\n\n    >>> help(dpath.merge)\n    Help on function merge in module dpath:\n\n    merge(dst, src, afilter=None, flags=4, _path='')\n        Merge source into destination. Like dict.update() but performs\n        deep merging.\n\n        flags is an OR'ed combination of MergeType enum members.\n            * ADDITIVE : List objects are combined onto one long\n              list (NOT a set). This is the default flag.\n            * REPLACE : Instead of combining list objects, when\n              2 list objects are at an equal depth of merge, replace\n              the destination with the source.\n            * TYPESAFE : When 2 keys at equal levels are of different\n              types, raise a TypeError exception. By default, the source\n              replaces the destination in this situation.\n\n    >>> y = {'a': {'b': { 'e': {'f': {'h': [None, 0, 1, None, 13, 14]}}}, 'c': 'RoffleWaffles'}}\n    >>> print(json.dumps(y, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"e\": {\n                    \"f\": {\n                        \"h\": [\n                            null,\n                            0,\n                            1,\n                            null,\n                            13,\n                            14\n                        ]\n                    }\n                }\n            },\n            \"c\": \"RoffleWaffles\"\n        }\n    }\n    >>> dpath.merge(x, y)\n    >>> print(json.dumps(x, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"3\": 2,\n                \"43\": 30,\n                \"c\": \"Waffles\",\n                \"d\": \"Waffles\",\n                \"e\": {\n                    \"f\": {\n                        \"g\": \"Roffle\",\n                        \"h\": [\n                            null,\n                            0,\n                            1,\n                            null,\n                            13,\n                            14,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            null,\n                            \"Wow this is a big array, it sure is lonely in here by myself\"\n                        ]\n                    }\n                }\n            },\n            \"c\": \"RoffleWaffles\"\n        }\n    }\n\nNow that's handy. You shouldn't try to use this as a replacement for the\ndeepcopy method, however - while merge does create new dict and list\nobjects inside the target, the terminus objects (strings and ints) are\nnot copied, they are just re-referenced in the merged object.\n\nFiltering\n=========\n\nAll of the methods in this library (except new()) support a 'afilter'\nargument. This can be set to a function that will return True or False\nto say 'yes include that value in my result set' or 'no don't include\nit'.\n\nFiltering functions receive every terminus node in a search - e.g.,\nanything that is not a dict or a list, at the very end of the path. For\neach value, they return True to include that value in the result set, or\nFalse to exclude it.\n\nConsider this example. Given the source dictionary, we want to find ALL\nkeys inside it, but we only really want the ones that contain \"ffle\" in\nthem:\n\n.. code-block:: pycon\n\n    >>> print(json.dumps(x, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"3\": 2,\n                \"43\": 30,\n                \"c\": \"Waffles\",\n                \"d\": \"Waffles\",\n                \"e\": {\n                    \"f\": {\n                        \"g\": \"Roffle\"\n                    }\n                }\n            }\n        }\n    }\n    >>> def afilter(x):\n    ...     if \"ffle\" in str(x):\n    ...             return True\n    ...     return False\n    ...\n    >>> result = dpath.search(x, '**', afilter=afilter)\n    >>> print(json.dumps(result, indent=4, sort_keys=True))\n    {\n        \"a\": {\n            \"b\": {\n                \"c\": \"Waffles\",\n                \"d\": \"Waffles\",\n                \"e\": {\n                    \"f\": {\n                      \"g\": \"Roffle\"\n                    }\n                }\n            }\n        }\n    }\n\nObviously filtering functions can perform more advanced tests (regular\nexpressions, etc etc).\n\nKey Names\n=========\n\nBy default, dpath only understands dictionary keys that are integers or\nstrings. String keys must be non-empty. You can change this behavior by\nsetting a library-wide dpath option:\n\n.. code-block:: python\n\n    import dpath.options\n    dpath.options.ALLOW_EMPTY_STRING_KEYS = True\n\nAgain, by default, this behavior is OFF, and empty string keys will\nresult in ``dpath.exceptions.InvalidKeyName`` being thrown.\n\nSeparator got you down? Use lists as paths\n==========================================\n\nThe default behavior in dpath is to assume that the path given is a string, which must be tokenized by splitting at the separator to yield a distinct set of path components against which dictionary keys can be individually glob tested. However, this presents a problem when you want to use paths that have a separator in their name; the tokenizer cannot properly understand what you mean by '/a/b/c' if it is possible for '/' to exist as a valid character in a key name.\n\nTo get around this, you can sidestep the whole \"filesystem path\" style, and abandon the separator entirely, by using lists as paths. All of the methods in dpath.* support the use of a list instead of a string as a path. So for example:\n\n.. code-block:: python\n\n   >>> x = { 'a': {'b/c': 0}}\n   >>> dpath.get(['a', 'b/c'])\n   0\n\ndpath.segments : The Low-Level Backend\n======================================\n\ndpath is where you want to spend your time: this library has the friendly\nfunctions that will understand simple string globs, afilter functions, etc.\n\ndpath.segments is the backend pathing library. It passes around tuples of path\ncomponents instead of string globs.\n\n.. |PyPI| image:: https://img.shields.io/pypi/v/dpath.svg?style=flat\n    :target: https://pypi.python.org/pypi/dpath/\n    :alt: PyPI: Latest Version\n\n.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dpath?style=flat\n    :target: https://pypi.python.org/pypi/dpath/\n    :alt: Supported Python Version\n\n.. |Build Status| image:: https://github.com/dpath-maintainers/dpath-python/actions/workflows/tests.yml/badge.svg\n    :target: https://github.com/dpath-maintainers/dpath-python/actions/workflows/tests.yml\n   \n.. |Gitter| image:: https://badges.gitter.im/dpath-python/chat.svg\n    :target: https://gitter.im/dpath-python/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge\n    :alt: Gitter\n\nContributors\n============\n\nWe would like to thank the community for their interest and involvement. You\nhave all made this project significantly better than the sum of its parts, and\nyour continued feedback makes it better every day. Thank you so much!\n\nThe following authors have contributed to this project, in varying capacities:\n\n+ Caleb Case <calebcase@gmail.com>\n+ Andrew Kesterson <andrew@aklabs.net>\n+ Marc Abramowitz <marc@marc-abramowitz.com>\n+ Richard Han <xhh2a@berkeley.edu>\n+ Stanislav Ochotnicky <sochotnicky@redhat.com>\n+ Misja Hoebe <misja@conversify.com>\n+ Gagandeep Singh <gagandeep.2020@gmail.com>\n+ Alan Gibson <alan.gibson@gmail.com>\n\nAnd many others! If we've missed you please open an PR and add your name here.\n"
  },
  {
    "path": "dpath/__init__.py",
    "content": "# Needed for pre-3.10 versions\nfrom __future__ import annotations\n\n__all__ = [\n    \"new\",\n    \"delete\",\n    \"set\",\n    \"get\",\n    \"values\",\n    \"search\",\n    \"merge\",\n    \"exceptions\",\n    \"options\",\n    \"segments\",\n    \"types\",\n    \"version\",\n    \"MergeType\",\n    \"PathSegment\",\n    \"Filter\",\n    \"Glob\",\n    \"Path\",\n    \"Hints\",\n    \"Creator\",\n]\n\nfrom collections.abc import MutableMapping, MutableSequence\nfrom typing import Union, List, Any, Callable, Optional\n\nfrom dpath import segments, options\nfrom dpath.exceptions import InvalidKeyName, PathNotFound\nfrom dpath.types import MergeType, PathSegment, Creator, Filter, Glob, Path, Hints\n\n_DEFAULT_SENTINEL = object()\n\n\ndef _split_path(path: Path, separator: Optional[str] = \"/\") -> Union[List[PathSegment], PathSegment]:\n    \"\"\"\n    Given a path and separator, return a tuple of segments. If path is\n    already a non-leaf thing, return it.\n\n    Note that a string path with the separator at index[0] will have the\n    separator stripped off. If you pass a list path, the separator is\n    ignored, and is assumed to be part of each key glob. It will not be\n    stripped.\n    \"\"\"\n    if not segments.leaf(path):\n        split_segments = path\n    else:\n        split_segments = path.lstrip(separator).split(separator)\n\n    return split_segments\n\n\ndef new(obj: MutableMapping, path: Path, value, separator=\"/\", creator: Creator | None = None) -> MutableMapping:\n    \"\"\"\n    Set the element at the terminus of path to value, and create\n    it if it does not exist (as opposed to 'set' that can only\n    change existing keys).\n\n    path will NOT be treated like a glob. If it has globbing\n    characters in it, they will become part of the resulting\n    keys\n\n    creator allows you to pass in a creator method that is\n    responsible for creating missing keys at arbitrary levels of\n    the path (see the help for dpath.path.set)\n    \"\"\"\n    split_segments = _split_path(path, separator)\n    if creator:\n        return segments.set(obj, split_segments, value, creator=creator)\n    return segments.set(obj, split_segments, value)\n\n\ndef delete(obj: MutableMapping, glob: Glob, separator=\"/\", afilter: Filter | None = None) -> int:\n    \"\"\"\n    Given a obj, delete all elements that match the glob.\n\n    Returns the number of deleted objects. Raises PathNotFound if no paths are\n    found to delete.\n    \"\"\"\n    globlist = _split_path(glob, separator)\n\n    def f(obj, pair, counter):\n        (path_segments, value) = pair\n\n        # Skip segments if they no longer exist in obj.\n        if not segments.has(obj, path_segments):\n            return\n\n        matched = segments.match(path_segments, globlist)\n        selected = afilter and segments.leaf(value) and afilter(value)\n\n        if (matched and not afilter) or selected:\n            key = path_segments[-1]\n            parent = segments.get(obj, path_segments[:-1])\n\n            # Deletion behavior depends on parent type\n            if isinstance(parent, MutableMapping):\n                del parent[key]\n\n            else:\n                # Handle sequence types\n                # TODO: Consider cases where type isn't a simple list (e.g. set)\n\n                if len(parent) - 1 == key:\n                    # Removing the last element of a sequence. It can be\n                    # truly removed without affecting the ordering of\n                    # remaining items.\n                    #\n                    # Note: In order to achieve proper behavior we are\n                    # relying on the reverse iteration of\n                    # non-dictionaries from segments.kvs().\n                    # Otherwise we'd be unable to delete all the tails\n                    # of a list and end up with None values when we\n                    # don't need them.\n                    del parent[key]\n\n                else:\n                    # This key can't be removed completely because it\n                    # would affect the order of items that remain in our\n                    # result.\n                    parent[key] = None\n\n            counter[0] += 1\n\n    [deleted] = segments.foldm(obj, f, [0])\n    if not deleted:\n        raise PathNotFound(f\"Could not find {glob} to delete it\")\n\n    return deleted\n\n\ndef set(obj: MutableMapping, glob: Glob, value, separator=\"/\", afilter: Filter | None = None) -> int:\n    \"\"\"\n    Given a path glob, set all existing elements in the document\n    to the given value. Returns the number of elements changed.\n    \"\"\"\n    globlist = _split_path(glob, separator)\n\n    def f(obj, pair, counter):\n        (path_segments, found) = pair\n\n        # Skip segments if they no longer exist in obj.\n        if not segments.has(obj, path_segments):\n            return\n\n        matched = segments.match(path_segments, globlist)\n        selected = afilter and segments.leaf(found) and afilter(found)\n\n        if (matched and not afilter) or (matched and selected):\n            segments.set(obj, path_segments, value, creator=None)\n            counter[0] += 1\n\n    [changed] = segments.foldm(obj, f, [0])\n    return changed\n\n\ndef get(\n        obj: MutableMapping,\n        glob: Glob,\n        separator=\"/\",\n        default: Any = _DEFAULT_SENTINEL\n) -> Union[MutableMapping, object, Callable]:\n    \"\"\"\n    Given an object which contains only one possible match for the given glob,\n    return the value for the leaf matching the given glob.\n    If the glob is not found and a default is provided,\n    the default is returned.\n\n    If more than one leaf matches the glob, ValueError is raised. If the glob is\n    not found and a default is not provided, KeyError is raised.\n    \"\"\"\n    if isinstance(glob, str) and glob == \"/\" or len(glob) == 0:\n        return obj\n\n    globlist = _split_path(glob, separator)\n\n    def f(_, pair, results):\n        (path_segments, found) = pair\n\n        if segments.match(path_segments, globlist):\n            results.append(found)\n        if len(results) > 1:\n            return False\n\n    results = segments.fold(obj, f, [])\n\n    if len(results) == 0:\n        if default is not _DEFAULT_SENTINEL:\n            return default\n\n        raise KeyError(glob)\n    elif len(results) > 1:\n        raise ValueError(f\"dpath.get() globs must match only one leaf: {glob}\")\n\n    return results[0]\n\n\ndef values(obj: MutableMapping, glob: Glob, separator=\"/\", afilter: Filter | None = None, dirs=True):\n    \"\"\"\n    Given an object and a path glob, return an array of all values which match\n    the glob. The arguments to this function are identical to those of search().\n    \"\"\"\n    yielded = True\n\n    return [v for p, v in search(obj, glob, yielded, separator, afilter, dirs)]\n\n\ndef search(obj: MutableMapping, glob: Glob, yielded=False, separator=\"/\", afilter: Filter | None = None, dirs=True):\n    \"\"\"\n    Given a path glob, return a dictionary containing all keys\n    that matched the given glob.\n\n    If 'yielded' is true, then a dictionary will not be returned.\n    Instead, tuples will be yielded in the form of (path, value) for\n    every element in the document that matched the glob.\n    \"\"\"\n\n    split_glob = _split_path(glob, separator)\n\n    def keeper(path, found):\n        \"\"\"\n        Generalized test for use in both yielded and folded cases.\n        Returns True if we want this result. Otherwise, returns False.\n        \"\"\"\n        if not dirs and not segments.leaf(found):\n            return False\n\n        matched = segments.match(path, split_glob)\n        selected = afilter and afilter(found)\n\n        return (matched and not afilter) or (matched and selected)\n\n    if yielded:\n        def yielder():\n            for path, found in segments.walk(obj):\n                if keeper(path, found):\n                    yield separator.join(map(segments.int_str, path)), found\n\n        return yielder()\n    else:\n        def f(obj, pair, result):\n            (path, found) = pair\n\n            if keeper(path, found):\n                segments.set(result, path, found, hints=segments.types(obj, path))\n\n        return segments.fold(obj, f, {})\n\n\ndef merge(\n        dst: MutableMapping,\n        src: MutableMapping,\n        separator=\"/\",\n        afilter: Filter | None = None,\n        flags=MergeType.ADDITIVE\n):\n    \"\"\"\n    Merge source into destination. Like dict.update() but performs deep\n    merging.\n\n    NOTE: This does not do a deep copy of the source object. Applying merge\n    will result in references to src being present in the dst tree. If you do\n    not want src to potentially be modified by other changes in dst (e.g. more\n    merge calls), then use a deep copy of src.\n\n    NOTE that merge() does NOT copy objects - it REFERENCES. If you merge\n    take these two dictionaries:\n\n    >>> a = {'a': [0] }\n    >>> b = {'a': [1] }\n\n    ... and you merge them into an empty dictionary, like so:\n\n    >>> d = {}\n    >>> dpath.merge(d, a)\n    >>> dpath.merge(d, b)\n\n    ... you might be surprised to find that a['a'] now contains [0, 1].\n    This is because merge() says (d['a'] = a['a']), and thus creates a reference.\n    This reference is then modified when b is merged, causing both d and\n    a to have ['a'][0, 1]. To avoid this, make your own deep copies of source\n    objects that you intend to merge. For further notes see\n    https://github.com/akesterson/dpath-python/issues/58\n\n    flags is an OR'ed combination of MergeType enum members.\n    \"\"\"\n    filtered_src = search(src, '**', afilter=afilter, separator='/')\n\n    def are_both_mutable(o1, o2):\n        mapP = isinstance(o1, MutableMapping) and isinstance(o2, MutableMapping)\n        seqP = isinstance(o1, MutableSequence) and isinstance(o2, MutableSequence)\n\n        if mapP or seqP:\n            return True\n\n        return False\n\n    def merger(dst, src, _segments=()):\n        for key, found in segments.make_walkable(src):\n            # Our current path in the source.\n            current_path = _segments + (key,)\n\n            if len(key) == 0 and not options.ALLOW_EMPTY_STRING_KEYS:\n                raise InvalidKeyName(\"Empty string keys not allowed without \"\n                                     \"dpath.options.ALLOW_EMPTY_STRING_KEYS=True: \"\n                                     f\"{current_path}\")\n\n            # Validate src and dst types match.\n            if flags & MergeType.TYPESAFE:\n                if segments.has(dst, current_path):\n                    target = segments.get(dst, current_path)\n                    tt = type(target)\n                    ft = type(found)\n                    if tt != ft:\n                        path = separator.join(current_path)\n                        raise TypeError(f\"Cannot merge objects of type {tt} and {ft} at {path}\")\n\n            # Path not present in destination, create it.\n            if not segments.has(dst, current_path):\n                segments.set(dst, current_path, found)\n                continue\n\n            # Retrieve the value in the destination.\n            target = segments.get(dst, current_path)\n\n            # If the types don't match, replace it.\n            if type(found) is not type(target) and not are_both_mutable(found, target):\n                segments.set(dst, current_path, found)\n                continue\n\n            # If target is a leaf, the replace it.\n            if segments.leaf(target):\n                segments.set(dst, current_path, found)\n                continue\n\n            # At this point we know:\n            #\n            # * The target exists.\n            # * The types match.\n            # * The target isn't a leaf.\n            #\n            # Pretend we have a sequence and account for the flags.\n            try:\n                if flags & MergeType.ADDITIVE:\n                    target += found\n                    continue\n\n                if flags & MergeType.REPLACE:\n                    try:\n                        target[\"\"]\n                    except TypeError:\n                        segments.set(dst, current_path, found)\n                        continue\n                    except Exception:\n                        raise\n            except Exception:\n                # We have a dictionary like thing and we need to attempt to\n                # recursively merge it.\n                merger(dst, found, current_path)\n\n    merger(dst, filtered_src)\n\n    return dst\n"
  },
  {
    "path": "dpath/exceptions.py",
    "content": "class InvalidGlob(Exception):\n    \"\"\"The glob passed is invalid.\"\"\"\n    pass\n\n\nclass PathNotFound(Exception):\n    \"\"\"One or more elements of the requested path did not exist in the object\"\"\"\n    pass\n\n\nclass InvalidKeyName(Exception):\n    \"\"\"This key contains the separator character or another invalid character\"\"\"\n    pass\n\n\nclass FilteredValue(Exception):\n    \"\"\"Unable to return a value, since the filter rejected it\"\"\"\n    pass\n"
  },
  {
    "path": "dpath/options.py",
    "content": "ALLOW_EMPTY_STRING_KEYS = False\n"
  },
  {
    "path": "dpath/py.typed",
    "content": ""
  },
  {
    "path": "dpath/segments.py",
    "content": "from copy import deepcopy\nfrom fnmatch import fnmatchcase\nfrom typing import Sequence, Tuple, Iterator, Any, Union, Optional, MutableMapping, MutableSequence\n\nfrom dpath import options\nfrom dpath.exceptions import InvalidGlob, InvalidKeyName, PathNotFound\nfrom dpath.types import PathSegment, Creator, Hints, Glob, Path, ListIndex\n\n\ndef make_walkable(node) -> Iterator[Tuple[PathSegment, Any]]:\n    \"\"\"\n    Returns an iterator which yields tuple pairs of (node index, node value), regardless of node type.\n\n    * For dict nodes `node.items()` will be returned.\n    * For sequence nodes (lists/tuples/etc.) a zip between index number and index value will be returned.\n    * Edge cases will result in an empty iterator being returned.\n\n    make_walkable(node) -> (generator -> (key, value))\n    \"\"\"\n    try:\n        return iter(node.items())\n    except AttributeError:\n        try:\n            indices = range(len(node))\n            # Convert all list indices to objects so negative indices are supported.\n            indices = map(lambda i: ListIndex(i, len(node)), indices)\n            return zip(indices, node)\n        except TypeError:\n            # This can happen in cases where the node isn't leaf(node) == True,\n            # but also isn't actually iterable. Instead of this being an error\n            # we will treat this node as if it has no children.\n            return enumerate([])\n\n\ndef leaf(thing):\n    \"\"\"\n    Return True if thing is a leaf, otherwise False.\n    \"\"\"\n    leaves = (bytes, str, int, float, bool, type(None))\n\n    return isinstance(thing, leaves)\n\n\ndef leafy(thing):\n    \"\"\"\n    Same as leaf(thing), but also treats empty sequences and\n    dictionaries as True.\n    \"\"\"\n\n    try:\n        return leaf(thing) or len(thing) == 0\n    except TypeError:\n        # In case thing has no len()\n        return False\n\n\ndef walk(obj, location=()):\n    \"\"\"\n    Yield all valid (segments, value) pairs (from a breadth-first\n    search, right-to-left on sequences).\n\n    walk(obj) -> (generator -> (segments, value))\n    \"\"\"\n    if not leaf(obj):\n        for k, v in make_walkable(obj):\n            length = None\n\n            try:\n                length = len(k)\n            except TypeError:\n                pass\n\n            if length is not None and length == 0 and not options.ALLOW_EMPTY_STRING_KEYS:\n                raise InvalidKeyName(\"Empty string keys not allowed without \"\n                                     \"dpath.options.ALLOW_EMPTY_STRING_KEYS=True: \"\n                                     f\"{location + (k,)}\")\n            yield (location + (k,)), v\n\n        for k, v in make_walkable(obj):\n            for found in walk(v, location + (k,)):\n                yield found\n\n\ndef get(obj, segments: Path):\n    \"\"\"\n    Return the value at the path indicated by segments.\n\n    get(obj, segments) -> value\n    \"\"\"\n    current = obj\n    for i, segment in enumerate(segments):\n        if leaf(current):\n            raise PathNotFound(f\"Path: {segments}[{i}]\")\n\n        if isinstance(current, Sequence) and isinstance(segment, str) and segment.isdecimal():\n            segment = int(segment)\n\n        current = current[segment]\n    return current\n\n\ndef has(obj, segments):\n    \"\"\"\n    Return True if the path exists in the obj. Otherwise return False.\n\n    has(obj, segments) -> bool\n    \"\"\"\n    try:\n        get(obj, segments)\n        return True\n    except:\n        return False\n\n\ndef expand(segments):\n    \"\"\"\n    Yield a tuple of segments for each possible length of segments.\n    Starting from the shortest length of segments and increasing by 1.\n\n    expand(keys) -> (..., keys[:-2], keys[:-1])\n    \"\"\"\n    index = 0\n    for _ in segments:\n        index += 1\n        yield segments[:index]\n\n\ndef types(obj, segments):\n    \"\"\"\n    For each segment produce a tuple of (segment, type(value)).\n\n    types(obj, segments) -> ((segment[0], type0), (segment[1], type1), ...)\n    \"\"\"\n    result = []\n    for depth in expand(segments):\n        result.append((depth[-1], type(get(obj, depth))))\n    return tuple(result)\n\n\ndef leaves(obj):\n    \"\"\"\n    Yield all leaves as (segment, value) pairs.\n\n    leaves(obj) -> (generator -> (segment, value))\n    \"\"\"\n    return filter(lambda p: leafy(p[1]), walk(obj))\n\n\ndef int_str(segment: PathSegment) -> PathSegment:\n    \"\"\"\n    If the segment is an integer, return the string conversion.\n    Otherwise return the segment unchanged. The conversion uses 'str'.\n\n    int_str(segment) -> str\n    \"\"\"\n    if isinstance(segment, int):\n        return str(segment)\n    return segment\n\n\nclass Star(object):\n    \"\"\"\n    Used to create a global STAR symbol for tracking stars added when\n    expanding star-star globs.\n    \"\"\"\n    pass\n\n\nSTAR = Star()\n\n\ndef match(segments: Path, glob: Glob):\n    \"\"\"\n    Return True if the segments match the given glob, otherwise False.\n\n    For the purposes of matching, integers are converted to their string\n    equivalent (via str(segment)). This conversion happens on both the\n    segments and the glob. This implies you cannot (with this function)\n    differentiate a list index 0 from a dictionary key '0'.\n\n    Star-star segments are a special case in that they will expand to 0\n    or more star segments and the type will be coerced to match that of\n    the segment.\n\n    A segment is considered to match a glob if the function\n    fnmatch.fnmatchcase returns True. If fnmatchcase returns False or\n    throws an exception the result will be False.\n\n    match(segments, glob) -> bool\n    \"\"\"\n    segments = tuple(segments)\n    glob = tuple(glob)\n\n    path_len = len(segments)\n    glob_len = len(glob)\n\n    # The star-star normalized glob ('**' has been removed).\n    ss_glob = glob\n\n    if '**' in glob:\n        # Index of the star-star in the glob.\n        ss = glob.index('**')\n\n        if '**' in glob[ss + 1:]:\n            raise InvalidGlob(f\"Invalid glob. Only one '**' is permitted per glob: {glob}\")\n\n        # Convert '**' segment into multiple '*' segments such that the\n        # lengths of the path and glob match. '**' also can collapse and\n        # result in the removal of 1 segment.\n        if path_len >= glob_len:\n            # Path and glob have the same number of stars or the glob\n            # needs more stars (which we add).\n            more_stars = (STAR,) * (path_len - glob_len + 1)\n            ss_glob = glob[:ss] + more_stars + glob[ss + 1:]\n        elif path_len == glob_len - 1:\n            # Glob has one more segment than the path. Here we remove\n            # the '**' segment altogether to match the lengths up.\n            ss_glob = glob[:ss] + glob[ss + 1:]\n\n    # If we were successful in matching up the lengths, then we can\n    # compare them using fnmatch.\n    if path_len == len(ss_glob):\n        i = zip(segments, ss_glob)\n        for s, g in i:\n            # Match the stars we added to the glob to the type of the\n            # segment itself.\n            if g is STAR:\n                if isinstance(s, bytes):\n                    g = b'*'\n                else:\n                    g = '*'\n\n            try:\n                # If search path segment (s) is an int then assume currently evaluated index (g) might be a sequence\n                # index as well. Try converting it to an int.\n                if isinstance(s, int) and s == int(g):\n                    continue\n            except:\n                # Will reach this point if g can't be converted to an int (e.g. when g is a RegEx pattern).\n                # In this case convert s to a str so fnmatch can work on it.\n                s = str(s)\n\n            try:\n                # Let's see if the glob matches. We will turn any kind of\n                # exception while attempting to match into a False for the\n                # match.\n                if not fnmatchcase(s, g):\n                    return False\n            except:\n                return False\n\n        # All of the segments matched so we have a complete match.\n        return True\n\n    # Otherwise the lengths aren't the same and we couldn't have a\n    # match.\n    return False\n\n\ndef extend(thing: MutableSequence, index: int, value=None):\n    \"\"\"\n    Extend a sequence like thing such that it contains at least index +\n    1 many elements. The extension values will be None (default).\n\n    extend(thing, int) -> [thing..., None, ...]\n    \"\"\"\n    try:\n        expansion = type(thing)()\n\n        # Using this rather than the multiply notation in order to support a\n        # wider variety of sequence like things.\n        extra = (index + 1) - len(thing)\n        for i in range(extra):\n            expansion += [value]\n        thing.extend(expansion)\n    except TypeError:\n        # We attempted to extend something that doesn't support it. In\n        # this case we assume thing is actually more like a dictionary\n        # and doesn't need to be extended.\n        pass\n\n    return thing\n\n\ndef _default_creator(\n        current: Union[MutableMapping, Sequence],\n        segments: Sequence[PathSegment],\n        i: int,\n        hints: Sequence[Tuple[PathSegment, type]] = ()\n):\n    \"\"\"\n    Create missing path components. If the segment is an int, then it will\n    create a list. Otherwise a dictionary is created.\n\n    set(obj, segments, value) -> obj\n    \"\"\"\n    segment = segments[i]\n    length = len(segments)\n\n    if isinstance(current, Sequence):\n        segment = int(segment)\n\n    if isinstance(current, MutableSequence):\n        extend(current, segment)\n\n    # Infer the type from the hints provided.\n    if i < len(hints):\n        current[segment] = hints[i][1]()\n    else:\n        # Peek at the next segment to determine if we should be\n        # creating an array for it to access or dictionary.\n        if i + 1 < length:\n            segment_next = segments[i + 1]\n        else:\n            segment_next = None\n\n        if isinstance(segment_next, int) or (isinstance(segment_next, str) and segment_next.isdecimal()):\n            current[segment] = []\n        else:\n            current[segment] = {}\n\n\ndef set(\n        obj: MutableMapping,\n        segments: Sequence[PathSegment],\n        value,\n        creator: Optional[Creator] = _default_creator,\n        hints: Hints = ()\n) -> MutableMapping:\n    \"\"\"\n    Set the value in obj at the place indicated by segments. If creator is not\n    None (default _default_creator), then call the creator function to\n    create any missing path components.\n\n    set(obj, segments, value) -> obj\n    \"\"\"\n    current = obj\n    length = len(segments)\n\n    # For everything except the last value, walk down the path and\n    # create if creator is set.\n    for (i, segment) in enumerate(segments[:-1]):\n\n        # If segment is non-int but supposed to be a sequence index\n        if isinstance(segment, str) and isinstance(current, Sequence) and segment.isdecimal():\n            segment = int(segment)\n\n        try:\n            # Optimistically try to get the next value. This makes the\n            # code agnostic to whether current is a list or a dict.\n            # Unfortunately, for our use, 'x in thing' for lists checks\n            # values, not keys whereas dicts check keys.\n            current[segment]\n        except:\n            if creator is not None:\n                creator(current, segments, i, hints)\n            else:\n                raise\n\n        current = current[segment]\n        if i != length - 1 and leaf(current):\n            raise PathNotFound(f\"Path: {segments}[{i}]\")\n\n    last_segment = segments[-1]\n\n    # Resolve ambiguity of last segment\n    if isinstance(last_segment, str) and isinstance(current, Sequence) and last_segment.isdecimal():\n        last_segment = int(last_segment)\n\n    if isinstance(last_segment, int):\n        extend(current, last_segment)\n\n    current[last_segment] = value\n\n    return obj\n\n\ndef fold(obj, f, acc):\n    \"\"\"\n    Walk obj applying f to each path and returning accumulator acc.\n\n    The function f will be called, for each result in walk(obj):\n\n        f(obj, (segments, value), acc)\n\n    If the function f returns False (exactly False), then processing\n    will stop. Otherwise processing will continue with the next value\n    retrieved from the walk.\n\n    fold(obj, f(obj, (segments, value), acc) -> bool, acc) -> acc\n    \"\"\"\n    for pair in walk(obj):\n        if f(obj, pair, acc) is False:\n            break\n    return acc\n\n\ndef foldm(obj, f, acc):\n    \"\"\"\n    Same as fold(), but permits mutating obj.\n\n    This requires all paths in walk(obj) to be loaded into memory\n    (whereas fold does not).\n\n    foldm(obj, f(obj, (segments, value), acc) -> bool, acc) -> acc\n    \"\"\"\n    pairs = tuple(walk(obj))\n    for pair in pairs:\n        if f(obj, pair, acc) is False:\n            break\n    return acc\n\n\ndef view(obj: MutableMapping, glob: Glob):\n    \"\"\"\n    Return a view of the object where the glob matches. A view retains\n    the same form as the obj, but is limited to only the paths that\n    matched. Views are new objects (a deepcopy of the matching values).\n\n    view(obj, glob) -> obj'\n    \"\"\"\n\n    def f(obj, pair, result):\n        (segments, value) = pair\n        if match(segments, glob):\n            if not has(result, segments):\n                set(result, segments, deepcopy(value), hints=types(obj, segments))\n\n    return fold(obj, f, type(obj)())\n"
  },
  {
    "path": "dpath/types.py",
    "content": "from enum import IntFlag, auto\nfrom typing import Union, Any, Callable, Sequence, Tuple, List, Optional, MutableMapping\n\n\nclass ListIndex(int):\n    \"\"\"Same as a normal int but mimics the behavior of list indices (can be compared to a negative number).\"\"\"\n\n    def __new__(cls, value: int, list_length: int, *args, **kwargs):\n        if value >= list_length:\n            raise TypeError(\n                f\"Tried to initiate a {cls.__name__} with a value ({value}) \"\n                f\"greater than the provided max value ({list_length})\"\n            )\n\n        obj = super().__new__(cls, value)\n        obj.list_length = list_length\n\n        return obj\n\n    def __eq__(self, other):\n        if not isinstance(other, int):\n            return False\n\n        # Based on how Python sequences handle negative indices as described in footnote (3) of https://docs.python.org/3/library/stdtypes.html#common-sequence-operations\n        return other == int(self) or self.list_length + other == int(self)\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {int(self)}/{self.list_length}>\"\n\n    def __str__(self):\n        return str(int(self))\n\n\nclass MergeType(IntFlag):\n    ADDITIVE = auto()\n    \"\"\"List objects are combined onto one long list (NOT a set). This is the default flag.\"\"\"\n\n    REPLACE = auto()\n    \"\"\"Instead of combining list objects, when 2 list objects are at an equal depth of merge, replace the destination \\\n    with the source.\"\"\"\n\n    TYPESAFE = auto()\n    \"\"\"When 2 keys at equal levels are of different types, raise a TypeError exception. By default, the source \\\n    replaces the destination in this situation.\"\"\"\n\n\nPathSegment = Union[int, str, bytes]\n\"\"\"Type alias for dict path segments where integers are explicitly casted.\"\"\"\n\nFilter = Callable[[Any], bool]\n\"\"\"Type alias for filter functions.\n\n(Any) -> bool\"\"\"\n\nGlob = Union[str, Sequence[str]]\n\"\"\"Type alias for glob parameters.\"\"\"\n\nPath = Union[str, Sequence[PathSegment]]\n\"\"\"Type alias for path parameters.\"\"\"\n\nHints = Sequence[Tuple[PathSegment, type]]\n\"\"\"Type alias for creator function hint sequences.\"\"\"\n\nCreator = Callable[[Union[MutableMapping, List], Path, int, Optional[Hints]], None]\n\"\"\"Type alias for creator functions.\n\nExample creator function signature:\n\n    def creator(\n        current: Union[MutableMapping, List],\n        segments: Sequence[PathSegment],\n        i: int,\n        hints: Sequence[Tuple[PathSegment, type]] = ()\n    )\"\"\"\n"
  },
  {
    "path": "dpath/util.py",
    "content": "import warnings\n\nimport dpath\nfrom dpath import _DEFAULT_SENTINEL\nfrom dpath.types import MergeType\n\n\ndef deprecated(func):\n    message = \\\n        \"The dpath.util package is being deprecated. All util functions have been moved to dpath package top level.\"\n\n    def wrapper(*args, **kwargs):\n        warnings.warn(message, DeprecationWarning, stacklevel=2)\n        return func(*args, **kwargs)\n\n    return wrapper\n\n\n@deprecated\ndef new(obj, path, value, separator=\"/\", creator=None):\n    return dpath.new(obj, path, value, separator, creator)\n\n\n@deprecated\ndef delete(obj, glob, separator=\"/\", afilter=None):\n    return dpath.delete(obj, glob, separator, afilter)\n\n\n@deprecated\ndef set(obj, glob, value, separator=\"/\", afilter=None):\n    return dpath.set(obj, glob, value, separator, afilter)\n\n\n@deprecated\ndef get(obj, glob, separator=\"/\", default=_DEFAULT_SENTINEL):\n    return dpath.get(obj, glob, separator, default)\n\n\n@deprecated\ndef values(obj, glob, separator=\"/\", afilter=None, dirs=True):\n    return dpath.values(obj, glob, separator, afilter, dirs)\n\n\n@deprecated\ndef search(obj, glob, yielded=False, separator=\"/\", afilter=None, dirs=True):\n    return dpath.search(obj, glob, yielded, separator, afilter, dirs)\n\n\n@deprecated\ndef merge(dst, src, separator=\"/\", afilter=None, flags=MergeType.ADDITIVE):\n    return dpath.merge(dst, src, separator, afilter, flags)\n"
  },
  {
    "path": "dpath/version.py",
    "content": "VERSION = \"2.2.0\"\n"
  },
  {
    "path": "flake8.ini",
    "content": "[flake8]\nfilename=\n    setup.py,\n    dpath/,\n    tests/\n"
  },
  {
    "path": "maintainers_log.md",
    "content": "# 03/29/2020\n\nAttendees : Caleb, Andrew\n\n## Old business :\n\n* Need to onboard new member Vladimir Ulogov <vladimir.ulogov@me.com>\n  * No movement\n* Need to make project board for 1.5 open bugs\n  * Done\n\n## New business :\n\n* Andrew to define maintainers meeting process and establish log of decisions, process for filing open action items\n* Andrew to forward maintainers invite to Vladimir and include in next monthly maintainers meeting\n* Andrew to set followup for 1wk from now to check for comments on PRs and cut release version for 1.x / 2.x\n* Andrew to rename LTS branches from version/1.0 version/2.0 to version/1.x and version/2.x\n"
  },
  {
    "path": "setup.py",
    "content": "import os\nfrom setuptools import setup\n\nimport dpath.version\n\nlong_description = open(\n    os.path.join(\n        os.path.dirname(__file__),\n        'README.rst'\n    )\n).read()\n\nif __name__ == \"__main__\":\n    setup(\n        name=\"dpath\",\n        url=\"https://github.com/dpath-maintainers/dpath-python\",\n        version=dpath.version.VERSION,\n        description=\"Filesystem-like pathing and searching for dictionaries\",\n        long_description=long_description,\n        author=(\"Caleb Case, \"\n                \"Andrew Kesterson\"),\n        author_email=\"calebcase@gmail.com, andrew@aklabs.net\",\n        license=\"MIT\",\n        install_requires=[],\n        scripts=[],\n        packages=[\"dpath\"],\n        data_files=[],\n        package_data={\"dpath\": [\"py.typed\"]},\n\n        # Type hints are great.\n        # Function annotations were added in Python 3.0.\n        # Typing module was added in Python 3.5.\n        # Variable annotations were added in Python 3.6.\n        # Python versions that are >=3.6 are more popular.\n        #   (Source: https://github.com/hugovk/pypi-tools/blob/master/README.md)\n        #\n        # Conclusion: In order to accommodate type hinting support must be limited to Python versions >=3.6.\n        # 3.6 was dropped because of EOL and this issue: https://github.com/actions/setup-python/issues/544\n        python_requires=\">=3.7\",\n        classifiers=[\n            'Development Status :: 5 - Production/Stable',\n            'Environment :: Console',\n            'Intended Audience :: Developers',\n            'License :: OSI Approved :: MIT License',\n            'Natural Language :: English',\n            'Programming Language :: Python :: 3',\n            'Programming Language :: Python :: 3.7',\n            'Programming Language :: Python :: 3.8',\n            'Programming Language :: Python :: 3.9',\n            'Programming Language :: Python :: 3.10',\n            'Programming Language :: Python :: 3.11',\n            'Programming Language :: Python :: 3.12',\n            'Topic :: Software Development :: Libraries :: Python Modules',\n            'Typing :: Typed',\n        ],\n    )\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "import warnings\n\nwarnings.simplefilter(\"always\", DeprecationWarning)\n"
  },
  {
    "path": "tests/test_broken_afilter.py",
    "content": "import dpath\nimport sys\n\n\ndef test_broken_afilter():\n    def afilter(x):\n        if x in [1, 2]:\n            return True\n        return False\n\n    dict = {\n        \"a\": {\n            \"view_failure\": \"a\",\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n    paths = [\n        'a/b/c/e',\n        'a/b/c/f',\n    ]\n\n    for (path, value) in dpath.search(dict, '/**', yielded=True, afilter=afilter):\n        assert path in paths\n    assert \"view_failure\" not in dpath.search(dict, '/**', afilter=afilter)['a']\n    assert \"d\" not in dpath.search(dict, '/**', afilter=afilter)['a']['b']['c']\n\n    for (path, value) in dpath.search(dict, ['**'], yielded=True, afilter=afilter):\n        assert path in paths\n    assert \"view_failure\" not in dpath.search(dict, ['**'], afilter=afilter)['a']\n    assert \"d\" not in dpath.search(dict, ['**'], afilter=afilter)['a']['b']['c']\n\n    def filter(x):\n        sys.stderr.write(str(x))\n        if hasattr(x, 'get'):\n            return x.get('type', None) == 'correct'\n        return False\n\n    a = {\n        'actions': [\n            {\n                'type': 'correct'\n            },\n            {\n                'type': 'incorrect'\n            },\n        ],\n    }\n\n    results = [[x[0], x[1]] for x in dpath.search(a, 'actions/*', yielded=True)]\n    results = [[x[0], x[1]] for x in dpath.search(a, 'actions/*', afilter=filter, yielded=True)]\n    assert len(results) == 1\n    assert results[0][1]['type'] == 'correct'\n"
  },
  {
    "path": "tests/test_delete.py",
    "content": "from nose2.tools.such import helper\n\nimport dpath\nimport dpath.exceptions\n\n\ndef test_delete_separator():\n    dict = {\n        \"a\": {\n            \"b\": 0,\n        },\n    }\n\n    dpath.delete(dict, ';a;b', separator=\";\")\n    assert 'b' not in dict['a']\n\n\ndef test_delete_existing():\n    dict = {\n        \"a\": {\n            \"b\": 0,\n        },\n    }\n\n    dpath.delete(dict, '/a/b')\n    assert 'b' not in dict['a']\n\n\ndef test_delete_missing():\n    dict = {\n        \"a\": {\n        },\n    }\n\n    with helper.assertRaises(dpath.exceptions.PathNotFound):\n        dpath.delete(dict, '/a/b')\n\n\ndef test_delete_filter():\n    def afilter(x):\n        if int(x) == 31:\n            return True\n        return False\n\n    dict = {\n        \"a\": {\n            \"b\": 0,\n            \"c\": 1,\n            \"d\": 31,\n        },\n    }\n\n    dpath.delete(dict, '/a/*', afilter=afilter)\n    assert dict['a']['b'] == 0\n    assert dict['a']['c'] == 1\n    assert 'd' not in dict['a']\n"
  },
  {
    "path": "tests/test_get_values.py",
    "content": "import datetime\nimport decimal\nimport time\n\nfrom unittest import mock\n\nfrom nose2.tools.such import helper\n\nimport dpath\n\n\ndef test_util_get_root():\n    x = {'p': {'a': {'t': {'h': 'value'}}}}\n\n    ret = dpath.get(x, '/p/a/t/h')\n    assert ret == 'value'\n\n    ret = dpath.get(x, '/')\n    assert ret == x\n\n    ret = dpath.get(x, [])\n    assert ret == x\n\n\ndef test_get_explicit_single():\n    ehash = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n\n    assert dpath.get(ehash, '/a/b/c/f') == 2\n    assert dpath.get(ehash, ['a', 'b', 'c', 'f']) == 2\n    assert dpath.get(ehash, ['a', 'b', 'c', 'f'], default=5) == 2\n    assert dpath.get(ehash, ['does', 'not', 'exist'], default=None) is None\n    assert dpath.get(ehash, ['doesnt', 'exist'], default=5) == 5\n\n\ndef test_get_glob_single():\n    ehash = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n\n    assert dpath.get(ehash, '/a/b/*/f') == 2\n    assert dpath.get(ehash, ['a', 'b', '*', 'f']) == 2\n    assert dpath.get(ehash, ['a', 'b', '*', 'f'], default=5) == 2\n    assert dpath.get(ehash, ['doesnt', '*', 'exist'], default=6) == 6\n\n\ndef test_get_glob_multiple():\n    ehash = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                },\n                \"e\": {\n                    \"d\": 0,\n                },\n            },\n        },\n    }\n\n    helper.assertRaises(ValueError, dpath.get, ehash, '/a/b/*/d')\n    helper.assertRaises(ValueError, dpath.get, ehash, ['a', 'b', '*', 'd'])\n    helper.assertRaises(ValueError, dpath.get, ehash, ['a', 'b', '*', 'd'], default=3)\n\n\ndef test_get_absent():\n    ehash = {}\n\n    helper.assertRaises(KeyError, dpath.get, ehash, '/a/b/c/d/f')\n    helper.assertRaises(KeyError, dpath.get, ehash, ['a', 'b', 'c', 'd', 'f'])\n\n\ndef test_values():\n    ehash = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n\n    ret = dpath.values(ehash, '/a/b/c/*')\n    assert isinstance(ret, list)\n    assert 0 in ret\n    assert 1 in ret\n    assert 2 in ret\n\n    ret = dpath.values(ehash, ['a', 'b', 'c', '*'])\n    assert isinstance(ret, list)\n    assert 0 in ret\n    assert 1 in ret\n    assert 2 in ret\n\n\n@mock.patch('dpath.search')\ndef test_values_passes_through(searchfunc):\n    searchfunc.return_value = []\n\n    def y():\n        return False\n\n    dpath.values({}, '/a/b', ':', y, False)\n    searchfunc.assert_called_with({}, '/a/b', True, ':', y, False)\n\n    dpath.values({}, ['a', 'b'], ':', y, False)\n    searchfunc.assert_called_with({}, ['a', 'b'], True, ':', y, False)\n\n\ndef test_none_values():\n    d = {'p': {'a': {'t': {'h': None}}}}\n\n    v = dpath.get(d, 'p/a/t/h')\n    assert v is None\n\n\ndef test_values_list():\n    a = {\n        'actions': [\n            {\n                'type': 'correct',\n            },\n            {\n                'type': 'incorrect',\n            },\n        ],\n    }\n\n    ret = dpath.values(a, 'actions/*')\n    assert isinstance(ret, list)\n    assert len(ret) == 2\n\n\ndef test_non_leaf_leaf():\n    # The leaves in this test aren't leaf(thing) == True, but we should still\n    # be able to get them. They should also not prevent fetching other values.\n\n    def func(x):\n        return x\n\n    testdict = {\n        'a': func,\n        'b': lambda x: x,\n        'c': [\n            {\n                'a',\n                'b',\n            },\n        ],\n        'd': [\n            decimal.Decimal(1.5),\n            decimal.Decimal(2.25),\n        ],\n        'e': datetime.datetime(2020, 1, 1),\n        'f': {\n            'config': 'something',\n        },\n    }\n\n    # It should be possible to get the callables:\n    assert dpath.get(testdict, 'a') == func\n    assert dpath.get(testdict, 'b')(42) == 42\n\n    # It should be possible to get other values:\n    assert dpath.get(testdict, 'c/0') == testdict['c'][0]\n    assert dpath.get(testdict, 'd')[0] == testdict['d'][0]\n    assert dpath.get(testdict, 'd/0') == testdict['d'][0]\n    assert dpath.get(testdict, 'd/1') == testdict['d'][1]\n    assert dpath.get(testdict, 'e') == testdict['e']\n\n    # Values should also still work:\n    assert dpath.values(testdict, 'f/config') == ['something']\n\n    # Data classes should also be retrievable:\n    try:\n        import dataclasses\n    except:\n        return\n\n    @dataclasses.dataclass\n    class Connection:\n        group_name: str\n        channel_name: str\n        last_seen: float\n\n    testdict['g'] = {\n        'my-key': Connection(\n            group_name='foo',\n            channel_name='bar',\n            last_seen=time.time(),\n        ),\n    }\n\n    assert dpath.search(testdict, 'g/my*')['g']['my-key'] == testdict['g']['my-key']\n"
  },
  {
    "path": "tests/test_merge.py",
    "content": "import copy\n\nfrom nose2.tools.such import helper\n\n\nimport dpath\nfrom dpath import MergeType\n\n\ndef test_merge_typesafe_and_separator():\n    src = {\n        \"dict\": {\n            \"integer\": 0,\n        },\n    }\n    dst = {\n        \"dict\": {\n            \"integer\": \"3\",\n        },\n    }\n\n    try:\n        dpath.merge(dst, src, flags=(dpath.MergeType.ADDITIVE | dpath.MergeType.TYPESAFE), separator=\";\")\n    except TypeError as e:\n        assert str(e).endswith(\"dict;integer\")\n\n        return\n    raise Exception(\"MERGE_TYPESAFE failed to raise an exception when merging between str and int!\")\n\n\ndef test_merge_simple_int():\n    src = {\n        \"integer\": 0,\n    }\n    dst = {\n        \"integer\": 3,\n    }\n\n    dpath.merge(dst, src)\n    assert dst[\"integer\"] == src[\"integer\"], \"%r != %r\" % (dst[\"integer\"], src[\"integer\"])\n\n\ndef test_merge_simple_string():\n    src = {\n        \"string\": \"lol I am a string\",\n    }\n    dst = {\n        \"string\": \"lol I am a string\",\n    }\n\n    dpath.merge(dst, src)\n    assert dst[\"string\"] == src[\"string\"], \"%r != %r\" % (dst[\"string\"], src[\"string\"])\n\n\ndef test_merge_simple_list_additive():\n    src = {\n        \"list\": [7, 8, 9, 10],\n    }\n    dst = {\n        \"list\": [0, 1, 2, 3],\n    }\n\n    dpath.merge(dst, src, flags=MergeType.ADDITIVE)\n    assert dst[\"list\"] == [0, 1, 2, 3, 7, 8, 9, 10], \"%r != %r\" % (dst[\"list\"], [0, 1, 2, 3, 7, 8, 9, 10])\n\n\ndef test_merge_simple_list_replace():\n    src = {\n        \"list\": [7, 8, 9, 10],\n    }\n    dst = {\n        \"list\": [0, 1, 2, 3],\n    }\n\n    dpath.merge(dst, src, flags=dpath.MergeType.REPLACE)\n    assert dst[\"list\"] == [7, 8, 9, 10], \"%r != %r\" % (dst[\"list\"], [7, 8, 9, 10])\n\n\ndef test_merge_simple_dict():\n    src = {\n        \"dict\": {\n            \"key\": \"WEHAW\",\n        },\n    }\n    dst = {\n        \"dict\": {\n            \"key\": \"\",\n        },\n    }\n\n    dpath.merge(dst, src)\n    assert dst[\"dict\"][\"key\"] == src[\"dict\"][\"key\"], \"%r != %r\" % (dst[\"dict\"][\"key\"], src[\"dict\"][\"key\"])\n\n\ndef test_merge_filter():\n    def afilter(x):\n        if \"rubber\" not in str(x):\n            return False\n        return True\n\n    src = {\n        \"key\": \"metal\",\n        \"key2\": \"rubber\",\n        \"otherdict\": {\n            \"key3\": \"I shouldn't be here\",\n        },\n    }\n    dst = {}\n\n    dpath.merge(dst, src, afilter=afilter)\n    assert \"key2\" in dst\n    assert \"key\" not in dst\n    assert \"otherdict\" not in dst\n\n\ndef test_merge_typesafe():\n    src = {\n        \"dict\": {\n        },\n    }\n    dst = {\n        \"dict\": [\n        ],\n    }\n\n    helper.assertRaises(TypeError, dpath.merge, dst, src, flags=dpath.MergeType.TYPESAFE)\n\n\ndef test_merge_mutables():\n    class tcid(dict):\n        pass\n\n    class tcis(list):\n        pass\n\n    src = {\n        \"mm\": {\n            \"a\": \"v1\",\n        },\n        \"ms\": [\n            0,\n        ],\n    }\n    dst = {\n        \"mm\": tcid([\n            (\"a\", \"v2\"),\n            (\"casserole\", \"this should keep\"),\n        ]),\n        \"ms\": tcis(['a', 'b', 'c']),\n    }\n\n    dpath.merge(dst, src)\n    print(dst)\n    assert dst[\"mm\"][\"a\"] == src[\"mm\"][\"a\"]\n    assert dst['ms'][2] == 'c'\n    assert \"casserole\" in dst[\"mm\"]\n\n    helper.assertRaises(TypeError, dpath.merge, dst, src, flags=dpath.MergeType.TYPESAFE)\n\n\ndef test_merge_replace_1():\n    dct_a = {\"a\": {\"b\": [1, 2, 3]}}\n    dct_b = {\"a\": {\"b\": [1]}}\n    dpath.merge(dct_a, dct_b, flags=dpath.MergeType.REPLACE)\n    assert len(dct_a['a']['b']) == 1\n\n\ndef test_merge_replace_2():\n    d1 = {'a': [0, 1, 2]}\n    d2 = {'a': ['a']}\n    dpath.merge(d1, d2, flags=dpath.MergeType.REPLACE)\n    assert len(d1['a']) == 1\n    assert d1['a'][0] == 'a'\n\n\ndef test_merge_list():\n    src = {\"l\": [1]}\n    p1 = {\"l\": [2], \"v\": 1}\n    p2 = {\"v\": 2}\n\n    dst1 = {}\n    for d in [copy.deepcopy(src), copy.deepcopy(p1)]:\n        dpath.merge(dst1, d)\n    dst2 = {}\n    for d in [copy.deepcopy(src), copy.deepcopy(p2)]:\n        dpath.merge(dst2, d)\n    assert dst1[\"l\"] == [1, 2]\n    assert dst2[\"l\"] == [1]\n\n    dst1 = {}\n    for d in [src, p1]:\n        dpath.merge(dst1, d)\n    dst2 = {}\n    for d in [src, p2]:\n        dpath.merge(dst2, d)\n    assert dst1[\"l\"] == [1, 2]\n    assert dst2[\"l\"] == [1, 2]\n"
  },
  {
    "path": "tests/test_new.py",
    "content": "import dpath\n\n\ndef test_set_new_separator():\n    dict = {\n        \"a\": {\n        },\n    }\n\n    dpath.new(dict, ';a;b', 1, separator=\";\")\n    assert dict['a']['b'] == 1\n\n    dpath.new(dict, ['a', 'b'], 1, separator=\";\")\n    assert dict['a']['b'] == 1\n\n\ndef test_set_new_dict():\n    dict = {\n        \"a\": {\n        },\n    }\n\n    dpath.new(dict, '/a/b', 1)\n    assert dict['a']['b'] == 1\n\n    dpath.new(dict, ['a', 'b'], 1)\n    assert dict['a']['b'] == 1\n\n\ndef test_set_new_list():\n    dict = {\n        \"a\": [\n        ],\n    }\n\n    dpath.new(dict, '/a/1', 1)\n    assert dict['a'][1] == 1\n    assert dict['a'][0] is None\n\n    dpath.new(dict, ['a', 1], 1)\n    assert dict['a'][1] == 1\n    assert dict['a'][0] is None\n\n\ndef test_set_list_with_dict_int_ambiguity():\n    d = {\"list\": [{\"root\": {\"1\": {\"k\": None}}}]}\n\n    dpath.new(d, \"list/0/root/1/k\", \"new\")\n\n    expected = {\"list\": [{\"root\": {\"1\": {\"k\": \"new\"}}}]}\n\n    assert d == expected\n\n\ndef test_int_segment_list_type_check():\n    d = {}\n    dpath.new(d, \"a/b/0/c/0\", \"hello\")\n    assert 'b' in d.get(\"a\", {})\n    assert isinstance(d[\"a\"][\"b\"], list)\n    assert len(d[\"a\"][\"b\"]) == 1\n    assert 'c' in d[\"a\"][\"b\"][0]\n    assert isinstance(d[\"a\"][\"b\"][0][\"c\"], list)\n    assert len(d[\"a\"][\"b\"][0][\"c\"]) == 1\n\n\ndef test_int_segment_dict_type_check():\n    d = {\"a\": {\"b\": {\"0\": {}}}}\n    dpath.new(d, \"a/b/0/c/0\", \"hello\")\n    assert \"b\" in d.get(\"a\", {})\n    assert isinstance(d[\"a\"][\"b\"], dict)\n    assert '0' in d[\"a\"][\"b\"]\n    assert 'c' in d[\"a\"][\"b\"][\"0\"]\n    assert isinstance(d[\"a\"][\"b\"][\"0\"][\"c\"], list)\n\n\ndef test_set_new_list_path_with_separator():\n    # This test kills many birds with one stone, forgive me\n    dict = {\n        \"a\": {\n        },\n    }\n\n    dpath.new(dict, ['a', 'b/c/d', 0], 1)\n    assert len(dict['a']) == 1\n    assert len(dict['a']['b/c/d']) == 1\n    assert dict['a']['b/c/d'][0] == 1\n\n\ndef test_set_new_list_integer_path_with_creator():\n    d = {}\n\n    def mycreator(obj, pathcomp, nextpathcomp, hints):\n        print(hints)\n        print(pathcomp)\n        print(nextpathcomp)\n        print(\"...\")\n\n        target = pathcomp[0]\n        if isinstance(obj, list) and (target.isdigit()):\n            target = int(target)\n\n        if ((nextpathcomp is not None) and (isinstance(nextpathcomp, int) or str(nextpathcomp).isdigit())):\n            obj[target] = [None] * (int(nextpathcomp) + 1)\n            print(\"Created new list in target\")\n        else:\n            print(\"Created new dict in target\")\n            obj[target] = {}\n        print(obj)\n\n    dpath.new(d, '/a/2', 3, creator=mycreator)\n    print(d)\n    assert isinstance(d['a'], list)\n    assert len(d['a']) == 3\n    assert d['a'][2] == 3\n"
  },
  {
    "path": "tests/test_path_get.py",
    "content": "import dpath.segments\nimport dpath.exceptions\n\n\ndef test_path_get_list_of_dicts():\n    tdict = {\n        \"a\": {\n            \"b\": [\n                {0: 0},\n                {0: 1},\n                {0: 2},\n            ],\n        },\n    }\n    segments = ['a', 'b', 0, 0]\n\n    res = dpath.segments.view(tdict, segments)\n    assert isinstance(res['a']['b'], list)\n    assert len(res['a']['b']) == 1\n    assert res['a']['b'][0][0] == 0\n"
  },
  {
    "path": "tests/test_path_paths.py",
    "content": "from nose2.tools.such import helper\n\nimport dpath.segments\nimport dpath.exceptions\nimport dpath.options\n\n\ndef test_path_paths_empty_key_disallowed():\n    tdict = {\n        \"Empty\": {\n            \"\": {\n                \"Key\": \"\"\n            }\n        }\n    }\n\n    with helper.assertRaises(dpath.exceptions.InvalidKeyName):\n        for x in dpath.segments.walk(tdict):\n            pass\n\n\ndef test_path_paths_empty_key_allowed():\n    tdict = {\n        \"Empty\": {\n            \"\": {\n                \"Key\": \"\"\n            }\n        }\n    }\n\n    segments = []\n    dpath.options.ALLOW_EMPTY_STRING_KEYS = True\n\n    for segments, value in dpath.segments.leaves(tdict):\n        pass\n\n    dpath.options.ALLOW_EMPTY_STRING_KEYS = False\n    assert \"/\".join(segments) == \"Empty//Key\"\n"
  },
  {
    "path": "tests/test_paths.py",
    "content": "import dpath\n\n\ndef test_util_safe_path_list():\n    res = dpath._split_path([\"Ignore\", \"the/separator\"], None)\n\n    assert len(res) == 2\n    assert res[0] == \"Ignore\"\n    assert res[1] == \"the/separator\"\n"
  },
  {
    "path": "tests/test_search.py",
    "content": "import dpath\n\n\ndef test_search_paths_with_separator():\n    dict = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n    paths = [\n        'a',\n        'a;b',\n        'a;b;c',\n        'a;b;c;d',\n        'a;b;c;e',\n        'a;b;c;f',\n    ]\n\n    for (path, value) in dpath.search(dict, '/**', yielded=True, separator=\";\"):\n        assert path in paths\n\n    for (path, value) in dpath.search(dict, ['**'], yielded=True, separator=\";\"):\n        assert path in paths\n\n\ndef test_search_paths():\n    dict = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n    paths = [\n        'a',\n        'a/b',\n        'a/b/c',\n        'a/b/c/d',\n        'a/b/c/e',\n        'a/b/c/f',\n    ]\n\n    for (path, value) in dpath.search(dict, '/**', yielded=True):\n        assert path in paths\n\n    for (path, value) in dpath.search(dict, ['**'], yielded=True):\n        assert path in paths\n\n\ndef test_search_afilter():\n    def afilter(x):\n        if x in [1, 2]:\n            return True\n        return False\n\n    dict = {\n        \"a\": {\n            \"view_failure\": \"a\",\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n    paths = [\n        'a/b/c/e',\n        'a/b/c/f',\n    ]\n\n    for (path, value) in dpath.search(dict, '/**', yielded=True, afilter=afilter):\n        assert path in paths\n    assert \"view_failure\" not in dpath.search(dict, '/**', afilter=afilter)['a']\n    assert \"d\" not in dpath.search(dict, '/**', afilter=afilter)['a']['b']['c']\n\n    for (path, value) in dpath.search(dict, ['**'], yielded=True, afilter=afilter):\n        assert path in paths\n    assert \"view_failure\" not in dpath.search(dict, ['**'], afilter=afilter)['a']\n    assert \"d\" not in dpath.search(dict, ['**'], afilter=afilter)['a']['b']['c']\n\n\ndef test_search_globbing():\n    dict = {\n        \"a\": {\n            \"b\": {\n                \"c\": {\n                    \"d\": 0,\n                    \"e\": 1,\n                    \"f\": 2,\n                },\n            },\n        },\n    }\n    paths = [\n        'a/b/c/d',\n        'a/b/c/f',\n    ]\n\n    for (path, value) in dpath.search(dict, '/a/**/[df]', yielded=True):\n        assert path in paths\n\n    for (path, value) in dpath.search(dict, ['a', '**', '[df]'], yielded=True):\n        assert path in paths\n\n\ndef test_search_return_dict_head():\n    tdict = {\n        \"a\": {\n            \"b\": {\n                0: 0,\n                1: 1,\n                2: 2,\n            },\n        },\n    }\n    res = dpath.search(tdict, '/a/b')\n    assert isinstance(res['a']['b'], dict)\n    assert len(res['a']['b']) == 3\n    assert res['a']['b'] == {0: 0, 1: 1, 2: 2}\n\n    res = dpath.search(tdict, ['a', 'b'])\n    assert isinstance(res['a']['b'], dict)\n    assert len(res['a']['b']) == 3\n    assert res['a']['b'] == {0: 0, 1: 1, 2: 2}\n\n\ndef test_search_return_dict_globbed():\n    tdict = {\n        \"a\": {\n            \"b\": {\n                0: 0,\n                1: 1,\n                2: 2,\n            },\n        },\n    }\n\n    res = dpath.search(tdict, '/a/b/[02]')\n    assert isinstance(res['a']['b'], dict)\n    assert len(res['a']['b']) == 2\n    assert res['a']['b'] == {0: 0, 2: 2}\n\n    res = dpath.search(tdict, ['a', 'b', '[02]'])\n    assert isinstance(res['a']['b'], dict)\n    assert len(res['a']['b']) == 2\n    assert res['a']['b'] == {0: 0, 2: 2}\n\n\ndef test_search_return_list_head():\n    tdict = {\n        \"a\": {\n            \"b\": [\n                0,\n                1,\n                2,\n            ],\n        },\n    }\n\n    res = dpath.search(tdict, '/a/b')\n    assert isinstance(res['a']['b'], list)\n    assert len(res['a']['b']) == 3\n    assert res['a']['b'] == [0, 1, 2]\n\n    res = dpath.search(tdict, ['a', 'b'])\n    assert isinstance(res['a']['b'], list)\n    assert len(res['a']['b']) == 3\n    assert res['a']['b'] == [0, 1, 2]\n\n\ndef test_search_return_list_globbed():\n    tdict = {\n        \"a\": {\n            \"b\": [\n                0,\n                1,\n                2,\n            ]\n        }\n    }\n\n    res = dpath.search(tdict, '/a/b/[02]')\n    assert isinstance(res['a']['b'], list)\n    assert len(res['a']['b']) == 3\n    assert res['a']['b'] == [0, None, 2]\n\n    res = dpath.search(tdict, ['a', 'b', '[02]'])\n    assert isinstance(res['a']['b'], list)\n    assert len(res['a']['b']) == 3\n    assert res['a']['b'] == [0, None, 2]\n\n\ndef test_search_list_key_with_separator():\n    tdict = {\n        \"a\": {\n            \"b\": {\n                \"d\": 'failure',\n            },\n            \"/b/d\": 'success',\n        },\n    }\n\n    res = dpath.search(tdict, ['a', '/b/d'])\n    assert 'b' not in res['a']\n    assert res['a']['/b/d'] == 'success'\n\n\ndef test_search_multiple_stars():\n    testdata = {\n        'a': [\n            {\n                'b': [\n                    {'c': 1},\n                    {'c': 2},\n                    {'c': 3},\n                ],\n            },\n        ],\n    }\n    testpath = 'a/*/b/*/c'\n\n    res = dpath.search(testdata, testpath)\n    assert len(res['a'][0]['b']) == 3\n    assert res['a'][0]['b'][0]['c'] == 1\n    assert res['a'][0]['b'][1]['c'] == 2\n    assert res['a'][0]['b'][2]['c'] == 3\n\n\ndef test_search_negative_index():\n    d = {'a': {'b': [1, 2, 3]}}\n    res = dpath.search(d, 'a/b/-1')\n\n    assert res == dpath.search(d, \"a/b/2\")\n"
  },
  {
    "path": "tests/test_segments.py",
    "content": "import os\nfrom unittest import TestCase\n\nimport hypothesis.strategies as st\nfrom hypothesis import given, assume, settings, HealthCheck\n\nimport dpath.segments as api\nfrom dpath import options\n\nsettings.register_profile(\"default\", suppress_health_check=(HealthCheck.too_slow,))\nsettings.load_profile(os.getenv(u'HYPOTHESIS_PROFILE', 'default'))\n\nrandom_key_int = st.integers(0, 1000)\nrandom_key_str = st.binary() | st.text()\nrandom_key = random_key_str | random_key_int\nrandom_segments = st.lists(random_key)\nrandom_leaf = st.integers() | st.floats() | st.booleans() | st.binary() | st.text() | st.none()\n\nrandom_thing = st.recursive(\n    random_leaf,\n    lambda children: st.lists(children) | st.tuples(children) | st.dictionaries(st.binary() | st.text(), children),\n    max_leaves=100\n)\nrandom_node = random_thing.filter(lambda thing: isinstance(thing, (list, tuple, dict)))\n\nrandom_mutable_thing = st.recursive(\n    random_leaf,\n    lambda children: st.lists(children) | st.dictionaries(st.binary() | st.text(), children)\n)\nrandom_mutable_node = random_mutable_thing.filter(lambda thing: isinstance(thing, (list, dict)))\n\n\n@st.composite\ndef mutate(draw, segment):\n    # Convert number segments.\n    segment = api.int_str(segment)\n\n    # Infer the type constructor for the result.\n    kind = type(segment)\n\n    # Produce a valid kind conversion for our wildcards.\n    if isinstance(segment, bytes):\n        def to_kind(v):\n            try:\n                return bytes(v, 'utf-8')\n            except:\n                return kind(v)\n    else:\n        def to_kind(v):\n            return kind(v)\n\n    # Convert to an list of single values.\n    converted = []\n    for i in range(len(segment)):\n        # This carefully constructed nonsense to get a single value\n        # is necessary to work around limitations in the bytes type\n        # iteration returning integers instead of byte strings of\n        # length 1.\n        c = segment[i:i + 1]\n\n        # Check for values that need to be escaped.\n        if c in tuple(map(to_kind, ('*', '?', '[', ']'))):\n            c = to_kind('[') + c + to_kind(']')\n\n        converted.append(c)\n\n    # Start with a non-mutated result.\n    result = converted\n\n    # 50/50 chance we will attempt any mutation.\n    change = draw(st.sampled_from((True, False)))\n    if change:\n        result = []\n\n        # For every value in segment maybe mutate, maybe not.\n        for c in converted:\n            # If the length isn't 1 then, we know this value is already\n            # an escaped special character. We will not mutate these.\n            if len(c) != 1:\n                result.append(c)\n            else:\n                result.append(draw(st.sampled_from((c, to_kind('?'), to_kind('*')))))\n\n    combined = kind().join(result)\n\n    # If we by chance produce the star-star result, then just revert\n    # back to the original converted segment. This is not the mutation\n    # you are looking for.\n    if combined == to_kind('**'):\n        combined = kind().join(converted)\n\n    return combined\n\n\n@st.composite\ndef random_segments_with_glob(draw):\n    segments = draw(random_segments)\n    glob = list(map(lambda x: draw(mutate(x)), segments))\n\n    # 50/50 chance we will attempt to add a star-star to the glob.\n    use_ss = draw(st.sampled_from((True, False)))\n    if use_ss:\n        # Decide if we are inserting a new segment or replacing a range.\n        insert_ss = draw(st.sampled_from((True, False)))\n        if insert_ss:\n            index = draw(st.integers(0, len(glob)))\n            glob.insert(index, '**')\n        else:\n            start = draw(st.integers(0, len(glob)))\n            stop = draw(st.integers(start, len(glob)))\n            glob[start:stop] = ['**']\n\n    return segments, glob\n\n\n@st.composite\ndef random_segments_with_nonmatching_glob(draw):\n    (segments, glob) = draw(random_segments_with_glob())\n\n    # Generate a segment that is not in segments.\n    invalid = draw(random_key.filter(lambda x: x not in segments and x not in ('*', '**')))\n\n    # Do we just have a star-star glob? It matches everything, so we\n    # need to replace it entirely.\n    if len(glob) == 1 and glob[0] == '**':\n        glob = [invalid]\n    # Do we have a star glob and only one segment? It matches anything\n    # in the segment, so we need to replace it entirely.\n    elif len(glob) == 1 and glob[0] == '*' and len(segments) == 1:\n        glob = [invalid]\n    # Otherwise we can add something we know isn't in the segments to\n    # the glob.\n    else:\n        index = draw(st.integers(0, len(glob)))\n        glob.insert(index, invalid)\n\n    return (segments, glob)\n\n\n@st.composite\ndef random_walk(draw):\n    node = draw(random_mutable_node)\n    found = tuple(api.walk(node))\n    assume(len(found) > 0)\n    return (node, draw(st.sampled_from(found)))\n\n\n@st.composite\ndef random_leaves(draw):\n    node = draw(random_mutable_node)\n    found = tuple(api.leaves(node))\n    assume(len(found) > 0)\n    return (node, draw(st.sampled_from(found)))\n\n\nclass TestSegments(TestCase):\n    @classmethod\n    def setUpClass(cls):\n        # Allow empty strings in segments.\n        options.ALLOW_EMPTY_STRING_KEYS = True\n\n    @classmethod\n    def tearDownClass(cls):\n        # Revert back to default.\n        options.ALLOW_EMPTY_STRING_KEYS = False\n\n    @given(random_node)\n    def test_kvs(self, node):\n        '''\n        Given a node, kvs should produce a key that when used to extract\n        from the node renders the exact same value given.\n        '''\n        for k, v in api.make_walkable(node):\n            assert node[k] is v\n\n    @given(random_leaf)\n    def test_leaf_with_leaf(self, leaf):\n        '''\n        Given a leaf, leaf should return True.\n        '''\n        assert api.leaf(leaf) is True\n\n    @given(random_node)\n    def test_leaf_with_node(self, node):\n        '''\n        Given a node, leaf should return False.\n        '''\n        assert api.leaf(node) is False\n\n    @given(random_thing)\n    def test_walk(self, thing):\n        '''\n        Given a thing to walk, walk should yield key, value pairs where key\n        is a tuple of non-zero length.\n        '''\n        for k, v in api.walk(thing):\n            assert isinstance(k, tuple)\n            assert len(k) > 0\n\n    @given(random_node)\n    def test_get(self, node):\n        '''\n        Given a node, get should return the exact value given a key for all\n        key, value pairs in the node.\n        '''\n        for k, v in api.walk(node):\n            assert api.get(node, k) is v\n\n    @given(random_node)\n    def test_has(self, node):\n        '''\n        Given a node, has should return True for all paths, False otherwise.\n        '''\n        for k, v in api.walk(node):\n            assert api.has(node, k) is True\n\n            # If we are at a leaf, then we can create a value that isn't\n            # present easily.\n            if api.leaf(v):\n                assert api.has(node, k + (0,)) is False\n\n    @given(random_segments)\n    def test_expand(self, segments):\n        '''\n        Given segments expand should produce as many results are there were\n        segments and the last result should equal the given segments.\n        '''\n        count = len(segments)\n        result = list(api.expand(segments))\n\n        assert count == len(result)\n\n        if count > 0:\n            assert segments == result[-1]\n\n    @given(random_node)\n    def test_types(self, node):\n        '''\n        Given a node, types should yield a tuple of key, type pairs and the\n        type indicated should equal the type of the value.\n        '''\n        for k, v in api.walk(node):\n            ts = api.types(node, k)\n            ta = ()\n            for tk, tt in ts:\n                ta += (tk,)\n                assert type(api.get(node, ta)) is tt\n\n    @given(random_node)\n    def test_leaves(self, node):\n        '''\n        Given a node, leaves should yield only leaf key, value pairs.\n        '''\n        for k, v in api.leaves(node):\n            assert api.leafy(v)\n\n    @given(random_segments_with_glob())\n    def test_match(self, pair):\n        '''\n        Given segments and a known good glob, match should be True.\n        '''\n        (segments, glob) = pair\n        assert api.match(segments, glob) is True\n\n    @given(random_segments_with_nonmatching_glob())\n    def test_match_nonmatching(self, pair):\n        '''\n        Given segments and a known bad glob, match should be False.\n        '''\n        (segments, glob) = pair\n        assert api.match(segments, glob) is False\n\n    @given(walkable=random_walk(), value=random_thing)\n    def test_set_walkable(self, walkable, value):\n        '''\n        Given a walkable location, set should be able to update any value.\n        '''\n        (node, (segments, found)) = walkable\n        api.set(node, segments, value)\n        assert api.get(node, segments) is value\n\n    @given(walkable=random_leaves(),\n           kstr=random_key_str,\n           kint=random_key_int,\n           value=random_thing,\n           extension=random_segments)\n    def test_set_create_missing(self, walkable, kstr, kint, value, extension):\n        '''\n        Given a walkable non-leaf, set should be able to create missing\n        nodes and set a new value.\n        '''\n        (node, (segments, found)) = walkable\n        assume(api.leaf(found))\n\n        parent_segments = segments[:-1]\n        parent = api.get(node, parent_segments)\n\n        if isinstance(parent, list):\n            assume(len(parent) < kint)\n            destination = parent_segments + (kint,) + tuple(extension)\n        elif isinstance(parent, dict):\n            assume(kstr not in parent)\n            destination = parent_segments + (kstr,) + tuple(extension)\n        else:\n            raise Exception('mad mad world')\n\n        api.set(node, destination, value)\n        assert api.get(node, destination) is value\n\n    @given(thing=random_thing)\n    def test_fold(self, thing):\n        '''\n        Given a thing, count paths with fold.\n        '''\n\n        def f(o, p, a):\n            a[0] += 1\n\n        [count] = api.fold(thing, f, [0])\n        assert count == len(tuple(api.walk(thing)))\n\n    @given(walkable=random_walk())\n    def test_view(self, walkable):\n        '''\n        Given a walkable location, view that location.\n        '''\n        (node, (segments, found)) = walkable\n        assume(found == found)  # Hello, nan! We don't want you here.\n\n        view = api.view(node, segments)\n        assert api.get(view, segments) == api.get(node, segments)\n"
  },
  {
    "path": "tests/test_set.py",
    "content": "import dpath\n\n\ndef test_set_existing_separator():\n    dict = {\n        \"a\": {\n            \"b\": 0,\n        },\n    }\n\n    dpath.set(dict, ';a;b', 1, separator=\";\")\n    assert dict['a']['b'] == 1\n\n    dict['a']['b'] = 0\n    dpath.set(dict, ['a', 'b'], 1, separator=\";\")\n    assert dict['a']['b'] == 1\n\n\ndef test_set_existing_dict():\n    dict = {\n        \"a\": {\n            \"b\": 0,\n        },\n    }\n\n    dpath.set(dict, '/a/b', 1)\n    assert dict['a']['b'] == 1\n\n    dict['a']['b'] = 0\n    dpath.set(dict, ['a', 'b'], 1)\n    assert dict['a']['b'] == 1\n\n\ndef test_set_existing_list():\n    dict = {\n        \"a\": [\n            0,\n        ],\n    }\n\n    dpath.set(dict, '/a/0', 1)\n    assert dict['a'][0] == 1\n\n    dict['a'][0] = 0\n    dpath.set(dict, ['a', '0'], 1)\n    assert dict['a'][0] == 1\n\n\ndef test_set_filter():\n    def afilter(x):\n        if int(x) == 31:\n            return True\n        return False\n\n    dict = {\n        \"a\": {\n            \"b\": 0,\n            \"c\": 1,\n            \"d\": 31,\n        }\n    }\n\n    dpath.set(dict, '/a/*', 31337, afilter=afilter)\n    assert dict['a']['b'] == 0\n    assert dict['a']['c'] == 1\n    assert dict['a']['d'] == 31337\n\n    dict = {\n        \"a\": {\n            \"b\": 0,\n            \"c\": 1,\n            \"d\": 31,\n        }\n    }\n\n    dpath.set(dict, ['a', '*'], 31337, afilter=afilter)\n    assert dict['a']['b'] == 0\n    assert dict['a']['c'] == 1\n    assert dict['a']['d'] == 31337\n\n\ndef test_set_existing_path_with_separator():\n    dict = {\n        \"a\": {\n            'b/c/d': 0,\n        },\n    }\n\n    dpath.set(dict, ['a', 'b/c/d'], 1)\n    assert len(dict['a']) == 1\n    assert dict['a']['b/c/d'] == 1\n"
  },
  {
    "path": "tests/test_types.py",
    "content": "from collections.abc import MutableSequence, MutableMapping\n\nfrom nose2.tools.such import helper\n\nimport dpath\nfrom dpath import MergeType\n\n\nclass TestMapping(MutableMapping):\n    def __init__(self, data=None):\n        if data is None:\n            data = {}\n\n        self._mapping = {}\n        self._mapping.update(data)\n\n    def __len__(self):\n        return len(self._mapping)\n\n    def __iter__(self):\n        return iter(self._mapping)\n\n    def __contains__(self, key):\n        return key in self._mapping\n\n    def __getitem__(self, key):\n        return self._mapping[key]\n\n    def __setitem__(self, key, value):\n        self._mapping[key] = value\n\n    def __delitem__(self, key):\n        del self._mapping[key]\n\n\nclass TestSequence(MutableSequence):\n    def __init__(self, data=None):\n        if data is None:\n            data = list()\n\n        self._list = [] + data\n\n    def __len__(self):\n        return len(self._list)\n\n    def __getitem__(self, idx):\n        return self._list[idx]\n\n    def __delitem__(self, idx):\n        del self._list[idx]\n\n    def __setitem__(self, idx, value):\n        self._list[idx] = value\n\n    def __str__(self):\n        return str(self._list)\n\n    def __eq__(self, other):\n        return self._list == other._list\n\n    def __ne__(self, other):\n        return not self.__eq__(other)\n\n    def insert(self, idx, value):\n        self._list.insert(idx, value)\n\n    def append(self, value):\n        self.insert(len(self._list), value)\n\n\ndef test_types_set():\n    data = TestMapping({\"a\": TestSequence([0])})\n\n    dpath.set(data, '/a/0', 1)\n    assert data['a'][0] == 1\n\n    data['a'][0] = 0\n\n    dpath.set(data, ['a', '0'], 1)\n    assert data['a'][0] == 1\n\n\ndef test_types_get_list_of_dicts():\n    tdict = TestMapping({\n        \"a\": TestMapping({\n            \"b\": TestSequence([\n                {0: 0},\n                {0: 1},\n                {0: 2},\n            ]),\n        }),\n    })\n\n    res = dpath.segments.view(tdict, ['a', 'b', 0, 0])\n\n    assert isinstance(res['a']['b'], TestSequence)\n    assert len(res['a']['b']) == 1\n    assert res['a']['b'][0][0] == 0\n\n\ndef test_types_merge_simple_list_replace():\n    src = TestMapping({\n        \"list\": TestSequence([7, 8, 9, 10])\n    })\n    dst = TestMapping({\n        \"list\": TestSequence([0, 1, 2, 3])\n    })\n\n    dpath.merge(dst, src, flags=MergeType.REPLACE)\n    assert dst[\"list\"] == TestSequence([7, 8, 9, 10]), \"%r != %r\" % (dst[\"list\"], TestSequence([7, 8, 9, 10]))\n\n\ndef test_types_get_absent():\n    ehash = TestMapping()\n    helper.assertRaises(KeyError, dpath.get, ehash, '/a/b/c/d/f')\n    helper.assertRaises(KeyError, dpath.get, ehash, ['a', 'b', 'c', 'd', 'f'])\n\n\ndef test_types_get_glob_multiple():\n    ehash = TestMapping({\n        \"a\": TestMapping({\n            \"b\": TestMapping({\n                \"c\": TestMapping({\n                    \"d\": 0,\n                }),\n                \"e\": TestMapping({\n                    \"d\": 0,\n                }),\n            }),\n        }),\n    })\n\n    helper.assertRaises(ValueError, dpath.get, ehash, '/a/b/*/d')\n    helper.assertRaises(ValueError, dpath.get, ehash, ['a', 'b', '*', 'd'])\n\n\ndef test_delete_filter():\n    def afilter(x):\n        if int(x) == 31:\n            return True\n        return False\n\n    data = TestMapping({\n        \"a\": TestMapping({\n            \"b\": 0,\n            \"c\": 1,\n            \"d\": 31,\n        }),\n    })\n\n    dpath.delete(data, '/a/*', afilter=afilter)\n    assert data['a']['b'] == 0\n    assert data['a']['c'] == 1\n    assert 'd' not in data['a']\n"
  },
  {
    "path": "tests/test_unicode.py",
    "content": "import dpath\n\n\ndef test_unicode_merge():\n    a = {'中': 'zhong'}\n    b = {'文': 'wen'}\n\n    dpath.merge(a, b)\n    assert len(a.keys()) == 2\n    assert a['中'] == 'zhong'\n    assert a['文'] == 'wen'\n\n\ndef test_unicode_search():\n    a = {'中': 'zhong'}\n\n    results = [[x[0], x[1]] for x in dpath.search(a, '*', yielded=True)]\n    assert len(results) == 1\n    assert results[0][0] == '中'\n    assert results[0][1] == 'zhong'\n\n\ndef test_unicode_str_hybrid():\n    a = {'first': u'1'}\n    b = {u'second': '2'}\n\n    dpath.merge(a, b)\n    assert len(a.keys()) == 2\n    assert a[u'second'] == '2'\n    assert a['second'] == u'2'\n    assert a[u'first'] == '1'\n    assert a['first'] == u'1'\n"
  },
  {
    "path": "tox.ini",
    "content": "# Tox (http://tox.testrun.org/) is a tool for running tests\n# in multiple virtualenvs. This configuration file will run the\n# test suite on all supported python versions. To use it, \"pip install tox\"\n# and then run \"tox\" from this directory.\n\n[flake8]\nignore = E501,E722\n\n[tox]\nenvlist = pypy37, py38, py39, py310, py311, py312\n\n[gh-actions]\npython =\n    pypy-3.7: pypy37\n    3.8: py38\n    3.9: py39\n    3.10: py310\n    3.11: py311\n    3.12: py312\n\n[testenv]\ndeps =\n    hypothesis\n    nose2\ncommands = nose2 {posargs}\n"
  }
]