Skip to content

Commit

Permalink
Merge pull request #7 from tjol/kdl-v2
Browse files Browse the repository at this point in the history
Implements support for KDLv2, but does not activate it by default
  • Loading branch information
tjol authored Oct 12, 2024
2 parents 817e670 + eaae77f commit eea9944
Show file tree
Hide file tree
Showing 900 changed files with 2,990 additions and 345 deletions.
2 changes: 1 addition & 1 deletion COPYING
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2022 Thomas Jollans
Copyright (c) 2022-2024 Thomas Jollans

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ under `doc/` in this repository.

### Status

ckdl passes all test cases in the KDL test suite except for one where a number is
formatted a little differently.
ckdl has full support for **KDL 1.0.0** and passes the upstream test suite.

It's reasonable to suspect that ckdl is *close* to being standard-compliant, but
of course it's always possible there are bugs affecting some finer details.
ckdl has experimental (opt-in) support for a [draft version of KDL 2.0.0][kdl2].
For the time being, KDLv2 support has to be explicitly requested via parser/emitter
options; this behaviour is subject to change once KDLv2 is finalized.

The parser also supports a hybrid mode that accepts both KDLv2 and KDLv1
documents.
8 changes: 8 additions & 0 deletions bindings/cpp/include/kdlpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ typedef struct _kdl_parser kdl_parser;

namespace kdl {

enum class KdlVersion {
Kdl_1,
Kdl_2,
Any
};

template <typename T> concept _arithmetic = std::is_arithmetic_v<T>;

class TypeError : public std::exception {
Expand Down Expand Up @@ -346,10 +352,12 @@ class KDLPP_EXPORT Document {
auto end() { return m_nodes.end(); }

std::u8string to_string() const;
std::u8string to_string(KdlVersion version) const;
};

// Load a KDL document from string
KDLPP_EXPORT Document parse(std::u8string_view kdl_text);
KDLPP_EXPORT Document parse(std::u8string_view kdl_text, KdlVersion version);

} // namespace kdl

Expand Down
28 changes: 24 additions & 4 deletions bindings/cpp/src/kdlpp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,20 +201,40 @@ Document Document::read_from(kdl_parser* parser)
}
}

std::u8string Document::to_string() const
std::u8string Document::to_string() const { return to_string(KdlVersion::Kdl_1); }

std::u8string Document::to_string(KdlVersion version) const
{
kdl_emitter* emitter = kdl_create_buffering_emitter(&KDL_DEFAULT_EMITTER_OPTIONS);
kdl_emitter_options opts = KDL_DEFAULT_EMITTER_OPTIONS;

if (version == KdlVersion::Kdl_1) opts.version = KDL_VERSION_1;
if (version == KdlVersion::Kdl_2) opts.version = KDL_VERSION_2;

kdl_emitter* emitter = kdl_create_buffering_emitter(&opts);
if (emitter == nullptr) throw EmitterError{"Error initializing the KDL emitter"};
emit_nodes(emitter, m_nodes);
auto result = std::u8string{to_u8string_view(kdl_get_emitter_buffer(emitter))};
kdl_destroy_emitter(emitter);
return result;
}

Document parse(std::u8string_view kdl_text)
Document parse(std::u8string_view kdl_text) { return parse(kdl_text, KdlVersion::Kdl_1); }

Document parse(std::u8string_view kdl_text, KdlVersion version)
{
kdl_parse_option opts = KDL_DEFAULTS;
if (version == KdlVersion::Kdl_1) opts = KDL_READ_VERSION_1;
else if (version == KdlVersion::Kdl_2) opts = KDL_READ_VERSION_2;
else if (version == KdlVersion::Any) {
try {
return parse(kdl_text, KdlVersion::Kdl_2);
} catch (ParseError const&) {
return parse(kdl_text, KdlVersion::Kdl_1);
}
}

kdl_str text = {reinterpret_cast<char const*>(kdl_text.data()), kdl_text.size()};
kdl_parser* parser = kdl_create_string_parser(text, KDL_DEFAULTS);
kdl_parser* parser = kdl_create_string_parser(text, opts);
if (parser == nullptr) throw std::runtime_error("Error initializing the KDL parser");
auto doc = Document::read_from(parser);
kdl_destroy_parser(parser);
Expand Down
14 changes: 14 additions & 0 deletions bindings/cpp/tests/kdlpp_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,26 @@ static void test_writing_demo()
// clang-format off
}

