Package: kdl-py
KDL-pyA handwritten Python 3.7+ implemenation of a parser
for theKDL Document Language,
fully compliant with KDL 1.0.0.KDLis, as the name suggests, a document language,
filling approximately the same niche as JSON/YAML/XML/etc
to be a simple but powerful language for config files.
It combines the best of several of these languages,
while avoiding their pitfalls:
more general than JSON and more powerful than XML,
while avoiding the verbosity of XML
or the explosive complexity of YAML.kdl-py, in particular, is focused onease-of-use,
supporting things like date/times, ip addresses, urls, uuids, regexes, and binary data
directly in your KDL document
(via powerful but simple tagged values),
and parsing them into native Python types automatically,
or doing the reverse and letting you build KDL document trees
with these values directly,
and automatically and safely serializing them into KDL text
for later parsing!You can customize parsing and serialization further for your application very easily,
by providing node or value "converters"
to turn plain KDL values or nodes
into application-specific types,
and then turning them back into KDL text automatically
merely by adding a.to_kdl()method to your classes.Installingpipxinstallkdl-pyWhen installed, akdlreformatcommand-line program is also made available,
which can canonicalize a KDL document. Seebelowfor options.UsingThekdl.parse(str, parseConfig|None)function parses, you guessed it, a string of KDL into a KDL document object:importkdl>>>importkdl>>>doc=kdl.parse('''... node_name "arg" {...     child_node foo=1 bar=true... }... ''')>>>>>>docDocument(nodes=[Node(name='node',args=['arg'],nodes=[Node(name='child',props=OrderedDict([('foo',1.0),('bar',True)]))])])You can also create akdl.Parser()object and call its.parse()method;Parserobjects can set up parsing and printing options that'll apply by default. See below for how to configure parsing options.Either way, you'll get back akdl.Documentobject, which is fully mutable. By default, untagged KDL values are represented with native Python objects.>>>doc.nodes[0].nodes[0].props["foo"]=2>>>>>>print(doc)node_name"arg"{child_nodefoo=2bar=true}Stringifying akdl.Documentobject will produce a valid KDL document back. You can also calldoc.print(printConfig|None)to customize the printing with aPrintConfigobject, described below.  See below for how to configure printing options.Inserting Native Valueskdl-py allows a number of native Python objects to be used directly in KDL documents by default,
and allows you to customize your own objects for use.kdl-py automatically recognizes and correctly serializes the following objects:bool: as untaggedtrueorfalseNone: as untaggednullint,float: as untagged decimal numberstr: as untagged stringbytes: as(base64)-tagged stringdecimal.Decimal: as(decimal)-tagged stringdatetime,date, andtime: as(date-time),(date), or(time)-tagged stringsipaddress.IPv4Addressandipaddress.IPv6Address: as(ipv4)or(ipv6)-tagged stringsurllib.parse.ParseResult(the result of callingurllib.parse.urlparse()): as(url)-tagged stringuuid.UUID: as(uuid)-tagged stringre.Pattern(the result of callingre.compile()): as(regex)-tagged raw stringAll of the tags used above are reserved and predefined by theKDL specification.In addition, any value with a.to_kdl()method
can be used in a kdl-py document.
The method will be called when the document is stringified,
and must return one of the kdl-py types,
or any of the native types defined above.(ForparsingKDL into these native types,
or your own types,
see theParseConfigsection, below.)Customizing ParsingParsing can be controlled via akdl.ParseConfigobject,
which can be provided in three ways.
In order of importance:Passing aParseConfigobject tokdl.parse(str, ParseConfig|None)orparser.parse(str, ParseConfig|None)(if you've constructed akdl.Parser).Creating akdl.Parser(parseConfig|None, printConfig|None),
which automatically applies it to its.parse()method if not overriden.Fiddling with thekdl.parsing.defaultsobject,
which is used if nothing else provides a config.AParseConfigobject has the following properties:nativeUntaggedValues: bool = TrueControls whether the parser produces native Python objects (str,int,float,bool,None) when parsing untagged values (those without a(foo)prefix), or always produces kdl-py objects (such askdl.String,kdl.Decimal, etc).nativeTaggedValues: bool = TrueControls whether the parser produces native Python objects
when parsingtaggedvalues,
for some ofKDL's predefined tags:i8,i16,i32,i64,u8,u16,u32,u64on numbers:
Checks that the value is in the specified range,
then converts it to anint.
(It will serialize back out as an ordinary untagged number.)f32,f64on numbers:
Converts it to afloat.
(It will serialize back out as an ordinary untagged number.)decimal64,decimal128on numbers, anddecimalon strings:
Converts it to adecimal.Decimalobject.
(Always reserializes to a(decimal)-tagged string.)date-time,date,timeon strings:
Converts to adatetime,time, ordateobject.ipv4,ipv6on strings:
Converts it to anipaddress.IPv4Addressoripaddress.IPv6Addressobject.urlon strings:
Converts it to aurllib.parse.ParseResulttuple.uuidon strings:
Converts it to auuid.UUIDobject.regexon strings:
Converts it to are.Patternobject.
(It will serialize back out as a raw string.)base64on strings:
Converts it to abytesobject.valueConverters: Dict[ValueKey, Callable] = {}A dictionary of ValueKey->converter functions,
letting you parse values
(like(date)"2021-01-01")
into whatever types you'd like.Whenever the parser encounters a value,
it will attempt to find an entry in this dict
whoseValueKeymatches the value.
If it succeeds,
it will call the associated converter function with two arguments:
the fully-constructed kdl-py object,
and aParseFragmentobject giving you access
to the precise characters parsed from the document
(for the value; the tag, if any, won't be included, as you can get it from the object itself).
Whatever you return will be inserted into the document
instead of the originally-parsed value.You can produce KDL values
(such as parsing(hex)"0x12.e5"into akdl.Decimal,
since KDL doesn't support fractional hex values),
or produce any other Python type.
If you return a non-KDL type,
you probably want to ensure it has a.to_kdl()method
(or is one of the supported built-in types),
so it can be serialized back into a KDL document.Note that onlyoneconversion can happen to a given value.
Your converters are checked
in the dictionary's iteration order,
then if none of them succeeded
it will attempt to run thenativeUntaggedValuesornativeTaggedValuesbehaviors,
if they're turned on.
If a converter returnsNotImplemented,
it will continue looking for a matching converter.nodeConverters: Dict[NodeKey, Callable] = {}Similar tovalueConverters,
except it usesNodeKeys,
and it convertskdl.Nodes instead.There is no native conversion of nodes;
if none of your converters successfully run,
the node will be inserted as-is.ParseFragmentkdl.ParseFragmentis passed to your custom converters,
specified inkdl.ParseConfig.tags,
giving you direct access to the input characters
before any additional processing was done on them.
This is useful, for example,
to handle numeric types
that might have lost precision in the normal parse.It exposes a.fragmentproperty,
containing the raw text of the value
(after the tag, if any).It also exposes a.error(str)method,
which takes a custom error message
and returns akdl.ParseErrorwith theParseFragment's location already built in,
ready for you toraise.
This should be used if your conversion fails for any reason,
so your errors look the same as native parse errors.Customizing PrintingLike parsing, printing a kdl-pyDocumentback to a KDL string can be controlled by akdl.PrintConfigobject,
which can be provided in three ways.
In order of importance:Passing aPrintConfigobject todoc.print(PrintConfig|None).Settingdoc.printConfigto aPrintConfig.
(This is done automatically for any documents produced by aParser,
if you pass theprintConfigoption to the constructor.)Fiddling with thekdl.printing.defaultsobject,
which is used if nothing else provides a config.APrintConfigobject has the following properties:indent: str = "\t"The string used for each indent level.
Defaults to tabs,
but can be set to a sequence of spaces if desired
(or anything else).semicolons: bool = FalseWhether or not nodes are ended with semicolons.
(The printer always ends nodes with a newline anyway,
so this is purely a stylistic choice.)printNullArgs: bool = TrueWhenFalse, automatically skips over any "null"/Nonearguments.
This will corrupt documents that use the "null" keyword intentionally,
but can be useful if you'd prefer to use aNonevalue
as a signal that the argument has been removed.printNullProps: bool = TrueIdentical toprintNullArgs,
but applies to properties rather than arguments.respectStringType: bool = TrueWhenTrue, the printer will output strings as the same type they were in the input,
either raw (r#"foo"#) or normal ("foo").
WhenFalse, the printer always outputs normal strings.Note that this only has an effect onkdl.Stringandkdl.RawStringobjects;
if the document contains Pythonstrobjects,
they will always output as normal strings.respectRadix: bool = TrueSimilar torespectStringType,
whenTruethe printer will output numbers as the radix they were in the input,
like0x1bfor hex numbers.
WhenFalse, the printer always outputs decimal numbers.Again, this only has an effect on kdl-py objects;
native Python numbers are printed as normal for Python.exponent: str = "e"What character to use for the exponent part of decimal numbers,
when printed with scientific notation.
Should only be set to "e" or "E".Like the previous options, this only has an effect on kdl-py objects;
native Python numbers are printed as normal for Python.Full API Referencekdl.parse(str, config: kdl.ParseConfig|None) -> kdl.Documentkdl.Parser(parseConfig: kdl.ParseConfig|None, printConfig: kdl.PrintConfig|None)parser.parse(str, config: kdl.ParseConfig|None) -> kdl.Documentparser.print(config: kdl.PrintConfig|None) -> strkdl.Document(nodes: list[kdl.Node]?, printConfig: kdl.PrintConfig|None)doc.print(PrintConfig|None) -> strdoc[NodeKey] -> Nodereturns the first child node matching theNodeKey. Raises aKeyErrorif nothing matches theNodeKey, similar to adict.doc.get(NodeKey, default: T = None) -> kdl.Node | Treturns the first child node matching theNodeKey. Returns the default value if nothing matches.doc.getAll(NodeKey) -> Iterable[kdl.Node]returns all child nodes matching theNodeKeykdl.Node(name: str, tag: str|None, args: list[Any]?, props: dict[str, Any]?, nodes: list[kdl.Node]?)node[NodeKey] -> Nodereturns the first child node matching theNodeKey. Raises aKeyErrorif nothing matches theNodeKey, similar to adict.node.get(NodeKey, default: T = None) -> kdl.Node | Treturns the first child node matching theNodeKey. Returns the default value if nothing matches.node.getAll(NodeKey) -> Iterable[kdl.Node]returns all child nodes matching theNodeKeynode.getProps(ValueKey) -> Iterable[tuple[str, Any]]returns an iterator of(name, value)pairs for the properties whose value matches theValueKey(Note: for this purpose, non-KDL values, such asint, have no tag, and can only meaningfully be tested via aTypeKey.)node.getArgs(ValueKey) -> Iterable[Any]returns an iterator of the arguments that match theValueKey(Same disclaimer as.getProps().)node.matchesKey(NodeKey) -> boolreturns whether the node matches theNodeKeykdl.Value‡val.matchesKey(ValueKey) -> boolreturns whether the value matches theValueKeykdl.Binary(value: int, tag: str|None)kdl.Octal(value: int, tag: str|None)kdl.Decimal(mantissa: int|float, exponent: int|None, tag: str|None)dec.value: readonly,mantissa * (10**exponent)kdl.Hex(value: int, tag: str|None)kdl.Bool(value: bool, tag: str|None)kdl.Null(tag: str|None)null.value: readonly, alwaysNonekdl.RawString(value: str, tag: str|None)kdl.String(value: str, tag: str|None)kdl.ExactValue(chars: str, tag: str|None)†kdl.Numberish,kdl.Stringish‡kdl.ParseConfig(...)see above for optionskdl.parsing.defaults: defaultParseConfigkdl.PrintConfig(...)see above for optionskdl.printing.defaults: defaultPrintConfigkdl.ParseError: thrown for all parsing errorserror.msg: str: hopefully informativeerror.line: int: 1-indexederror.col: int: 1-indexedkdl.ParseFragment: passed to converter functionspf.fragment: slice from the source stringpf.error(msg: str)returns akdl.ParseErrorwith error location set properly alreadykdl.nodeMatchesKey(val: Any, key: kdl.NodeKey) -> boolkdl.valueMatchesKey(val: Any, key: kdl.ValueKey) -> boolkdl.tagMatchesKey(val: str|None, key: kdl.TagKey) -> boolkdl.nameMatchesKey(val: str|None, key: kdl.NameKey) -> boolkdl.typeMatchesKey(val: str|None, key: kdl.TypeKey) -> boolFunctions implementing the tag/name/type matching
used by thenode.matchesKey()andvalue.matchesKey()methods,
in case you want to use the same filtering yourself.† Not produced by the parser.
Can be returned by a user's.to_kdl()method
if they want to produce a valuepreciselyin a particular syntax,
in a way that the built-in kdl-py classes don't.‡ Not produced by the parser.
These are abstract base classes to help in type testing:Valuematches all eight value classes,Numberishmatches all four numeric value classes,
andStringishmatches both string value classes.A few type aliases also exist,
used by the module in a few places
and potentially useful for your code:kdl.KDLAny: aDocument,Node, or any of theValuesubtypeskdl.KDLValue: any of theValuesubtypeskdl.KDLishValue: aKDLValueor one of the supported Python native typessee "Inserting Native Types"kdl.ValueKey,kdl.NodeKey,kdl.TagKey,kdl.NameKey,kdl.TypeKey:
the types forValueKeysandNodeKeys(and their individual pieces)These aliases only exist whentyping.TYPE_CHECKINGis true,
so they'reonlyuseful for writing types;
they won't be visible at runtime.NodeKeyA few data structures and functions take aNodeKeyto match against a node,
based on its name and/or tag.ANodeKeyis either aNameKeyor a(TagKey, NameKey)tuple,
matching against the node's name and/or tag.NameKeys andTagKeys can be:None:
When matched against a tag, only succeeds against aNonetag
(aka a tagless node likefoo).
When matched against a name, automatically succeeds,
since nodes are guaranteed to have a name....:
Always succeeds.
Use this when, say,
you want to match against a particular tag, regardless of the nodename,
like("my-tag", ...).astr:
Succeeds if the tag/name is that exact string.are.Pattern(a regex, such asre.compile(r".*foo"):
Succeeds if the regex matches the tag/name.
Note that this uses.match()semantics,
automatically anchoring against the start of the string;
prepend your regex with.*?if you want it to match anywhere.a function, taking astr | Noneand returning abool:
Succeeds if the function,
when called with the tag/name,
returns True.So, for example,doc.getAll("foo")would return all nodes whose name is "foo",
regardless of tag.doc.getAll(("my-tag", ...))would return all nodes whose tag is "my-tag",
regardless of the node name.doc.getAll(re.compile(r"-"))would return all nodes whose name starts with "-".
Etc.ValueKeySimilarly toNodeKey, a few things take aValueKeyto match against values,
based on its tag and/or type.AValueKeyis either aTagKeyor a(TagKey, TypeKey)tuple,
matching against the value's tag and/or type.TagKeyworks identically to how it appears inNodeKey.TypeKeyis either..., which matches any type,
or the same sort of argument you'd pass as the second argument toisinstance().For example,
a"foo"key would match any values with the tag "foo", like(foo)1or(foo)"bar".
A(..., kdl.Numberish)will match any value that's a "number" type: a Hex, Octal, Binary, or Decimal, regardless of its tag.
Etc.kdlreformatThekdlreformatcommand-line program is installed by default
when you install this module from pypi.
It can also be run manually from thekdlreformat.pyfile
at the root of this repository
(or from thekdl.cli.cli()function)usage: kdlreformat [-h] [--indent INDENT] [--semicolons] [--radix]
                   [--no-radix] [--raw-strings] [--no-raw-strings]
                   [--exponent EXPONENT]
                   [infile] [outfile]

KDL parser/printer, letting you easily reformat KDL files into a canonical
representation.

positional arguments:
  infile
  outfile

optional arguments:
  -h, --help           show this help message and exit
  --indent INDENT      How many spaces for each level of indent. -1 indicates
                       to indent with tabs.
  --semicolons         Whether to end nodes with semicolons or not.
  --radix              Output numeric values in the radix used by the input.
                       (0x1a outputs as 0x1a)
  --no-radix           Convert all numeric arguments to decimal. (0x1a outputs
                       as 26)
  --raw-strings        Output string values in the string type used by the
                       input.
  --no-raw-strings     Convert all string arguments into plain strings.
  --exponent EXPONENT  What character to use ('e' or 'E') for indicating
                       exponents on scinot numbers.