{"id":15896225,"url":"https://github.com/numberoverzero/texas","last_synced_at":"2025-04-02T18:25:59.344Z","repository":{"id":142804095,"uuid":"51989541","full_name":"numberoverzero/texas","owner":"numberoverzero","description":"nested dictionaries with paths","archived":false,"fork":false,"pushed_at":"2017-03-11T21:41:01.000Z","size":63,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-08T09:12:20.495Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/numberoverzero.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-02-18T07:43:22.000Z","updated_at":"2017-06-23T05:48:43.000Z","dependencies_parsed_at":"2023-04-12T07:46:40.679Z","dependency_job_id":null,"html_url":"https://github.com/numberoverzero/texas","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Ftexas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Ftexas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Ftexas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/numberoverzero%2Ftexas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/numberoverzero","download_url":"https://codeload.github.com/numberoverzero/texas/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246867760,"owners_count":20846809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-06T09:07:02.310Z","updated_at":"2025-04-02T18:25:59.314Z","avatar_url":"https://github.com/numberoverzero.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":".. image:: https://img.shields.io/travis/numberoverzero/texas/master.svg?style=flat-square\n    :target: https://travis-ci.org/numberoverzero/texas\n.. image:: https://img.shields.io/codecov/c/github/numberoverzero/texas/master.svg?style=flat-square\n    :target: https://codecov.io/gh/numberoverzero/texas/branch/master\n.. image:: https://img.shields.io/pypi/v/texas.svg?style=flat-square\n    :target: https://pypi.python.org/pypi/texas\n.. image:: https://img.shields.io/github/issues-raw/numberoverzero/texas.svg?style=flat-square\n    :target: https://github.com/numberoverzero/texas/issues\n.. image:: https://img.shields.io/pypi/l/texas.svg?style=flat-square\n    :target: https://github.com/numberoverzero/texas/blob/master/LICENSE\n\nPure python.  Path keys.  ChainedMap on steroids.\n\nInstallation\n============\n\n::\n\n    pip install texas\n\nQuick Start\n===========\n\n::\n\n    import texas\n\n    context = texas.Context()\n\n    environment = context.include(\"environment\")\n    cli = context.include(\"cli\")\n\n    config = context.include(\"environment\", \"cli\")\n\n    environment[\"src.root\"] = \"~/pics\"\n    cli[\"src.type\"] = \"jpg\"\n\n    config[\"src.root\"]  # ~/pics\n    config[\"src.type\"]  # jpg\n\n    # Change cli's root\n    cli[\"src.root\"] = \"~/other\"\n\n    # Doesn't change the underlying environment root\n    environment[\"src.root\"]  # ~/pics\n\n    # Modifies cli, which is the top context in config\n    del config[\"src.root\"]\n    config[\"src.root\"]  # ~/pics\n\n    # Snapshot the contexts into a single dict for use in modules that\n    # typecheck against dict (instead of collections.abc.Mapping)\n    import pprint\n    pprint.pprint(config.snapshot)\n    # {\n    #     \"src\": {\n    #         \"root\": \"~/pics\",\n    #         \"type\": \"jpg\"\n    #     }\n    # }\n\nUsage\n=====\n\nContexts are namespaced python dictionaries with (configurable) path lookups::\n\n    import texas\n\n    context = texas.Context()\n    # Single context\n    root = context.include(\"root\")\n\n    # normal dictionary operations\n    root[\"foo\"] = \"bar\"\n    assert \"bar\" == root[\"foo\"]\n    del root[\"foo\"]\n\n    # paths\n    root[\"foo.bar\"] = \"baz\"\n    assert \"baz\" == root[\"foo.bar\"]\n    del root[\"foo.bar\"]\n\nInclude\n-------\n\nInclude takes a variable number of context names to load into a view::\n\n    bottom = context.include(\"bottom\")\n    top = context.include(\"top\")\n\n    both = context.include(\"bottom\", \"top\")\n\nThis can be used to create a priority when looking up values.  The top of the\ncontext stack will be checked for a key first, then the next, until a context\nwith the given key is found::\n\n    bottom[\"key\"] = \"bottom\"\n    assert both[\"key\"] == \"bottom\"\n\n    top[\"key\"] = \"top\"\n    assert both[\"key\"] == \"top\"\n\nCombined with paths, this can be very powerful for configuration management::\n\n    context = texas.Context()\n    env = context.include(\"env\")\n    cli = context.include(\"cli\")\n    config = context.include(\"env\", \"cli\")\n\n    env[\"src.root\"] = \"~/pics\"\n    cli[\"src.type\"] = \"jpg\"\n\n    assert config[\"src.root\"] == \"~/pics\"\n    assert config[\"src.type\"] == \"jpg\"\n\nThis even works with individual path segments, since ContextView returns\nproxies against the underlying mapping objects::\n\n    config[\"src\"]  # \u003ctexas.context.ContextView at ... \u003e\n    config[\"src\"][\"type\"]  # \"jpg\"\n\nSetting values only applies to the top context in the view, so the value in\nbottom is still the same::\n\n    assert bottom[\"key\"] == \"bottom\"\n\nThis breaks down with mutable values - for instance, this will modify the list\nin the bottom context::\n\n    context = texas.Context()\n    bottom = context.include(\"bottom\")\n    top = context.include(\"top\")\n    both = context.include(\"bottom\", \"top\")\n\n    bottom[\"list\"] = []\n    top[\"list\"].append(\"modified!\")\n\n    assert bottom[\"list\"] == [\"modified!\"]\n\nSnapshot\n--------\n\nContext does some heavy lifting to make paths and multiple dicts work together\ncomfortably.  Unfortunately, some libraries make ``isinstance`` checks against\n``dict``, and not ``collections.abc.Mapping``.\n\nThis is also useful when passing a ContextView to code that will perform many\nlookups in a tight loop.  Because an intermediate lookup on a deeply nested\nset of dicts creates one proxy per level (ie.\n``something[\"foo\"][\"bar\"][\"baz\"]`` creates two proxies for the value\n``something[\"foo.bar.baz\"] = \"blah\"``) it can be a significant speedup to\n\"snapshot\" or bake the ContextView for much faster reading.\n\nMerging dicts in general is a complex problem at best, with many ambiguities.\nTo simplify things, the following rules are used::\n\n    (1) For every key in each context, the top-most[0] context that contains\n        that key will determine if the value will be used directly, or merged\n        with other contexts.\n    (2) If that value is a collections.abc.Mapping, the value of that key in\n        each context that contains that key will be merged.\n        (A) If there is a context with that key whose value is NOT a mapping,\n            its value will be ignored.\n        (B) If that value is NOT a collections.abc.Mapping, the value will be\n            used directly and no merging occurs[1].\n    3) These rules are applied recursively[2] for any nested mappings.\n\nThe \"top-most context that contains that key\" is not always the top context.\nIn the following, the bottom context is the only one that contains the key\n\"bottom\"::\n\n    {\n        \"bottom\": \"bottom-value\"\n    },\n    {\n        \"top\": \"top-value\"\n    }\n\n    Snapshot:\n\n    {\n        \"bottom\": \"bottom-value\",\n        \"top\": \"top-value\"\n    }\n\nWhen there is a conflict in type (mapping, non-mapping) the top-most context\ndetermines the type.  For example, this will take the mapping values from\nbottom and top, but not middle (whose value is not a mapping)::\n\n    {\n        \"key\": {\n            \"bottom\": \"bottom-value\"\n        }\n    },\n    {\n        \"key\": [\"middle\", \"non\", \"mapping\"]\n    },\n    {\n        \"key\": {\n            \"top\": \"top-value\"\n        }\n    }\n\n    Snapshot:\n\n    {\n        \"key\": {\n            \"bottom\": \"bottom-value\",\n            \"top\": \"top-value\"\n        }\n    }\n\nWhile snapshot applies its rules recursively to mappings, the implementation is\nnot recursive.  A sample file that merges arbitrary iterables of mappings using\nthe same rules as texas is available\n`here \u003chttps://gist.github.com/numberoverzero/90a36aef936e6dd5a6c4#file-merge-py\u003e`_.\n\nContext Factory\n---------------\n\nBy default, texas uses simple ``dict``\\s for storage.  However, this can be\ncustomized with the ``context_factory`` function, such as using a\n``collections.OrderedDict`` or pre-loading values into the node.\n\nThis function is used when creating snapshots, the context root, new contexts,\nand intermediate segments when setting values by paths.\n\n::\n\n    created = 0\n\n    def factory():\n        global created\n        created += 1\n        return dict()\n\n    # Root context container\n    context = texas.Context(context_factory=factory)\n    assert created == 1\n\n    # Including contexts\n    ctx = context.include(\"some-context\")\n    assert created == 2\n\n    # Segments along a path when setting values\n    ctx[\"foo.bar\"] = \"value\"\n    assert created == 3\n\nInternals\n---------\n\nInternally, all data is stored in python dicts.  You can inspect the global\nstate of a context through its ``contexts`` attribute::\n\n    import texas\n    context = texas.Context()\n\n    context.include(\"root.something.or.foo\")\n    context.include(\"bar\", \"and.yet.another.foo\", \"finally\")\n\n    print(context._contexts)\n\nPath traversal is performed by the ``traverse`` function, which only handles\ntraversal of ``collestions.abc.Mapping``.  Therefore, when a non-mapping value\nis expected at the end of a path, the path should be split like so::\n\n    full_path = \"foo.bar.baz\"\n    path, last = full_path.rsplit(\".\", 1)\n\n    assert path == \"foo.bar\"\n    assert last = \"baz\"\n\nThis allows us to travers a root and create the intermediate ``foo`` and\n``bar`` dicts without modifying or inspecting ``baz``::\n\n    from texas.traversal import traverse, create_on_missing\n\n    root = dict()\n    full_path = \"foo.bar.baz\"\n    path, key = full_path.rsplit(\".\", 1)\n\n    node = traverse(root, path, \".\", create_on_missing(dict))\n    node[key] = \"value\"\n\n    assert root[\"foo\"][\"bar\"][\"baz\"] == \"value\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumberoverzero%2Ftexas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnumberoverzero%2Ftexas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnumberoverzero%2Ftexas/lists"}