# Adding Custom Tags
If you need tags that are not included and have external dependencies, you add them via Python plugins (see [entry-points](https://packaging.python.org/en/latest/specifications/entry-points/#entry-points) and {py:func}`~importlib.metadata.entry_points`).
---
## Configuring you plugin library
This library using the `granular_configuration_language_20_tag` group.
### Using `pyproject.toml`
Example `pyproject.toml` entry:
```toml
[project.entry-points."granular_configuration_language_20_tag"]
"official_extra" = "granular_configuration_language.yaml._tags.func_and_class"
```
- `official_extra` is the name of the plugin.
- [`G_CONFIG_DISABLE_PLUGINS`](configuration.md#environment-variables) uses this name.
- No whitespace in names.
- `granular_configuration_language.yaml._tags.func_and_class` is the module searched for Tags.
- Please keep this import lightweight.
---
## Writing your own tag
[Real Examples](https://github.com/lifedox/granular-configuration-language/tree/main/granular_configuration_language/yaml/_tags)
To make adding tags easy, the logic has been wrapped into a series of ordered decorators, leaving you three choices to make.
Everything you need is importable from {py:mod}`granular_configuration_language.yaml.decorators`.
### Example
```python
from credstash import getSecret
from granular_configuration_language.yaml.classes import Masked
from granular_configuration_language.yaml.decorators import (
Tag, as_lazy, interpolate_value_without_ref, string_tag
)
@string_tag(Tag("!Credstash")) # Tag Type Decorator
@as_lazy # Laziness Decorator
@interpolate_value_without_ref # (Optional) Interpolate Decorator
# @with_tag # (Optional) With Attribute Decorator
def handler(value: str) -> Masked: # Function Signature
return Masked(getSecret(value)) # Tag Logic
```
### Function Signature
- **Name**: Completely up to you. Only used for documentation and exception messages.
- **Parameters**: At most three positional are possible (determined by your chosen [Laziness Decorator](#laziness-decorator)).
1. `value` - This is the value loaded from YAML, after its passes through the {py:class}`.TagDecoratorBase`.
- Its type is determined by the [Tag Type Decorator](#tag-type-decorator) you use.
- `value` will always be a single variable no matter its type or union of types. It will never be processed with `*` or `**`.
2. {py:class}`.Root` - This is a reference to the root of final merged configuration.
- It is used by [`!Ref`](yaml.md#ref) and [`!Sub`](yaml.md#sub) to support JSON Path and JSON Pointer querying the final configuration.
3. {py:class}`.LoadOptions` - This a frozen {py:func}`dataclass ` instance that contains the options used while loading the configuration file.
- It is used [`!ParseFile`](yaml.md#parsefile--optionalparsefile) and [`!ParseEnv`](yaml.md#parseenv--parseenvsafe), so the delayed parsing uses the same options.
- **Return Type**: Fully controlled by you. Only used for documentation and exception messages.
### Tag Type Decorator
- Defines the Python type of the tag (`value`'s type), while naming the tag, using {py:class}`.Tag`.
- Explicitly requiring use of {py:class}`.Tag` is purely for easy `grep`-ing.
- Tags must start with `!`.
- There are four built-in options, but you can define you [own](#creating-your-own-tag-type-decorator).
- {py:class}`.string_tag` - `str`
- {py:class}`.string_or_twople_tag` - `str | tuple[str, typing.Any]`
- {py:class}`.sequence_of_any_tag` - `collections.abc.Sequence[typing.Any]`
- {py:class}`.mapping_of_any_tag` - `Configuration[typing.Any, typing.Any]`
### Laziness Decorator
- Defines the laziness of tags and the required positional parameters.
- `value`'s type is determined by [Tag Type Decorator](#tag-type-decorator).
- There are five options (these are all the possible options).
- These make the tag lazy, so the Tag Logic runs at Fetch.
- {py:func}`.as_lazy`
- _Positional Parameters_ - `(value: ... )`
- {py:func}`.as_lazy_with_load_options`
- _Positional Parameters_ - `(value: ... , options: LoadOptions)`
- {py:func}`.as_lazy_with_root`
- _Positional Parameters_ - `(value: ... , root: Root)`
- Note: {py:func}`.as_lazy_with_root` has a decorator factory version.
- Keyword Parameters:
- `needs_root_condition`: `Callable[[ ... ], bool]`
- Used by [`!Sub`](yaml.md#sub) to check if Root is required before holding on to a reference to it.
- {py:func}`.as_lazy_with_root_and_load_options`
- _Positional Parameters_ - `(value: ... , root: Root, options: LoadOptions)`
- This make the Tag Logic run at Load Time
- {py:func}`.as_not_lazy`
- _Positional Parameters_ - `(value: ... )`
### Interpolate Decorator
- Interpolate Decorator is **optional**. `value` must be a {py:class}`str`.
- These decorators run the interpolation syntax prior to running the Tag Logic (i.e. `value` is output of the interpolation).
- Options:
- {py:func}`.interpolate_value_without_ref` - Does not include JSON Path or JSON Pointer syntax.
- {py:func}`.interpolate_value_with_ref` - Includes full interpolation syntax.
- Requires {py:class}`.Root` as the second parameter, even if you don't use it in the Tag Logic.
### With Attribute Decorator
- Currently, only {py:func}`.with_tag` is available, if you need your tag.
- With the {py:func}`.with_tag` decorator, a {py:class}`.Tag` parameter is prepended before the `value` parameter.
- The {py:func}`.with_tag` decorator removes the {py:class}`.Tag` parameter from the function signature, so that it invisibly works with the functionality decorators.
**Example:**
```python
from credstash import getSecret
from granular_configuration_language.yaml.classes import Masked
from granular_configuration_language.yaml.decorators import (
Tag, as_lazy, interpolate_value_without_ref, string_tag, with_tag
)
@string_tag(Tag("!Credstash")) # Tag Type Decorator
@as_lazy # Laziness Decorator
@interpolate_value_without_ref # (Optional) Interpolate Decorator
@with_tag
def handler(tag: Tag, value: str) -> Masked: # Function Signature
return Masked(getSecret(value)) # Tag Logic
```
---
## Creating your own Tag Type Decorator
[Tag Type Decorators](#tag-type-decorator) are created by implementing {py:class}`.TagDecoratorBase`.
[Real Examples](https://github.com/lifedox/granular-configuration-language/tree/main/granular_configuration_language/yaml/decorators/_type_checking.py)
### Defining the Type of your Tag Type Decorator
```python
import typing
from granular_configuration_language.yaml.decorators import (
TagDecoratorBase,
)
class float_tag(TagDecoratorBase[float]):
Type: typing.TypeAlias = float
@property
def user_friendly_type(self) -> str:
return "float"
```
The required portion of {py:class}`.TagDecoratorBase` is to set the Python type a Tag takes as input. You set it in the {py:class}`~typing.Generic` argument, and you implement the {py:meth}`~.TagDecoratorBase.user_friendly_type` property. The property is expected to match Generic argument, but to be as straightforward as possible. For example, {py:class}`.sequence_of_any_tag` uses `collections.abc.Sequence[typing.Any]` as Generic argument, but property just returns `list[Any]`.
The {py:data}`~typing.TypeAlias`, `Type`, is just a nicety for users of your Tag Type Decorator. Case in point, `str | tuple[str, typing.Any]]` is more effort to type than `string_or_twople_tag.Type`.
:::{admonition} Implementation Excuse
:class: note
:collapsible: closed
While it is possible to implement {py:meth}`~.TagDecoratorBase.user_friendly_type` in the {py:class}`.TagDecoratorBase`. The properties required are not defined in PyRight, and it needs a lot of checks and special case handling, especially since I don't want `collections.abc.` or `typing.`
The guaranteed to never break and handling all possible cases solution is the author of the Tag Type Decorator just writing what they think is best.
:::
### Configuring your Tag Type Decorator
```python
class float_tag(TagDecoratorBase[float]):
# ...
@typing.override # Python 3.12+
def scalar_node_type_check(
self,
value: str,
) -> typing.TypeGuard[float]:
"""""" # Make undocumented
# As of Version 2.3.0, a `ValueError` raised
# during `type_check` or `transformers` will
# be converted into a properly messaged
# `TagHadUnsupportArgument`, so this method
# could just be `return True`.
try:
float(value)
return True
except ValueError:
return False
@typing.override # Python 3.12+
def scalar_node_transformer(
self,
value: typing.Any,
) -> float:
"""""" # Make undocumented
return float(value)
```
YAML Tags support Scalar, Sequence, and Mapping YAML types. These are {py:class}`str`, {py:class}`~collections.abc.Sequence`, and {py:class}`~collections.abc.Mapping` in Python.
For each YAML type, there is an associated `type_check` method. These methods are called after the YAML type is checked. Use these to narrow the type of your Tag Type Decorator.
- {py:meth}`~.TagDecoratorBase.scalar_node_type_check`
- Called if the type of `value` is {py:class}`str`.
- Default return is {py:data}`False`.
- Return type is {py:data}`TypeGuard[T] `, which is a {py:class}`bool` value.
- {py:meth}`~.TagDecoratorBase.sequence_node_type_check`
- Called if the type of `value` is `collections.abc.Sequence[typing.Any]`.
- Default return is {py:data}`False`.
- Return type is {py:data}`TypeGuard[T] `, which is a {py:class}`bool` value.
- {py:meth}`~.TagDecoratorBase.mapping_node_type_check`
- Called if the type of `value` is `collections.abc.Mapping[typing.Any, typing.Any]`.
- Default return is {py:data}`False`.
- Return type is {py:data}`TypeGuard[T] `, which is a {py:class}`bool` value.
If you need to mutate `value` from a YAML type to a different Python type, there are `transformer` methods. These methods are called after the `type_check` method.
- {py:meth}`~.TagDecoratorBase.scalar_node_transformer`
- Called if the type of `value` is {py:class}`str`.
- Default return is `value` (identity operation).
- Return type is {py:class}`~.T`
- {py:meth}`~.TagDecoratorBase.sequence_node_transformer`
- Called if the type of `value` is `collections.abc.Sequence[typing.Any]`.
- Default return is `value` (identity operation).
- Return type is {py:class}`~.T`
- {py:meth}`~.TagDecoratorBase.mapping_node_transformer`
- Called if the type of `value` is `collections.abc.Mapping[typing.Any, typing.Any]`.
- Default return is `value` (identity operation).
- Return type is {py:class}`~.T`
:::{tip}
If you generate documentation, it is recommended to override the doc-string with an empty string when you override a `type_check` or `transformer`, as these methods are implementation detail, not public interface.
:::
---
## Plugin Compatibility Versioning Note
:::{admonition} Notice of Future Intent
:class: note
`20` in `granular_configuration_language_20_tag` represents 2.0 tag plugin compatibility, and not directly connected to library version. Additional groups will be added only if there is feature change to plugin support.
- A minor plugin change (e.g. `21`) would represent an added feature that requires a structural change but not a change to the primary code.
- A minor compatibility version bump deprecates any previous compatibility version (e.g. `20`).
- Both `20` and `21` would be supported, with `20` deprecated, using a compatibility layer.
- A major plugin change (e.g. `30`) would represent a breaking change to plugin, potentially requiring a complete code change.
- A major compatibility version bump deprecates all previous compatibility versions (e.g. `20` and `21`).
- If the library does not major version, then `20`, `21`, `30` would be all be supported, with `20` and `21` deprecated, using compatibility layers.
- A major version bump to this library may or may not introduce a new plugin compatible.
- It would remove any deprecated versions.
- If there is no change to plugin compatibility, then only a non-zero minor plugin version would introduce to new major plugin version.
- If `21` and `22` were introduced within Version 2 of this library, then Version 3 removes `20` and `21`, keeps `22`, and adds `30` as a duplicate of `22`. Version 4 would remove `22`, keep `30`, and add `40` as duplicate of `30`
- If only `20` exists, then `30` is introduced as a duplicate of `20`, but `20` is not deprecated until a minor or major change.
- Adding `x0` for every non-breaking major version `x` is to reduce developer overhead. "Just match the major version of the minimum of your supported dependency range."
- If there is a minor plugin change, then that version becomes the next major compatibility version.
- If there is a major plugin change, all previously supported compatibility versions are removed.
:::