diff --git a/CMakeLists.txt b/CMakeLists.txt index f8f5670..8f29c77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,11 @@ project(ckdl C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_EXTENSIONS OFF) +set(KDL_COMPILE_OPTIONS) + if(MSVC) string(REGEX REPLACE "/W3" "/W4" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") - add_compile_options(/wd4996 /wd5105) + list(APPEND KDL_COMPILE_OPTIONS /wd4996 /wd5105) add_definitions(-D_CRT_SECURE_NO_WARNINGS) else() # try GCC-like options @@ -25,7 +27,7 @@ else() foreach(flag ${warning_flags}) check_c_compiler_flag("-${flag}" "COMPILER_FLAG_${flag}") if(${COMPILER_FLAG_${flag}}) - add_compile_options("-${flag}") + list(APPEND KDL_COMPILE_OPTIONS "-${flag}") endif() endforeach() endif() @@ -58,9 +60,11 @@ set(KDL_UTF8_C_SOURCES ) add_library(kdl-utf8 STATIC ${KDL_UTF8_C_SOURCES}) +target_compile_options(kdl-utf8 PRIVATE ${KDL_COMPILE_OPTIONS}) target_include_directories(kdl-utf8 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src) add_library(kdl ${KDL_C_SOURCES}) +target_compile_options(kdl PRIVATE ${KDL_COMPILE_OPTIONS}) target_link_libraries(kdl PRIVATE kdl-utf8 math) target_include_directories(kdl PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_compile_definitions(kdl PRIVATE -DBUILDING_KDL=1) diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt index 86b66ab..d7a3427 100644 --- a/bindings/cpp/CMakeLists.txt +++ b/bindings/cpp/CMakeLists.txt @@ -23,6 +23,7 @@ if(BUILD_KDLPP) ) add_library(kdlpp STATIC ${KDLPP_CXX_SOURCES}) + target_compile_options(kdlpp PRIVATE ${KDL_COMPILE_OPTIONS}) target_include_directories(kdlpp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(kdlpp PUBLIC kdl) target_compile_features(kdlpp PUBLIC cxx_std_20) diff --git a/bindings/python/src/ckdl/_ckdl.pyx b/bindings/python/src/ckdl/_ckdl.pyx index ed263ca..644095d 100644 --- a/bindings/python/src/ckdl/_ckdl.pyx +++ b/bindings/python/src/ckdl/_ckdl.pyx @@ -30,6 +30,11 @@ cdef class Value: def __str__(self): return str(Document(Node("-", [self])))[2:].strip() + def __eq__(self, other): + return (isinstance(other, type(self)) + and other.type_annotation == self.type_annotation + and other.value == self.value) + cdef _convert_kdl_value_no_type(const kdl_value* v): if v.type == KDL_TYPE_NULL: return None @@ -173,6 +178,14 @@ cdef class Node: def __str__(self): return str(Document(self)) + def __eq__(self, other): + return (isinstance(other, type(self)) + and other.type_annotation == self.type_annotation + and other.name == self.name + and other.args == self.args + and other.properties == self.properties + and other.children == self.children) + cdef kdl_value _make_kdl_value(value, kdl_owned_string* tmp_str_t, kdl_owned_string* tmp_str_v): cdef kdl_value result result.type_annotation.data = NULL @@ -302,14 +315,19 @@ cdef class Document: def __str__(self): return self.dump() - def dump(self): + def __eq__(self, other): + return isinstance(other, type(self)) and self.nodes == other.nodes + + def dump(self, EmitterOptions opts=EmitterOptions()): """Convert to KDL""" cdef kdl_emitter *emitter - cdef kdl_emitter_options opts + cdef kdl_emitter_options c_opts cdef kdl_str buf cdef str doc - emitter = kdl_create_buffering_emitter(&KDL_DEFAULT_EMITTER_OPTIONS) + c_opts = opts._to_c_struct() + + emitter = kdl_create_buffering_emitter(&c_opts) for node in self.nodes: _emit_node(emitter, node) @@ -321,6 +339,107 @@ cdef class Document: kdl_destroy_emitter(emitter) return doc +cpdef enum EscapeMode: + minimal = KDL_ESCAPE_MINIMAL + control = KDL_ESCAPE_CONTROL + newline = KDL_ESCAPE_NEWLINE + tab = KDL_ESCAPE_TAB + ascii_mode = KDL_ESCAPE_ASCII_MODE + default = KDL_ESCAPE_DEFAULT + +cdef class FloatMode: + cdef public bint always_write_decimal_point + cdef public bint always_write_decimal_point_or_exponent + cdef public bint capital_e + cdef public bint exponent_plus + cdef public bint plus + cdef public int min_exponent + + def __init__( + self, *, + always_write_decimal_point=None, + always_write_decimal_point_or_exponent=None, + capital_e=None, + exponent_plus=None, + plus=None, + min_exponent=None): + cdef kdl_float_printing_options* defaults = &KDL_DEFAULT_EMITTER_OPTIONS.float_mode + if always_write_decimal_point is not None: + self.always_write_decimal_point = always_write_decimal_point + else: + self.always_write_decimal_point = defaults.always_write_decimal_point + if always_write_decimal_point_or_exponent is not None: + self.always_write_decimal_point_or_exponent = always_write_decimal_point_or_exponent + else: + self.always_write_decimal_point_or_exponent = defaults.always_write_decimal_point_or_exponent + if capital_e is not None: + self.capital_e = capital_e + else: + self.capital_e = defaults.capital_e + if exponent_plus is not None: + self.exponent_plus = exponent_plus + else: + self.exponent_plus = defaults.exponent_plus + if plus is not None: + self.plus = plus + else: + self.plus = defaults.plus + if min_exponent is not None: + self.min_exponent = min_exponent + else: + self.min_exponent = defaults.min_exponent + + cdef kdl_float_printing_options _to_c_struct(self): + cdef kdl_float_printing_options res + res.always_write_decimal_point = self.always_write_decimal_point + res.always_write_decimal_point_or_exponent = self.always_write_decimal_point_or_exponent + res.capital_e = self.capital_e + res.exponent_plus = self.exponent_plus + res.plus = self.plus + res.min_exponent = self.min_exponent + return res + +cpdef enum IdentifierMode: + prefer_bare_identifiers = KDL_PREFER_BARE_IDENTIFIERS + quote_all_identifiers = KDL_QUOTE_ALL_IDENTIFIERS + ascii_identifiers = KDL_ASCII_IDENTIFIERS + +cdef class EmitterOptions: + cdef public int indent + cdef public EscapeMode escape_mode + cdef public IdentifierMode identifier_mode + cdef public FloatMode float_mode + + def __init__( + self, *, + indent=None, + escape_mode=None, + identifier_mode=None, + float_mode=None): + if indent is not None: + self.indent = indent + else: + self.indent = KDL_DEFAULT_EMITTER_OPTIONS.indent + if escape_mode is not None: + self.escape_mode = escape_mode + else: + self.escape_mode = KDL_DEFAULT_EMITTER_OPTIONS.escape_mode + if identifier_mode is not None: + self.identifier_mode = identifier_mode + else: + self.identifier_mode = KDL_DEFAULT_EMITTER_OPTIONS.identifier_mode + if float_mode is not None: + self.float_mode = float_mode + else: + self.float_mode = FloatMode() + + cdef kdl_emitter_options _to_c_struct(self): + cdef kdl_emitter_options res + res.indent = self.indent + res.escape_mode = self.escape_mode + res.identifier_mode = self.identifier_mode + res.float_mode = self.float_mode._to_c_struct() + return res def parse(str kdl_text): """ diff --git a/bindings/python/tests/ckdl_test.py b/bindings/python/tests/ckdl_test.py new file mode 100644 index 0000000..8d1750e --- /dev/null +++ b/bindings/python/tests/ckdl_test.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +import ckdl + +import unittest + + +class CKDLTest(unittest.TestCase): + def _dedent_str(self, s): + lines = s.split("\n") + while lines[0] in ("", "\r"): + lines = lines[1:] + indents = [] + for l in lines: + indent = 0 + for c in l: + if c == " ": + indent += 1 + else: + break + indents.append(indent) + indent = min(indents) + indent_str = " " * indent + dedented = [l[indent:] if l.startswith(indent_str) else l for l in lines] + return "\n".join(dedented) + + def test_simple_parsing(self): + kdl = '(tp)node "arg1" 2 3; node2' + doc = ckdl.parse(kdl) + 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(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(str(doc), expected) + self.assertEqual(doc.dump(), expected) + + def test_node_constructors(self): + doc = ckdl.Document( + ckdl.Node("n"), + ckdl.Node(None, "n"), + ckdl.Node(None, "n", "a"), + ckdl.Node("t", "n"), + ckdl.Node("n", None, True, False), + ckdl.Node("n", k="v"), + ckdl.Node("n", ckdl.Node("c1"), ckdl.Node("c2")), + ckdl.Node("n", 1, ckdl.Node("c1")), + ckdl.Node("t", "n", "a", k="v"), + ckdl.Node("n", [], []), + ckdl.Node("n", [1], [ckdl.Node("c1")]), + ckdl.Node( + "n", args=[None], properties={"#": True}, children=[ckdl.Node("c1")] + ), + ckdl.Node("n", [None], children=[ckdl.Node("c1")]), + ) + expected = self._dedent_str( + """ + n + n + n "a" + (t)n + n null true false + n k="v" + n { + c1 + c2 + } + n 1 { + c1 + } + (t)n "a" k="v" + n + n 1 { + c1 + } + n null #=true { + c1 + } + n null { + c1 + } + """ + ) + self.assertEqual(doc.dump(), expected) + + def test_emitter_opts(self): + doc = ckdl.Document(ckdl.Node("a", ckdl.Node(None, "🎉", "🎈", 0.002))) + expected_default = self._dedent_str( + """ + a { + 🎉 "🎈" 0.002 + } + """ + ) + self.assertEqual(doc.dump(), expected_default) + opt1 = ckdl.EmitterOptions( + indent=1, + escape_mode=ckdl.EscapeMode.ascii_mode, + float_mode=ckdl.FloatMode(min_exponent=2), + ) + expected_opt1 = self._dedent_str( + f""" + a {{ + 🎉 "\\u{{{ord('🎈'):x}}}" 2e-3 + }} + """ + ) + self.assertEqual(doc.dump(opt1), expected_opt1) + opt2 = ckdl.EmitterOptions( + indent=5, + identifier_mode=ckdl.IdentifierMode.quote_all_identifiers, + float_mode=ckdl.FloatMode(plus=True), + ) + expected_opt2 = self._dedent_str( + """ + "a" { + "🎉" "🎈" +0.002 + } + """ + ) + self.assertEqual(doc.dump(opt2), expected_opt2) + opt3 = ckdl.EmitterOptions( + indent=0, + identifier_mode=ckdl.IdentifierMode.ascii_identifiers, + float_mode=ckdl.FloatMode( + always_write_decimal_point=True, min_exponent=2, capital_e=True + ), + ) + expected_opt3 = self._dedent_str( + """ + a { + "🎉" "🎈" 2.0E-3 + } + """ + ) + self.assertEqual(doc.dump(opt2), expected_opt2) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyproject.toml b/pyproject.toml index 2beea4c..a1b5b5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ requires = [ "setuptools>=42", "scikit-build", "cmake>=3.8", - "ninja; platform_system!='Windows'" + "ninja; platform_system!='Windows'", + "cython" ] build-backend = "setuptools.build_meta" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 712b0d3..062891b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -108,6 +108,7 @@ if(NOT KDL_TEST_CASES_ROOT) endif() add_library(test_util STATIC test_util.c) +target_compile_options(test_util PUBLIC ${KDL_COMPILE_OPTIONS}) target_include_directories(test_util PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) add_executable(utf8_test utf8_test.c)