{"id":18147253,"url":"https://github.com/joshdreamland/yamlet","last_synced_at":"2026-02-22T17:05:14.124Z","repository":{"id":257985025,"uuid":"864294068","full_name":"JoshDreamland/Yamlet","owner":"JoshDreamland","description":"A GCL-like templating engine for YAML","archived":false,"fork":false,"pushed_at":"2025-04-15T18:46:22.000Z","size":395,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-15T19:44:43.219Z","etag":null,"topics":["configuration","gcl","yaml","yamlet"],"latest_commit_sha":null,"homepage":"","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/JoshDreamland.png","metadata":{"files":{"readme":"README.md","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,"zenodo":null}},"created_at":"2024-09-27T21:51:02.000Z","updated_at":"2025-04-15T18:46:26.000Z","dependencies_parsed_at":"2024-12-17T19:40:38.394Z","dependency_job_id":"556607ef-cf7f-4524-8a7c-7110abd4b653","html_url":"https://github.com/JoshDreamland/Yamlet","commit_stats":null,"previous_names":["joshdreamland/yamlet"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoshDreamland%2FYamlet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoshDreamland%2FYamlet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoshDreamland%2FYamlet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JoshDreamland%2FYamlet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JoshDreamland","download_url":"https://codeload.github.com/JoshDreamland/Yamlet/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250522293,"owners_count":21444510,"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":["configuration","gcl","yaml","yamlet"],"created_at":"2024-11-01T22:06:06.140Z","updated_at":"2026-02-22T17:05:14.070Z","avatar_url":"https://github.com/JoshDreamland.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Yamlet: A GCL-like templating engine for YAML\n\nYamlet is a tool for writing complex configurations in YAML.\n\nIt is reminiscent of GCL, but it adheres to strict YAML syntax, while offering\ncomplex expression evaluation and templating operations that would otherwise\nrequire an engine such as Jinja.\n\nYAML itself doesn't support even simple operations such as string concatenation,\nso a common wishlist item for people is the ability to use a YAML anchor to\nextend one value inside another.\n\nFor example:\n```yaml\nkey1: \u0026anchor my common value\nkey2: *anchor extra specialized value  # THIS DOES NOT WORK IN YAML!\n```\n\nYamlet solves this explicitly, on top of bundling a comprehensive templating\nengine:\n\n```yaml\nkey1: my common value\nkey2: !expr key1 + ' my extra specialized value'\n```\n\nGCL, Google's Generic Configuration Language, solves this problem more\ngenerally by deferring variable lookups into each scope that includes\nthem. Yamlet is a Pythonic implementation of this idea, in the way that\nJSonnet is a... jsonish implementation. The key difference is that JSonnet\nis owned by Google while Yamlet is hacked together by a former Google\nemployee in a few hundred lines of Python. On the plus side, tuple\ncomposition seems to actually work in this engine, which is more than\nI can say for `gcl.py` on Pip.\n\n(*Note for the uninitiated to GCL: A \"Tuple\" is the equivalent of a dictionary.*)\n\nThis tool is lightweight at the moment and kind of fun to reason about,\nso drop me issues or feature requests and I'll try to attend to them.\n\nThe biggest change that would make this project nicer is if YAML supported\nraw strings (literal style) for specific constructors without the use of a\nspecific style token. In particular, it's annoying having to insert a pipe\nand newline before any expression that you'd like to be evaluated GCL-style\ninstead of YAML style. Similarly, it would be great if I could define an\n`!else:` constructor that starts a mapping block, or revise the spec to\ndisallow colons at the end of tags (where followed by whitespace). Until then,\nthe best workaround I can recommend is to habitually parenthesize every Yamlet\nexpression and put spaces after all your tokens like it's the 80s.\n\nTo help work around this, I've added `!fmt` and `!composite` tags on top of\nthe core `!expr` tag so that the YAML parser can handle string interpretation\nand nested tuple parsing. In the examples below, I could have used\n`coolbeans: !fmt 'Hello, {subject}! I say {cool} {beans}!'` instead of that\npipe nonsense if I didn't want to show off string concatenation explicitly.\nI could probably have also gotten away with parentheses.\n\nI've also added a stream preprocessor that replaces `!else:` with `!else :`\nfor when someone inevitably forgets or doesn't read this.\n\n\n## Installation\n\n```bash\npip install yamlet\n```\n\nYamlet is a single Python file; you may also just copy `yamlet.py` into your\nproject wherever you like.\n\n\n## Features\n\nHere’s a summary of Yamlet’s features:\n- [String formatting](#string-formatting)\n- [GCL-Like tuple composition](#tuple-composition)\n- [Conditionals, as seen in procedural languages](#conditionals)\n- [File Imports](#file-imports)\n  (to allow splitting up configs or splitting out templates)\n- [Comprehensions](#comprehensions)\n- [Lambda expressions](#lambda-expressions)\n- [Custom functions](#custom-functions) (defined in Python)\n- [Custom tags](#custom-tags) for building user-defined types.\n- [Local Variables and Templates](#local-variables-and-templates) to control\n  which values are exposed to the Python API.\n- [GCL Special Values](#gcl-special-values) `null` and `external`.\n- Explicit [value referencing](#scoping-quirks) in composited tuples using\n  `up`/`super`\n  - `up` refers to the scope that contains the current scope, as in nested\n     tuples.\n  - `super` refers to the scope from which the current scope was composed,\n     as in the template from which some of its values were inherited.\n\n\n## Examples\n\nHere’s a whirlwind tour of Yamlet. Consider a main file, `yaml-gcl.yaml`:\n\n```yaml\nt1: !import yaml-gcl2.yaml\nt2:\n  beans: beans\n  coolbeans: !expr |\n      'Hello, {subject}! ' + 'I say {cool} {beans}!'\n\nchildtuple: !expr t1.tuple t2\nchildtuple2: !expr t2 t1.tuple2\n```\n\nAnd a separate file, `yaml-gcl2.yaml`:\n\n```yaml\ntuple:\n  cool: cooool\n  beans: sauce\n  subject: world\ntuple2: !composite\n  - tuple\n  - {\n    cool: awesome\n  }\n```\n\nIn Python, you can read these files like this:\n\n```python\nimport yamlet\n\nloader = yamlet.Loader()\nt = loader.load_file('yaml-gcl.yaml')\nprint(t['childtuple']['coolbeans'])\nprint(t['childtuple2']['coolbeans'])\n```\n\nThis will print the following:\n\n```\nHello, world! I say cooool beans!\nHello, world! I say awesome sauce!\n```\n\nYou can try this out by running `example.py`.\n\nFlipping the definitions of `childtuple` and `childtuple2` to instead read\n`t2 t1.tuple` and `t1.tuple2 t2` would instead print `cooool sauce` and\n`awesome beans`, respectively—which would be upsetting, so don't do that.\n(It's by design; this is how GCL templating works.) Each tuple you chain onto\nthe list overwrites the values in the previous tuples, and then expressions\ninherited from those tuples will use the new values.\n\nPlacing tuples next to each other in Yamlet composites them. For example, you\ncan use `my_template { my_key: overridden value }` to *instantiate* the tuple\n`my_template` and override `my_key` within that tuple with a new value.\n\nI'll break this down better later in this document\n(see [Tuple Composition](#tuple-composition)).\n\n### Conditionals\n\nYamlet adds support for conditional statements, on top of `cond()` as used in\nthe Google GCL spec. In Yamlet, conditionals look like this:\n\n```yaml\n!if platform == 'Windows':\n  directory_separator: \\\\\n  executable_extension: exe\n  dylib_extension: dll\n!elif platform == 'Linux':\n  directory_separator: /\n  executable_extension: null\n  dylib_extension: so\n!else:\n  directory_separator: /\n  executable_extension: bin\n  dylib_extension: dylib\n```\n\nNote that YAML requires you to have a space between the `!else` tag and the\nfollowing colon. However, Yamlet's stream preprocessor handles this for you,\nat the cost of any data that would otherwise contain the string `\"!else:\"`...\nwhich should be a non-issue, right?\n\n### String Formatting\n\nYamlet offers several syntaxes for string composition.\n\n```yaml\nsubject: world\nstr1: !expr ('Hello, {subject}!')\nstr2: !expr ('Hello, ' + subject + '!')\nstr3: !fmt 'Hello, {subject}!'\n```\n\nAll of these will evaluate to `Hello, world!`.\n\nThe next section explains how to create a template that lets you modify the\nvalue of `subject` from within your program or other contexts in Yamlet.\n\n### Tuple Composition\n\nIn GCL, the basic unit of configuration is a tuple, and templating happens\nthrough extension. Yamlet inherits this behavior. Tuple composition can be\ntricky to understand, so I’ll push the system to its limits to paint a clearer\npicture.\n\nIn both languages, *extension* occurs by opening a mapping immediately following\na tuple expression (i.e., an expression naming or creating a tuple). For example:\n\n```yaml\nparent_tuple {\n  new_key: 'new value',\n  old_key: 'new overriding value',\n}\n```\n\nYamlet differs from GCL here by adopting Python’s `key: value` mapping syntax\nrather than GCL’s `key = value`. This is a crucial difference, as GCL maintains\na distinction between `old_key { ... extension ... }` and\n`old_key = { ... override ... }`, which in Yamlet would need to be accomplished\nby replacing the old dictionary with a new *expression,* not tuple.\n\nHowever, in most cases, you want nested tuples within a child to extend the\nidentically named tuples in the parent. This is easy to express in Yamlet, and\ncan be done in several ways.\n\nThe first of these is just as above, more GCL-style:\n\n```yaml\nchild_tuple: !expr |\n  parent_tuple {\n    new_key: 'new value',  # Python dicts and YAML flow mappings require commas.\n    old_key: 'new overriding value',  # String values must be quoted in Yamlet.\n  }\n```\n\nThis example uses a literal-style scalar (denoted by the pipe character, `|`)\nso that the YAML parser correctly reads the Yamlet expression snippet.\n\nQuoting the entire expression by any other means is equally valid, but probably\nmuch harder to read.\n\nAnother approach is to explicitly denote the tuple composition using the\n`!composite` tag, which was created for that purpose:\n\n```yaml\nchild_tuple: !composite\n  - parent_tuple  # The raw name of the parent tuple to composite\n  - new_key: new value  # This is a mapping block inside a sequence block!\n    old_key: new overriding value  # Note that normal YAML `k: v` is fine.\n```\n\nBoth examples above behave identically.\n\nDepending on how well your eyes are trained on YAML vs GCL, you may prefer one\nstyle to the other. A further option is to use a flow mapping for the extension\nfields. This makes it look closer to the GCL syntax while still allowing YAML\ntags and unquoted (plain-style) values. Plain style is not allowed in Yamlet\nmapping expressions; unquoted words are assumed to be identifiers.\n\nThis approach looks like this:\n\n```yaml\nchild_tuple: !composite\n  - parent_tuple  # The raw name of the parent tuple to composite\n  - {\n    new_key: new value,  # A comma is now required here!\n    old_key: new overriding value  # Plain style is still allowed.\n  }\n```\n\nOnce again, all these examples behave the same, and in fact, the YAML parse for\nthe latter two snippets is identical.\n\nAs mentioned, any nested tuples appearing both within the first element of the\ncomposite operation (`parent_tuple`) and the second element (the inline mapping)\nwill be extended in the same way.\n\nFor the morbidly curious, strictly replacing a tuple in Yamlet (i.e. overriding\na nested tuple rather than extending it) would look something like this:\n\n```yaml\nt1:\n  shared_key: Value that appears in both tuples\n  sub:\n    t1_only_key: Value that only appears in t1\n    t1_only_key2: Second value that only appears in t1\n  sub2:\n    shared_key2: Nested value in both\n\nt2: !composite\n  - t1\n  - t2_only_key: Value that only appears in t2\n    sub: !expr |\n        { t2_only_key2: 'Second value that only appears in t2' }\n    sub2:\n      t2_only_key3: Nested value only in t2\n```\n\nIn this case, the `sub` tuple is overridden, while `sub2` is extended.\nAny nested tuples can be extended or overridden depending on how you structure\nthe composite operation.\n\nNote that there are usually several ways of expressing a tuple composition in\nYamlet; you can typically use any of YAML's means of expressing the mapping\nnode of your choosing, or use a Yamlet `!expr` expression and dict literal to\ndenote the same thing. In this case, however, Yamlet's mapping syntax lacks a\nclean way to mark a nested tuple as an override rather than an extension.\n\nThere are, however, ways of accomplishing this. The following snippet would\ntechnically have the same effect:\n\n```yaml\nt2: !expr |\n  t1 {\n      t2_only_key: 'Value that only appears in t2',\n      sub: [{\n        t2_only_key2: 'Second value that only appears in t2'\n      }][0],  # Trick to replace `sub` entirely\n      sub2: {\n        t2_only_key3: 'Nested value only in t2'\n      }\n  }\n```\n\nIn this case, evaluation of the nested tuple is deferred within the child using\nan identity function, specifically `[x][0]`. This is not exactly recommended\nbehavior, though you could accomplish this more cleanly by adding an identity\nfunction through your `YamletOptions` and then passing the nested tuple to it.\n\n### File Imports\n\nImporting allows you to assign the structured content of another Yamlet file to\na variable:\n\n```yaml\nt1: !import my-configuration.yaml\n```\n\nThe example above reads `my-configuration.yaml` and stores its content in `t1`.\nImporting is actually deferred until data from the file is accessed, so you\nmay import as many files as you like, import files that don't exist, import\nyourself, or import files cyclically—errors will only occur if you try to\naccess undefined or cyclic values within those files.\n\n### Comprehensions\n\nYamlet expressions inherit list comprehension syntax from Python.\n\n```yaml\nmy_array: [1, 2, 'red', 'blue']\nfishes: !expr r', '.join('{x} fish' for x in my_array)\n```\n\nIn this example, the `fishes` array evaluates to\n`1 fish, 2 fish, red fish, blue fish`.\n\nA couple notes:\n1. A raw string is used for the comma character to stop YAML\n   from interpreting just the first literal as the scalar value.\n2. Though an f-string could be used in the generator expression,\n   it is not required as all Yamlet literals use {} for formatting.\n   Using an f-string would allow Python's formatting options in the {}.\n\n### Lambda Expressions\n\nLambda expressions in Yamlet are read in from YAML as normal strings, then\nexecuted as Yamlet expressions:\n\n```yaml\nadd_two_numbers: !lambda |\n                 x, y: x + y\nname_that_shape: !lambda |\n   x: cond(x \u003c 13, ['point', 'line', 'plane', 'triangle',\n           'quadrilateral', 'pentagon', 'hexagon', 'heptagon', 'octagon',\n           'nonagon', 'decagon', 'undecagon', 'dodecagon'][x], '{x}-gon')\nis_thirteen: !lambda |\n             x: 'YES!!!' if x is 13 else 'no'\nfive_plus_seven:      !expr add_two_numbers(5, 7)\nshape_with_4_sides:   !expr name_that_shape(4)\nshape_with_14_sides:  !expr name_that_shape(14)\nseven_is_thirteen:    !expr is_thirteen(7)\nthirteen_is_thirteen: !expr is_thirteen(13)\n```\n\n### Custom Functions\n\nIn addition to lambdas, you can also directly expose Python functions for use\nin Yamlet configurations. For example:\n\n```python\nloader = yamlet.Loader(YamletOptions(functions={\n    'quadratic': lambda a, b, c: (-b + (b * b - 4 * a * c)**.5) / (2 * a)\n}))\ndata = loader.load('''\n    a: 2\n    b: !expr a + c  # Evaluates to 9, eventually\n    c: 7\n    quad: !expr quadratic(a, b, c)\n    ''')\nprint(data['quad'])  # Prints -1\n```\n\nWith this approach, you can define custom functions for use in Yamlet\nexpressions, which can lead to even more expressive configuration files.\n\n\n### Custom Tags\n\nYou can add custom tag constructors to Yamlet the same way you would in Ruamel:\n\n```py\n    loader.add_constructor('!custom', CustomType)\n```\n\nIn this example, `CustomType` will be instantiated with a Ruamel Loader and\nNode, which you can handle as you would with vanilla Ruamel.\n\nOn top of this, however, Yamlet offers an additional \"style\" attribute for your\ncustom types:\n\n```py\nloader.add_constructor('!custom', CustomType,\n                       style=yamlet.ConstructStyle.SCALAR)\n```\n\nWith this setup, `CustomType` will be instantiated using the final scalar value\nobtained from Ruamel. Additionally, Yamlet provides the following composite tags\nby default (unless `tag_compositing=False` is specified):\n\n* `!custom:fmt`: Applies string formatting (as with Yamlet’s `!fmt` tag) and\n   constructs `CustomType` with the formatted string result.\n* `!custom:expr`: Evaluates the input as a Yamlet expression\n   (similar to Yamlet’s `!expr` tag) and constructs `CustomType` with the\n   resulting value, regardless of type.\n* `!custom:raw`: Constructs `CustomType` directly from the scalar value obtained\n   from Ruamel. This is the default behavior for `ConstructStyle.SCALAR`,\n   so in this case, `!custom` and `!custom:raw` behave identically.\n\nYou can also specify `ConstructStyle.FMT` or `ConstructStyle.EXPR` when\nregistering the constructor to set the default behavior of the base tag\n(`!custom`) to formatting or expression evaluation. All three composite tags\n(`:fmt`, `:expr`, and `:raw`) will still be available by default unless you\nset `tag_compositing=False`.\n\n\n### Local Variables and Templates\n\nSometimes you want to control which values are exposed by for-loops or even\nexplicit access in the fully-parsed configuration (i.e. the tuple returned by\n`load(yamlet_config)` or the dict obtained by calling `evaluate_fully()` on\nthat tuple).\n\nFor this, you can use local expressions:\n\n```yaml\n!local var_that_will_not_show_up: Hello, world!\n!local var_that_would_error: !expr undefined varnames with bad syntax\nvar_that_will_show_up: !expr var_that_will_not_show_up\n```\n\nThen in Python, the fully-evaluated tuple will contain no locals:\n```py\nt = yamlet.load(yamlet_config)\nself.assertEqual(t.evaluate_fully(),\n                 {'var_that_will_show_up': 'Hello, world!'})\n```\n\nThis will remain the case even if additional values (*not* marked `!local`) are\ncomposited into that tuple. For example:\n\n```yaml\ntup1:\n  !local my_local: irrelevant\n  my_nonlocal: !fmt 'Hello, {my_local}!'\ntup2: !composite\n  - tup1\n  - my_local: world\n```\n\nIn this case, the following assertion would succeed:\n```py\nself.assertEqual(parsed_config['tup2'].evaluate_fully(),\n                 {'my_nonlocal': 'Hello, world!'})\n```\n\nAdditionally, you may use the `!template` type for tuples (YAML mapping values)\nwhich should only be used for composing other tuples (and will also not be\nexported). Because Yamlet lazily-evaluates everything, there is no observable\ndistinction between templates and non-template tuples outside of this behavior.\n\nAs an example:\n\n```yaml\nlibrary_template: !template\n  !local STATIC_LIB_PREFIX: !expr ('s' if platform == 'windows' else '')\n  static_libs: !expr |\n      ['{LIB_PREFIX}{STATIC_LIB_PREFIX}{name}.{STATIC_LIB_EXT}' for name in lib_names]\n  dynamic_libs: !expr |\n      ['{LIB_PREFIX}{name}.{SHARED_LIB_EXT}' for name in lib_names]\n  !local lib_names: !external\n```\n\nWhen observed from the Python API, accessing the `library_template` will return\nthe template tuple, even though it would not appear in the dict returned by\n`evaluate_fully`. However, accessing the `lib_names` field on that template will\nraise a `KeyError`. This behavior may be modified in a later version of Yamlet.\n\n\n### GCL Special Values\n\nIn addition to `up` and `super`, GCL defines the special values `null` and\n`external`. Yamlet has rudimentary support for these.\n - `external` evaluates to `external` when used in any operation. Requesting\n   an external value explicitly results in an error. The observable behaviors\n   of this value are the default behaviors for using any undeclared value in\n   Yamlet, so unless I've missed something, `external` is not worth using.\n - `null` removes a key from a tuple, omitting it in compositing operations\n   unless it is added again later by an overriding tuple composition.\n   - Because of this behavior, having `null` assigned to a tuple key is not\n     the same as not having that key in the tuple. A `null` value still counts\n     toward the `len()` of a tuple, for example, until after composition.\n\nAs a simple example of `null`, consider the following:\n\n```yaml\nt1:\n  key_to_keep: present\n  key_to_delete: also present\ndeleter:\n  key_to_delete: !null\nt2: !expr t1 deleter\nt3: !expr t1 t2\n```\n\nIn the above example,\n - `t1` has both `key_to_keep` and `key_to_delete`\n - `t2` has *only* `key_to_keep`\n - `t3` has *both* `key_to_keep` and `key_to_delete` once again. This is because\n   the key was *missing* from `t2`, not `null` within it.\n - `len(t1)` is 2.\n - `len(t2)` is 1.\n - `len(deleter)` is also 1.\n - `len(t3)` is again 2.\n\n### Error Reporting\n\nYamlet is pretty good about telling you where a problem happened by converting\nthe Python stack trace into Yamlet traces, showing the lines involved in\nevaluating an expression. You can also directly query Yamlet's\nprovenance information to discover where a value came from.\n\n## Caveats\n\n### Yamlet is an Extension of YAML\n\nYamlet is built on top of YAML, meaning that the first tool to parse your\nconfiguration file ***is** a YAML interpreter,* namely Ruamel. The facilities\noffered by Yamlet will only work for you if you can convey your expression\nstrings through Ruamel.\n\nTo help with this, Yamlet offers separate tags for `!fmt`\n(formatting string values) and `!expr` (general expression evaluation).\n\nThese are there because if you try to write this:\n```yaml\nmy_string: !expr 'my formatted string with {inlined} {expressions}'\n```\n...you are NOT going to get a string! Ruamel will interpret the string value,\nremoving the single quotes and handing Yamlet a bunch of drivel.\n\nThe `!fmt` tag works around this by treating the entire Ruamel value as a\nstring literal. Alternatively, you can use a literal style block:\n\n```yaml\nmy_string: !expr |\n  'my formatted string with {inlined} {expressions}'\n```\n\nAt the time of writing, this is the only way to trigger literal style for a\nYAML value. I can’t achieve this behavior directly through a tag implementation.\n\n### Map Literals in Yamlet Expressions\n\nYamlet mappings (tuples, dictionaries) resemble YAML mappings but have slight\ndifferences:\n\n```yaml\nmy_yamlet_map: !expr |\n  {\n    key: 'my string value with {inlined} {expressions}',\n    otherkey: 'my other value'\n  }\n```\n\nThis is because the Yamlet uses the Python parser to handle expressions,\nincluding flow mappings, which are based on Python dicts. I have taken the\nliberty of allowing raw names as keys (per YAML) without unpacking them\nas expressions (per Python). To use a variable as the key, you would have to\nsay `'{key_variable}': value_variable`, but note that the `key_variable`\nmust be available in the compositing scope (the scope containing the mapping\nexpression) and CANNOT be deferred to access values from the resulting tuple.\n\nThe values within a Yamlet mapping, however, *are* deferred, with the exception\nof nested mappings, which are treated as part of the current mapping expression.\n\nFor your curiosity, a dynamic key would look like this:\n\n```yaml\nstatic_key: dynamic\ntup: !expr |\n    { '{static_key}_key': 'value' }\n```\n\nThe above example would define `dynamic_key` within the `tup` tuple.\nNote that other values defined in that mapping would be inaccessible\nfor use in keys; attempting to access them would result in an error\nor a different value being pulled in than the one you might expect.\n\nThis setup means that Yamlet mappings are neither pure Python dict literals\nnor pure YAML mappings. They are not like Pthon literals because identifiers\n(`NAME` tokens) used as keys are not treated as variables. They are also not\nYAML flow mapping literals, because every value is a raw Yamlet expression in\nwhich operators can be used and strings must be quoted.\n\nOne additional difference from YAML flow mappings is that all keys must have\nvalues; you may not simply mix dict pairs and set keys (YAML allows this;\nPython and Yamlet do not).\n\n### Scoping Quirks\n\nTuples (GCL or Yamlet dicts) inherit their scope from where they are defined.\n\n```yaml\ntuple_A:\n  fruit: Apple\n  tuple_B:\n    fruit: Banana\n    value: !fmt '{up.fruit} {fruit}'\ntuple_C: !expr |\n  tuple_A {\n    tuple_B: {\n      fruit: 'Blueberry',\n      value2: '{super.up.fruit} {super.fruit} {fruit} {up.fruit}',\n      value3: '{super.value}  -vs-  {value}',\n    },\n    fruit: 'Cherry'\n  }\n```\n\nThis example contains four tuples; let's start by examining the first two,\n`tuple_A` and `tuple_B`. The latter is *nested* within the former, but does\nnot inherit from it. That is, there’s no composition at this point, so\n`super` is undefined. But due to the nesting, `tuple_B.up` refers to `tuple_A`,\nso `tuple_B.value` evaluates to `Apple Banana`.\n\nIt gets complicated as we do composition.\n\nHere, `tuple_C` is defined as a specialization of `tuple_A`, where `fruit`\nis overridden, and `tuple_B` is extended in much the same way as `tuple_A`.\nBecause composition is involved, the `super` of `tuple_C` becomes `tuple_A`,\nand the `super` of its nested tuple, `tuple_C.tuple_B`, in turn becomes\n`tuple_A.tuple_B`. Expressions from each `super` tuple will be inherited by\ntheir respective children, but will be re-evaluated in the new scope.\nThat means expressions that reference variables (or even keywords such as `up`\nand `super` themselves) will be re-evaluated based on the new values\nwithin `tuple_C`.\n\nBecause `fruit` is overridden in the inheriting scope (`tuple_C.tuple_B`), and\nalso in the enclosing scope thereof, the expression in `tuple_A.tuple_B.value`\ntakes on an entirely new meaning in the context of `tuple_C.tuple_B`'s scope.\nThe resulting value in that context is `Cherry Blueberry`.\n\nThus, `tuple_C.tuple_B.value3` will evaluate to\n`Apple Banana  -vs-  Cherry Blueberry`.\n\nAll of these values are accessible from the innermost inheriting scope,\n`tuple_C.tuple_B`. In that scope, `value2` will evaluate to\n`Apple Banana Blueberry Cherry`, representing the values from the `super` tuple\npair first, followed by the values from the inheriting tuple pair.\n\nIt’s worth noting that `super.up.fruit` is equivalent to `up.super.fruit`.\n\n### Referencing vs Instantiating\n\nIn Yamlet, an expression can simply refer to another tuple *without* attempting\nto instantiate (extend or modify) it.\n\nIn this case, the tuple is returned from the expression by reference.\nIt will NOT be re-evaluated under its new enclosing scope.\n\nIn other words, the Yamlet expressions `t1` and `t1 {}` are semantically\ncompletely different; the former references `t1`, and the latter extends it\nwithout modification, creating a simple copy in the new scope.\n\nThis applies for composition done by listing tuples sequentially (e.g. `t1 t2`)\nas well; this expression creates a new tuple in the current scope.\n\n## Differences from GCL\n\n### Missing Features\n\nThere are a few features present in GCL that Yamlet currently doesn’t implement:\n- Assertions are not yet supported.\n- Additional builtin functions (`substr`, `tail`, etc.). The Python built-ins\n  and available list comprehensions pretty much suffice for this.\n- GCL-style (C++/C-style) comments cannot be used anywhere in Yamlet.\n  Yamlet uses Python/YAML-style comments, as handled by Ruamel.\n- The `args` tuple is not supported. This would ideally be the responsibility\n  of a command-line utility that preprocesses Yamlet into some other format\n  (such as Protobuf).\n- Support for `final` expressions is missing.\n  The language might be better without these... though adding them could create\n  a way to reliably pre-evaluate entire Yamlet files into other formats.\n\n### Improvements Over GCL\n\nYamlet tracks all origin information, so there's no need for a separate utility\nto trace where expressions came from. Consequently, you may chain `super`\nexpressions and it will \"just work.\" You can also invoke `explain_value` in any\nresulting dictionary to obtain a description of how a value was computed.\n\nFrom the included example, `print(t['childtuple'].explain_value('coolbeans'))`\nwill produce the following dump:\n\n```\n`coolbeans` was computed from evaluating expression `'Hello, {subject}! ' + 'I say {cool} {beans}!'` in \"yaml-gcl.yaml\", line 4, column 14\n     - With lookup of `subject` in this scope in \"/home/josh/Projects/Yamlet/yaml-gcl2.yaml\", line 2, column 3\n     - With lookup of `cool` in this scope in \"/home/josh/Projects/Yamlet/yaml-gcl2.yaml\", line 2, column 3\n     - With lookup of `beans` in this scope in \"/home/josh/Projects/Yamlet/yaml-gcl2.yaml\", line 2, column 3\n```\n\nBe advised that a complex Yamlet program can generate tens of thousands of lines\nof traceback for a single value... so don't get carried away. I suggest leaning\non user-defined functions rather than miles of inter-tuple dependencies.\n\n## Differences from the Rest of the Industry\n\nYamlet is probably the only templating or expression evaluation engine that\ndoesn't use jq. If you want to use jq, you can create a function that accepts\na jq string and use Yamlet's literal formatting to insert values into the string.\n\nYamlet shares a more procedural syntax with GCL, and supports basic arithmetic\noperations and several built-in functions, with more likely to be added in the\nfuture.\n\nA Yamlet configuration file (or \"program,\" as GCL calls its own scripts) doesn't\nreally need jq, because you can just invoke custom routines in it using a more\ntraditional, functional syntax.\n\nAdditionally, Jinja, a popular templating engine, does not pair well with YAML\nbecause Jinja is designed for manipulating lines of text, while YAML relies on\nindentation as part of its syntax. Import statements in Yamlet work\ndifferently: they import the final tuple value, not raw lines of\nunprocessed text.\n\n## What's in a Name?\n\nWho knows! The name \"Yamlet\" might play on \"JSonnet,\" drawing on a sort of\nShakespearean motif around the name \"Hamlet.\" It might also be a Portmanteau of\n\"YAML\" and \"template,\" or, more obscurely, some amalgam of \"YAML\" and \"Borglet.\"\nPerhaps it plays more directly on \"applet\" and how one might write one in YAML.\nOr maybe it's simply the product of whatever sort of fever dream leads to the\ninception of a tool such as this. Regardless, rest assured that a rose by any\nother name would still smell as much like durian.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshdreamland%2Fyamlet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshdreamland%2Fyamlet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshdreamland%2Fyamlet/lists"}