{"id":19708763,"url":"https://github.com/bendudson/frozen-options","last_synced_at":"2025-06-18T22:41:03.464Z","repository":{"id":137712284,"uuid":"259615738","full_name":"bendudson/frozen-options","owner":"bendudson","description":"Immutable configuration data for python","archived":false,"fork":false,"pushed_at":"2020-05-01T09:05:04.000Z","size":14,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-10T13:26:56.396Z","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/bendudson.png","metadata":{"files":{"readme":"README.org","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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":"2020-04-28T11:20:15.000Z","updated_at":"2020-05-01T09:05:07.000Z","dependencies_parsed_at":null,"dependency_job_id":"ecb75403-428c-47c2-a993-33098015d0a4","html_url":"https://github.com/bendudson/frozen-options","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/bendudson%2Ffrozen-options","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bendudson%2Ffrozen-options/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bendudson%2Ffrozen-options/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bendudson%2Ffrozen-options/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bendudson","download_url":"https://codeload.github.com/bendudson/frozen-options/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241025774,"owners_count":19896506,"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-11-11T21:44:46.924Z","updated_at":"2025-02-27T14:47:08.592Z","avatar_url":"https://github.com/bendudson.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"* Frozen Options\n\nAn immutable dictionary-like structure, intended for configuration\noptions.  Similar to [[https://github.com/slezica/python-frozendict][frozendict]], and other similar projects. See [[https://www.python.org/dev/peps/pep-0416/][this\nrejected PEP]] for a partial list, or [[https://www.python.org/dev/peps/pep-0603/][this more recent PEP]].\n\nwith some differences:\n- Additional constraints on modifications through =__setitem__=\n- Access to values using =__getitem__= like [[https://pypi.org/project/bunch/][Bunch]]\n- Selectively extract values only for known keys\n- Drop (remove) keys\n- Handles changes to nested data in an immutable way\n\n** Intended use\n\nThe use case is where there are many components which have configuration\noptions, and settings need to be passed from e.g. the user down through\nseveral layers to the components. \n - Default settings are defined by the individual components which depend on them\n - Higher level components collect default values from sub-components\n - The user settings are filtered and passed down to the sub-components\n - Using immutable structures minimises unexpected side-effects when settings are\n   shared between components.\n\nIn this example class =C= depends on classes =A= and =B=\n#+BEGIN_SRC python :session example :results output\nfrom frozen_options import Options\n\nclass A:\n  defaults = Options(setting = 3)\n  def __init__(self, user_settings):\n    self.settings = self.defaults.takeValues(user_settings)\n\nclass B:\n  defaults = Options(greeting = \"hello\")\n  def __init__(self, user_settings):\n    self.settings = self.defaults.takeValues(user_settings)\n\nclass C:\n  defaults = Options(A.defaults, B.defaults,  # Collect defaults from sub-components\n                     answer = 42)\n  def __init__(self, user_settings, **kwargs):\n    self.settings = self.defaults.takeValues(user_settings, kwargs)\n    \n  def doSomething(self):\n    a = A(self.settings) # Pass settings down\n#+END_SRC\n\n=Options= also handles nested structures, so if name clashes need to be avoided\nthen class =C= could be defined as:\n#+BEGIN_SRC python :session example :results output\nclass C:\n  defaults = Options(A = A.defaults,  # Nested Options\n                     B = B.defaults,\n                     answer = 42)\n  def __init__(self, *args, **kwargs):\n    self.settings = self.defaults.takeValues(*args, kwargs)  # Merges nested Options\n    \n  def doSomething(self):\n    a = A(self.settings.A)\n\nc = C(A = {'setting':4}) # c.settings =\u003e Options(A=Options(setting=4), \n                         #                       B=Options(greeting='hello'),\n                         #                       answer=42)\n#+END_SRC\n\n** Creating Options\n\nOptions can be created by setting key-values\n#+BEGIN_SRC python :session options-example :results output\nfrom frozen_options import Options\n\noption = Options(value = 42, greeting = \"hello\")\nprint(option) # =\u003e {'value': 42, 'greeting': 'hello'}\n#+END_SRC\n\n#+RESULTS:\n: {'value': 42, 'greeting': 'hello'}\n\nwhich can be combined with dictionaries or other =dict= like mapping\ncollections, including other =Options=:\n#+BEGIN_SRC python :session options-example :results output\nmydict = {\"pi\":3.14, \"alpha\":0.007297}\n\noption = Options(mydict, another=\"something\")\n\nprint(option)  # =\u003e {'pi': 3.14, 'alpha': 0.007297, 'another': 'something'}\n#+END_SRC\n\n#+RESULTS:\n: {'pi': 3.14, 'alpha': 0.007297, 'another': 'something'}\n\n** Mutating options\n\nDon't! Once an =Options= object has been created, it can be passed around as a value,\nwithout fear that it will be modified. The following will all raise =TypeError= exceptions:\n#+BEGIN_SRC python :session options-example :results output\noption.changes = 8   # =\u003e TypeError\noption[\"something\"] = None  # =\u003e TypeError\ndel option[\"alpha\"] # =\u003e TypeError\n#+END_SRC\n\n#+RESULTS:\n\nNote: Because this is python, there is always a way to modify the data\nif you really want to.\n\n** Converting back to dictionary\n\nOptions implements =collections.Mapping=, so can be used in many places which expect\na =dict=. If needed, it can be converted back to a =dict=:\n#+BEGIN_SRC python :session options-example :results output\nmutable_data = dict(option)\n#+END_SRC\nThis creates a copy, so the result =mutable_data= can be modified\nwithout affecting =option=.\n\n** Transforming Options\n\nOptions can be changed by creating a new =Options= object. Two use\ncases are supported: A new =Options= object can be created as a union\nof other =Options=, dicts and keywords; and values can be taken for\nknown keys using =takeValues=.\n\n*** Adding keys and replacing values\n\n=Options= can be initialised with a combination of =Options=, =dict=\nand other mapping objects, and keywords. First the arguments are\nprocessed in order, merging nested mapping structures (trees of\nOptions or dicts). This can be used to transform values in nested\n=Options=. Keywords are processed last, and replace keys set earlier.\n#+BEGIN_SRC python :session options-example :results output\noptions = Options(value = 42, greeting = \"hello\")\n\nmore_options = Options(options, value = 3, pi = 3.14)\nprint(more_options) # =\u003e {'value': 3, 'greeting': 'hello', 'pi': 3.14}\n#+END_SRC\n\n#+RESULTS:\n: {'value': 3, 'greeting': 'hello', 'pi': 3.14}\n\nNote that the original =options= object is not changed. By combining\n=dicts=, =Options= or other mapping objects, =Options= initialisation\ncreates the union of these objects:\n\n#+BEGIN_SRC python :session options-example :results output\nmydict = {'pi':3.14, 'alpha':0.007297}\n\nmore_options = Options(options, mydict)\nprint(more_options)  # =\u003e {'value': 42, 'greeting': 'hello', 'pi': 3.14, 'alpha': 0.007297}\n#+END_SRC\n\n#+RESULTS:\n: {'value': 42, 'greeting': 'hello', 'pi': 3.14, 'alpha': 0.007297}\n\n*** Nested immutable Options\n\nBecause Options construction merges nested mapping structures, keys in nested\nstructures can be transformed, to an arbitrary depth:\n#+BEGIN_SRC python :session options-example :results output\noptions = Options(value = 42, \n                  nested = Options(greeting = \"hello\",\n                                   pi = 3.14))\n\n# Transform nested structure\nnew_options = Options(options, {'nested':{'pi':3, 'alpha': 0.007297}})\n\nprint(new_options) # =\u003e {'value': 42, 'nested': Options(greeting='hello', pi=3, alpha=0.007297)}\n#+END_SRC\n\n#+RESULTS:\n: {'value': 42, 'nested': Options(greeting='hello', pi=3, alpha=0.007297)}\n\nNote that here the nested =Options= has been transformed, modifying\nthe =pi= value, and adding the =alpha= key.  If instead we wanted\nto replace the nested options, rather than merging them, we could use\nthe keyword\n#+BEGIN_SRC python :session options-example :results output\nnew_options = Options(options, nested=Options(pi=3, alpha=0.007297))\n\nprint(new_options) # =\u003e {'value': 42, 'nested': Options(pi=3, alpha=0.007297)}\n#+END_SRC\n\n#+RESULTS:\n: {'value': 42, 'nested': Options(pi=3, alpha=0.007297)}\n\n*** Replacing values of known keys\n\nThe other use-case is where there is a collection of default options, and a user-supplied\ncollection of settings. Not all of the user settings may apply to a particular part of the\ncode, so here we just want to take the keys we know about from the user settings.\n#+BEGIN_SRC python :session options-example :results output\ndefault = Options(greeting = \"hello\", value = 3)\n\n# User supplies some settings, including options not needed here\nuser_settings = Options(value = 42, other_setting = \"Goodbye\")\n\nsettings = default.takeValues(user_settings)\nprint(settings) # =\u003e {'greeting': 'hello', 'value': 42}\n#+END_SRC\n\n#+RESULTS:\n: {'greeting': 'hello', 'value': 42}\n\nNote that =other_setting= was ignored because it was not in the default options.\n\nThis also works on arbitrarily nested =Options= objects.\n\n*** Removing keys\n\nA new Options can be created, without copying any keys in a given list:\n#+BEGIN_SRC python :session options-example :results output\noptions = Options(value = 42, greeting = 'hello', pi=3.14)\n\nsmaller = options.drop('greeting', 'value')\nprint(smaller)  # =\u003e {'pi': 3.14}\n#+END_SRC\n\n#+RESULTS:\n: {'pi': 3.14}\n\nor this could be done by filtering, or a dict comprehension:\n#+BEGIN_SRC  python :session options-example :results output\nanother = Options({key:value for (key,value) in options.items()\n                             if key != \"pi\"})\nprint(another)  # =\u003e {'value': 42, 'greeting': 'hello'}\n#+END_SRC\n\n#+RESULTS:\n: {'value': 42, 'greeting': 'hello'}\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbendudson%2Ffrozen-options","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbendudson%2Ffrozen-options","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbendudson%2Ffrozen-options/lists"}