static void test_cycle_kdl2()
{
auto txt1 = u8"node1 #true";

auto doc1 = kdl::parse(txt1, kdl::KdlVersion::Kdl_2);
auto txt2 = doc1.to_string(kdl::KdlVersion::Kdl_2);
ASSERT(txt2 == txt1);

auto doc2 = kdl::parse(txt1, kdl::KdlVersion::Any);
auto txt3 = doc2.to_string(kdl::KdlVersion::Kdl_2);
ASSERT(txt3 == txt1);
}

void TEST_MAIN()
{
run_test("kdlpp: cycle", &test_cycle);
run_test("kdlpp: constructors", &test_constructing);
run_test("kdlpp: Value::from_string", &test_value_from_string);
run_test("kdlpp: reading demo code", &test_reading_demo);
run_test("kdlpp: writing demo code", &test_writing_demo);
run_test("kdlpp: KDLv2 support", &test_cycle_kdl2);
}

46 changes: 42 additions & 4 deletions bindings/python/src/ckdl/_ckdl.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -416,18 +416,24 @@ cpdef enum IdentifierMode:
quote_all_identifiers = KDL_QUOTE_ALL_IDENTIFIERS
ascii_identifiers = KDL_ASCII_IDENTIFIERS

cpdef enum KdlVersion:
kdl_1 = KDL_VERSION_1
kdl_2 = KDL_VERSION_2

cdef class EmitterOptions:
cdef public int indent
cdef public EscapeMode escape_mode
cdef public IdentifierMode identifier_mode
cdef public FloatMode float_mode
cdef public KdlVersion version

def __init__(
self, *,
indent=None,
escape_mode=None,
identifier_mode=None,
float_mode=None):
float_mode=None,
version=None):
if indent is not None:
self.indent = indent
else:
Expand All @@ -444,20 +450,38 @@ cdef class EmitterOptions:
self.float_mode = float_mode
else:
self.float_mode = FloatMode()
if isinstance(version, int):
if version == 1:
self.version = KdlVersion.kdl_1
elif version == 2:
self.version = KdlVersion.kdl_2
else:
raise ValueError(f"Unknown version: {version}")
elif isinstance(version, KdlVersion):
self.version = version
elif version is not None:
raise TypeError(f"Expected int or KdlVersion for version, not {type(version)}")

cdef kdl_emitter_options _to_c_struct(self):
cdef kdl_emitter_options res
cdef kdl_emitter_options res = KDL_DEFAULT_EMITTER_OPTIONS
res.indent = self.indent
res.escape_mode = <kdl_escape_mode>self.escape_mode
res.identifier_mode = <kdl_identifier_emission_mode>self.identifier_mode
res.float_mode = self.float_mode._to_c_struct()
res.version = <kdl_version>self.version
return res

def parse(str kdl_text):
def parse(str kdl_text, *, version=1):
"""
parse(kdl_text)
Parse a KDL document (must be a str) and return a Document.
Pass a ``version`` argument to specify the KDL version (default: 1)
parse(kdl_text, version=1)
parse(kdl_text, version=2)
parse(kdl_text, version="any")
"""

cdef kdl_event_data* ev
Expand All @@ -468,10 +492,24 @@ def parse(str kdl_text):
cdef list nodes = root_node_list
cdef Node current_node = None

cdef kdl_parse_option parse_opt

if version in (1, '1', '1.0.0'):
parse_opt = KDL_READ_VERSION_1
elif version in (2, '2', '2.0.0'):
parse_opt = KDL_READ_VERSION_2
elif version in (None, 'detect'):
try:
return parse(kdl_text, version=2)
except ParseError:
return parse(kdl_text, version=1)
else:
raise ValueError(f"Unexpected value for version: {version}")

byte_str = kdl_text.encode("utf-8")
kdl_doc.data = byte_str
kdl_doc.len = len(byte_str)
parser = kdl_create_string_parser(kdl_doc, KDL_DEFAULTS)
parser = kdl_create_string_parser(kdl_doc, parse_opt)

while True:
ev = kdl_parser_next_event(parser)
Expand Down
11 changes: 11 additions & 0 deletions bindings/python/src/ckdl/_libkdl.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ cdef extern from "kdl/common.h":
KDL_ESCAPE_ASCII_MODE =0x170,
KDL_ESCAPE_DEFAULT = KDL_ESCAPE_CONTROL | KDL_ESCAPE_NEWLINE | KDL_ESCAPE_TAB

ctypedef enum kdl_version:
KDL_VERSION_1,
KDL_VERSION_2

ctypedef size_t (*kdl_read_func)(void *user_data, char *buf, size_t bufsize);
ctypedef size_t (*kdl_write_func)(void *user_data, const char *data, size_t nbytes);

Expand All @@ -27,6 +31,9 @@ cdef extern from "kdl/common.h":
cdef kdl_owned_string kdl_escape(const kdl_str *s, kdl_escape_mode mode)
cdef kdl_owned_string kdl_unescape(const kdl_str *s)

cdef kdl_owned_string kdl_escape_v(kdl_version version, const kdl_str *s, kdl_escape_mode mode)
cdef kdl_owned_string kdl_unescape_v(kdl_version version, const kdl_str *s)

cdef extern from "kdl/value.h":
ctypedef enum kdl_type:
KDL_TYPE_NULL,
Expand Down Expand Up @@ -65,6 +72,9 @@ cdef extern from "kdl/parser.h":
ctypedef enum kdl_parse_option:
KDL_DEFAULTS = 0,
KDL_EMIT_COMMENTS = 1,
KDL_READ_VERSION_1 = 0x20000,
KDL_READ_VERSION_2 = 0x40000,
KDL_DETECT_VERSION = 0x70000

ctypedef struct kdl_event_data:
kdl_event event
Expand Down Expand Up @@ -99,6 +109,7 @@ cdef extern from "kdl/emitter.h":
kdl_escape_mode escape_mode
kdl_identifier_emission_mode identifier_mode
kdl_float_printing_options float_mode
kdl_version version

cdef kdl_emitter_options KDL_DEFAULT_EMITTER_OPTIONS

Expand Down
45 changes: 41 additions & 4 deletions bindings/python/tests/ckdl_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def _dedent_str(self, s):
dedented = [l[indent:] if l.startswith(indent_str) else l for l in lines]
return "\n".join(dedented)

def test_simple_parsing(self):
def test_simple_parsing_v1(self):
kdl = '(tp)node "arg1" 2 3; node2'
doc = ckdl.parse(kdl)
doc = ckdl.parse(kdl, version=1)
self.assertEqual(len(doc), 2)
self.assertEqual(doc[0].type_annotation, "tp")
self.assertEqual(doc[0].name, "node")
Expand All @@ -39,7 +39,22 @@ def test_simple_parsing(self):
self.assertEqual(doc[1].children, [])
self.assertEqual(doc[1].properties, {})

def test_simple_emission(self):
def test_simple_parsing_v2(self):
kdl = '(tp)node arg1 2 3; node2'
doc = ckdl.parse(kdl, version=2)
self.assertEqual(len(doc), 2)
self.assertEqual(doc[0].type_annotation, "tp")
self.assertEqual(doc[0].name, "node")
self.assertEqual(doc[0].args, ["arg1", 2, 3])
self.assertEqual(doc[0].children, [])
self.assertEqual(doc[0].properties, {})
self.assertIsNone(doc[1].type_annotation)
self.assertEqual(doc[1].name, "node2")
self.assertEqual(doc[1].args, [])
self.assertEqual(doc[1].children, [])
self.assertEqual(doc[1].properties, {})

def test_simple_emission_v1(self):
doc = ckdl.Document(
ckdl.Node(
None,
Expand All @@ -60,7 +75,29 @@ def test_simple_emission(self):
"""
)
self.assertEqual(str(doc), expected)
self.assertEqual(doc.dump(), expected)
self.assertEqual(doc.dump(opts=ckdl.EmitterOptions(version=1)), expected)

def test_simple_emission_v2(self):
doc = ckdl.Document(
ckdl.Node(
None,
"-",
"foo",
100,
None,
ckdl.Node("child1", a=ckdl.Value("i8", -1)),
ckdl.Node("child2", True),
)
)
expected = self._dedent_str(
"""
- foo 100 #null {
child1 a=(i8)-1
child2 #true
}
"""
)
self.assertEqual(doc.dump(ckdl.EmitterOptions(version=2)), expected)

def test_node_constructors(self):
doc = ckdl.Document(
Expand Down
Loading

0 comments on commit eea9944

Please sign in to comment.