From a365ad94f2fb11f304f31a2f2a7a6be7076cc402 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Fri, 15 May 2020 14:14:00 +0200 Subject: [PATCH] bindings CHANGE rewrite python binding from scratch Signed-off-by: Robin Jarry --- .travis.yml | 2 +- CMakeLists.txt | 10 +- packages/CMakeLists.txt | 7 +- packages/debian.control.in | 23 +- packages/debian.python3-libyang.install | 1 + packages/debian.python3-yang.install | 1 - packages/debian.rules.in | 6 +- python/CMakeLists.txt | 86 ++ python/LICENSE | 1 + python/MANIFEST.in | 20 + python/README.md | 146 +++ python/build-so.sh | 26 + python/cffi/build.py | 35 + python/cffi/cdefs.h | 451 +++++++++ python/cffi/source.c | 23 + python/clib | 1 + python/libyang/__init__.py | 298 ++++++ python/libyang/data.py | 479 +++++++++ python/libyang/diff.py | 302 ++++++ python/libyang/schema.py | 957 ++++++++++++++++++ python/libyang/util.py | 28 + python/pydistutils.cfg.in | 13 + python/run_python.sh.in | 10 + python/setup.cfg | 55 + python/setup.py | 164 +++ python/tests/__init__.py | 2 + python/tests/test_context.py | 81 ++ python/tests/test_data.py | 347 +++++++ python/tests/test_diff.py | 59 ++ python/tests/test_schema.py | 363 +++++++ python/tests/yang-old/omg/omg-extensions.yang | 15 + python/tests/yang-old/wtf/wtf-types.yang | 53 + python/tests/yang-old/yolo/yolo-system.yang | 137 +++ python/tests/yang/omg/omg-extensions.yang | 15 + python/tests/yang/wtf/wtf-types.yang | 53 + python/tests/yang/yolo/yolo-system.yang | 142 +++ swig/CMakeLists.txt | 32 - swig/python/CMakeLists.txt | 61 -- swig/python/README.md | 20 - swig/python/config.py.in | 15 - swig/python/examples/context.py | 40 - swig/python/examples/imp_cb.py | 24 - swig/python/examples/process_tree.py | 67 -- swig/python/examples/subtype.py | 47 - swig/python/examples/xpath.py | 51 - swig/python/run_python_test.sh | 29 - swig/python/tests/test_libyang.py | 561 ---------- swig/python/tests/test_tree_data.py | 747 -------------- swig/python/tests/test_tree_schema.py | 591 ----------- swig/python/yang.i | 166 --- swig/swig_base/python_base.i | 4 - 51 files changed, 4400 insertions(+), 2467 deletions(-) create mode 100644 packages/debian.python3-libyang.install delete mode 100644 packages/debian.python3-yang.install create mode 100644 python/CMakeLists.txt create mode 120000 python/LICENSE create mode 100644 python/MANIFEST.in create mode 100644 python/README.md create mode 100755 python/build-so.sh create mode 100755 python/cffi/build.py create mode 100644 python/cffi/cdefs.h create mode 100644 python/cffi/source.c create mode 120000 python/clib create mode 100644 python/libyang/__init__.py create mode 100644 python/libyang/data.py create mode 100644 python/libyang/diff.py create mode 100644 python/libyang/schema.py create mode 100644 python/libyang/util.py create mode 100644 python/pydistutils.cfg.in create mode 100755 python/run_python.sh.in create mode 100644 python/setup.cfg create mode 100755 python/setup.py create mode 100644 python/tests/__init__.py create mode 100644 python/tests/test_context.py create mode 100644 python/tests/test_data.py create mode 100644 python/tests/test_diff.py create mode 100644 python/tests/test_schema.py create mode 100644 python/tests/yang-old/omg/omg-extensions.yang create mode 100644 python/tests/yang-old/wtf/wtf-types.yang create mode 100644 python/tests/yang-old/yolo/yolo-system.yang create mode 100644 python/tests/yang/omg/omg-extensions.yang create mode 100644 python/tests/yang/wtf/wtf-types.yang create mode 100644 python/tests/yang/yolo/yolo-system.yang delete mode 100644 swig/python/CMakeLists.txt delete mode 100644 swig/python/README.md delete mode 100755 swig/python/config.py.in delete mode 100644 swig/python/examples/context.py delete mode 100644 swig/python/examples/imp_cb.py delete mode 100644 swig/python/examples/process_tree.py delete mode 100644 swig/python/examples/subtype.py delete mode 100644 swig/python/examples/xpath.py delete mode 100755 swig/python/run_python_test.sh delete mode 100755 swig/python/tests/test_libyang.py delete mode 100755 swig/python/tests/test_tree_data.py delete mode 100755 swig/python/tests/test_tree_schema.py delete mode 100644 swig/python/yang.i delete mode 100644 swig/swig_base/python_base.i diff --git a/.travis.yml b/.travis.yml index 50ae758e5..9316b696b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,7 +44,7 @@ before_install: - cmake .. && make -j2 && sudo make install - cd ../.. - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get update -qq; sudo apt-get install -y valgrind libpcre3-dev python3-dev swig; fi + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get update -qq; sudo apt-get install -y valgrind libpcre3-dev python3-dev swig python3-cffi python3-setuptools; fi - if [ "$TRAVIS_OS_NAME" = "linux" -a "$CC" = "gcc" -a "$TRAVIS_ARCH" = "amd64" ]; then pip install --user codecov; export CFLAGS="-coverage"; fi script: diff --git a/CMakeLists.txt b/CMakeLists.txt index e1db9f0f0..0c9203584 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,9 +106,7 @@ set(USER_TYPES_PLUGINS_DIR_MACRO "${PLUGINS_DIR}/user_types") # setup bindings set(GEN_LANGUAGE_BINDINGS 0 CACHE BOOL "Enable language bindings generation.") set(GEN_CPP_BINDINGS 1 CACHE BOOL "Enable C++ bindings.") -# Python bindings depend on C++ bindings because of SWIG set(GEN_PYTHON_BINDINGS 1 CACHE BOOL "Enable Python bindings.") -set(GEN_PYTHON_VERSION "3" CACHE STRING "Python version") set(GEN_JAVASCRIPT_BINDINGS 0 CACHE BOOL "Enable JavaScript bindings.") set(GEN_JAVA_BINDINGS 0 CACHE BOOL "Enable Java bindings.") @@ -376,3 +374,11 @@ endif() if(GEN_LANGUAGE_BINDINGS AND GEN_CPP_BINDINGS) add_subdirectory(swig) endif() + +if(GEN_LANGUAGE_BINDINGS AND GEN_PYTHON_BINDINGS) + if(ENABLE_STATIC) + message(WARNING "Python bindings do not work with a static build.") + else() + add_subdirectory(python) + endif() +endif() diff --git a/packages/CMakeLists.txt b/packages/CMakeLists.txt index 674b2ace5..118eace88 100644 --- a/packages/CMakeLists.txt +++ b/packages/CMakeLists.txt @@ -1,6 +1,6 @@ set(PACKAGE "libyang") set(CPP_PACKAGE "libyang-cpp") -set(PYTHON_PACKAGE "python3-yang") +set(PYTHON_PACKAGE "python3-libyang") find_program(DEB_BUILDER NAMES debuild) find_program(RPM_BUILDER NAMES rpmbuild) @@ -22,9 +22,8 @@ configure_file(${PROJECT_SOURCE_DIR}/packages/debian.${CPP_PACKAGE}.install ${PROJECT_BINARY_DIR}/build-packages/debian.${CPP_PACKAGE}.install COPYONLY) configure_file(${PROJECT_SOURCE_DIR}/packages/debian.${CPP_PACKAGE}-dev.install ${PROJECT_BINARY_DIR}/build-packages/debian.${CPP_PACKAGE}-dev.install COPYONLY) -# no python package for Debian because there is only SWIG 3.10 on Debian 9 :-/ -#configure_file(${PROJECT_SOURCE_DIR}/packages/debian.${PYTHON_PACKAGE}.install -# ${PROJECT_BINARY_DIR}/build-packages/debian.${PYTHON_PACKAGE}.install COPYONLY) +configure_file(${PROJECT_SOURCE_DIR}/packages/debian.${PYTHON_PACKAGE}.install + ${PROJECT_BINARY_DIR}/build-packages/debian.${PYTHON_PACKAGE}.install COPYONLY) if(NOT DEB_BUILDER) message(STATUS "Missing tools (devscripts, debhelper package) for building DEB package.") diff --git a/packages/debian.control.in b/packages/debian.control.in index a7a720934..c50b536f5 100644 --- a/packages/debian.control.in +++ b/packages/debian.control.in @@ -2,7 +2,13 @@ Source: @PACKAGE@ Maintainer: CESNET Priority: extra Standards-Version: 3.8.2 -Build-Depends: debhelper (>= 9), gcc +Build-Depends: + debhelper (>= 9), + gcc, + cmake, + make, + python3 (>= 3.5), + dh-python (>= 3.20180313~), Homepage: https://github.com/CESNET/libyang Package: @PACKAGE@ @@ -40,3 +46,18 @@ Depends: @CPP_PACKAGE@ (=@LIBYANG_VERSION@) Section: debug Architecture: any Description: Debug symbols of C++ bidings of libyang library. + +Package: @PYTHON_PACKAGE@ +Depends: + ${misc:Depends}, + ${python3:Depends}, + @PACKAGE@ (=@LIBYANG_VERSION@), +Section: libs +Architecture: any +Description: Bindings of libyang library to Python language. + +Package: @PYTHON_PACKAGE@-dbg +Depends: @PYTHON_PACKAGE@ (=@LIBYANG_VERSION@) +Section: debug +Architecture: any +Description: Debug symbols of Python bindings of libyang library. diff --git a/packages/debian.python3-libyang.install b/packages/debian.python3-libyang.install new file mode 100644 index 000000000..b1a8294ae --- /dev/null +++ b/packages/debian.python3-libyang.install @@ -0,0 +1 @@ +usr/lib/python3*/dist-packages/* diff --git a/packages/debian.python3-yang.install b/packages/debian.python3-yang.install deleted file mode 100644 index 3272cd653..000000000 --- a/packages/debian.python3-yang.install +++ /dev/null @@ -1 +0,0 @@ -usr/lib/python3/dist-packages/* diff --git a/packages/debian.rules.in b/packages/debian.rules.in index 86ea390f8..8206db358 100644 --- a/packages/debian.rules.in +++ b/packages/debian.rules.in @@ -4,16 +4,16 @@ export DH_VERBOSE=1 %: - dh $@ + dh $@ --with python3 override_dh_strip: dh_strip -p@PACKAGE@ --dbg-package=@PACKAGE@-dbg dh_strip -p@CPP_PACKAGE@ --dbg-package=@CPP_PACKAGE@-dbg -# dh_strip -p@PYTHON_PACKAGE@ --dbg-package=@PYTHON_PACKAGE@-dbg + dh_strip -p@PYTHON_PACKAGE@ --dbg-package=@PYTHON_PACKAGE@-dbg override_dh_auto_configure: cmake -DCMAKE_INSTALL_PREFIX=/usr -DFORCE_INSRC_BUILD=ON -DCMAKE_BUILD_TYPE="Package" -DENABLE_LYD_PRIV=ON \ - -DGEN_LANGUAGE_BINDINGS=ON -DGEN_PYTHON_BINDINGS=OFF . + -DGEN_LANGUAGE_BINDINGS=ON -DGEN_PYTHON_BINDINGS=ON . override_dh_auto_test: ctest --output-on-failure diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt new file mode 100644 index 000000000..a442727ae --- /dev/null +++ b/python/CMakeLists.txt @@ -0,0 +1,86 @@ +unset(PYTHON_LIBRARY CACHE) +unset(PYTHON_EXECUTABLE CACHE) +unset(PYTHON_INCLUDE_DIR CACHE) + +find_package(PythonInterp 3 REQUIRED) +find_package(PythonLibs 3 REQUIRED) +foreach(dep setuptools cffi) + execute_process( + COMMAND ${PYTHON_EXECUTABLE} -B -c "import ${dep}" + RESULT_VARIABLE PYTHON_DEP_RESULT) + if(NOT ${PYTHON_DEP_RESULT} EQUAL 0) + message(FATAL_ERROR "Missing required python module: ${dep}") + endif() +endforeach() + +set(PYTHON_MODULE_PATH_CMD " +from distutils.sysconfig import get_python_lib +print(get_python_lib(prefix='${CMAKE_INSTALL_PREFIX}', plat_specific=True)) +") +execute_process( + COMMAND ${PYTHON_EXECUTABLE} -B -c "${PYTHON_MODULE_PATH_CMD}" + OUTPUT_VARIABLE PYTHON_MODULE_PATH + OUTPUT_STRIP_TRAILING_WHITESPACE) +set(PYTHON_LIB_BUILDDIR_CMD " +import sys +from distutils.util import get_platform +print('lib.' + get_platform() + '-{}.{}'.format(*sys.version_info[:2])) +") +execute_process( + COMMAND ${PYTHON_EXECUTABLE} -B -c "${PYTHON_LIB_BUILDDIR_CMD}" + OUTPUT_VARIABLE PYTHON_LIB_BUILDDIR + OUTPUT_STRIP_TRAILING_WHITESPACE) +configure_file(pydistutils.cfg.in .pydistutils.cfg @ONLY) + +configure_file( + ${CMAKE_SOURCE_DIR}/src/libyang.h.in + ${CMAKE_BINARY_DIR}/python/include/libyang/libyang.h @ONLY) +foreach(header ${headers}) + configure_file( + ${CMAKE_SOURCE_DIR}/${header} + ${CMAKE_BINARY_DIR}/python/include/libyang/ COPYONLY) +endforeach() + +# Cmake does not support exporting specific environment variables for +# custom_target commands. The only way is to use a shell script. +configure_file(run_python.sh.in run_python.sh @ONLY) +add_custom_target( + libyang_python ALL + COMMAND ${CMAKE_BINARY_DIR}/python/run_python.sh ./setup.py build + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/python + DEPENDS yang) +add_custom_target( + libyang_python_sdist + COMMAND ${CMAKE_BINARY_DIR}/python/run_python.sh ./setup.py sdist + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/python) + +if(ENABLE_BUILD_TESTS) + set(PYTHON_TESTS + tests/test_context.py + tests/test_data.py + tests/test_diff.py + tests/test_schema.py) + foreach(test_file ${PYTHON_TESTS}) + string( + REGEX REPLACE "tests/test_([a-z]+)\\.py" "test_python_\\1" + test_name ${test_file}) + add_test( + NAME ${test_name} + COMMAND ${PYTHON_EXECUTABLE} -B -m unittest -cv ${test_file} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/python) + set_property( + TEST ${test_name} + PROPERTY ENVIRONMENT + PYTHONPATH=${CMAKE_BINARY_DIR}/python/${PYTHON_LIB_BUILDDIR} + LD_LIBRARY_PATH=${CMAKE_BINARY_DIR} + LIBYANG_EXTENSIONS_PLUGINS_DIR=${CMAKE_BINARY_DIR}/src/extensions + LIBYANG_USER_TYPES_PLUGINS_DIR=${CMAKE_BINARY_DIR}/src/user_types) + endforeach() +endif() + +install(CODE " +message(STATUS \"${CMAKE_BINARY_DIR}/python/run_python.sh ./setup.py install --root=\$ENV{DESTDIR}/\") +execute_process( + COMMAND ${CMAKE_BINARY_DIR}/python/run_python.sh ./setup.py install --root=\$ENV{DESTDIR}/ + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/python) +") diff --git a/python/LICENSE b/python/LICENSE new file mode 120000 index 000000000..ea5b60640 --- /dev/null +++ b/python/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 000000000..14d29e6dc --- /dev/null +++ b/python/MANIFEST.in @@ -0,0 +1,20 @@ +graft cffi +graft clib +prune clib/.git +prune clib/doc +prune clib/python +prune clib/swig +prune clib/tests +prune clib/tools/lint/examples +graft libyang +prune libyang/_lib +prune libyang/_include +include build-so.sh +include setup.py +include setup.cfg +include LICENSE +include README.md +global-exclude *.py[co] +global-exclude *.so +global-exclude *.o +global-exclude *.a diff --git a/python/README.md b/python/README.md new file mode 100644 index 000000000..dec8687cd --- /dev/null +++ b/python/README.md @@ -0,0 +1,146 @@ +# libyang + +Python CFFI bindings to [libyang][github-link]. + +[![pypi project][pypi-img]][pypi-link] [![supported python versions][pyversions-img]][pypi-link] [![build status][travis-img]][travis-link] [![license][license-img]][github-link] + +[pypi-img]: https://img.shields.io/pypi/v/libyang.svg +[pypi-link]: https://pypi.org/project/libyang +[pyversions-img]: https://img.shields.io/pypi/pyversions/libyang.svg +[license-img]: https://img.shields.io/github/license/CESNET/libyang.svg +[travis-img]: https://api.travis-ci.com/CESNET/libyang.svg +[travis-link]: https://travis-ci.com/CESNET/libyang +[github-link]: https://github.com/CESNET/libyang + +## Installation + +```bash +pip install libyang +``` + +This assumes `libyang.so` is installed in the system and that `libyang.h` is +available in the system include dirs. + +You need the following system dependencies installed: + +- Python development headers +- GCC +- Python CFFI module + +On a Debian/Ubuntu system:: + +``` +sudo apt-get install python3-dev gcc python3-cffi +``` + +### Compilation Flags + +If libyang headers and libraries are installed in a non-standard location, you +can specify them with the `LIBYANG_HEADERS` and `LIBYANG_LIBRARIES` variables. +Additionally, for finer control, you may use `LIBYANG_EXTRA_CFLAGS` and +`LIBYANG_EXTRA_LDFLAGS`: + +```bash +LIBYANG_HEADERS=/home/build/opt/ly/include \ +LIBYANG_LIBRARIES=/home/build/opt/ly/lib \ +LIBYANG_EXTRA_CFLAGS="-O3" \ +LIBYANG_EXTRA_LDFLAGS="-rpath=/opt/ly/lib" \ + pip install libyang +``` + +### Embedding `libyang.so` + +If libyang headers and libraries are not installed on the system, you may +build `libyang.so` and embed it into the `libyang` package before linking +the CFFI extension against it (with a custom `RPATH`). + +To do so, you must export the `LIBYANG_INSTALL=embed` variable when running +pip: + +```bash +LIBYANG_INSTALL=embed pip install libyang +``` + +This requires additional system dependencies in order to build the libyang +C code: + +- cmake +- Lib PCRE development headers + +On a Debian/Ubuntu system: + +```bash +sudo apt-get install cmake build-essential libpcre3-dev +``` + +## Examples + +```pycon +>>> import libyang +>>> ctx = libyang.Context('/usr/local/share/yang/modules') +>>> module = ctx.load_module('ietf-system') +>>> print(module) +module: ietf-system + +--rw system + | +--rw contact? string + | +--rw hostname? ietf-inet-types:domain-name + | +--rw location? string + | +--rw clock + | | +--rw (timezone)? + | | +--:(timezone-utc-offset) + | | +--rw timezone-utc-offset? int16 + | +--rw dns-resolver + | +--rw search* ietf-inet-types:domain-name + | +--rw server* [name] + | | +--rw name string + | | +--rw (transport) + | | +--:(udp-and-tcp) + | | +--rw udp-and-tcp + | | +--rw address ietf-inet-types:ip-address + | +--rw options + | +--rw timeout? uint8 <5> + | +--rw attempts? uint8 <2> + +--ro system-state + +--ro platform + | +--ro os-name? string + | +--ro os-release? string + | +--ro os-version? string + | +--ro machine? string + +--ro clock + +--ro current-datetime? ietf-yang-types:date-and-time + +--ro boot-datetime? ietf-yang-types:date-and-time + + rpcs: + +---x set-current-datetime + | +---- input + | +---w current-datetime ietf-yang-types:date-and-time + +---x system-restart + +---x system-shutdown + +>>> xpath = '/ietf-system:system/ietf-system:dns-resolver/ietf-system:server' +>>> dnsserver = next(ctx.find_path(xpath)) +>>> dnsserver + +>>> print(dnsserver.description()) +List of the DNS servers that the resolver should query. + +When the resolver is invoked by a calling application, it +sends the query to the first name server in this list. If +no response has been received within 'timeout' seconds, +the resolver continues with the next server in the list. +If no response is received from any server, the resolver +continues with the first server again. When the resolver +has traversed the list 'attempts' times without receiving +any response, it gives up and returns an error to the +calling application. + +Implementations MAY limit the number of entries in this +list. +>>> dnsserver.ordered() +True +>>> for node in dnsserver: +... print(repr(node)) +... + + +``` diff --git a/python/build-so.sh b/python/build-so.sh new file mode 100755 index 000000000..b5efa23ed --- /dev/null +++ b/python/build-so.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -e + +src_dir="$1" +build_dir="$2" +install_dir="$3" + +mkdir -p "$build_dir" +cd "$build_dir" + +cmake -DCMAKE_BUILD_TYPE=release \ + -DENABLE_BUILD_TESTS=OFF \ + -DENABLE_VALGRIND_TESTS=OFF \ + -DENABLE_CALGRIND_TESTS=OFF \ + -DENABLE_BUILD_FUZZ_TARGETS=OFF \ + -DCMAKE_INSTALL_PREFIX=$install_dir \ + -DCMAKE_INSTALL_LIBDIR=$install_dir/_lib \ + -DCMAKE_INSTALL_INCLUDEDIR=$install_dir/_include \ + -DPLUGINS_DIR=$install_dir/_lib \ + -DGEN_LANGUAGE_BINDINGS=0 \ + "$src_dir" + +make -s -j$(nproc) +make -s install +rm -rf -- $install_dir/bin $install_dir/share $install_dir/_lib/pkgconfig diff --git a/python/cffi/build.py b/python/cffi/build.py new file mode 100755 index 000000000..a7b7cccfe --- /dev/null +++ b/python/cffi/build.py @@ -0,0 +1,35 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +import os +import shlex + +import cffi + + +HERE = os.path.dirname(__file__) + +BUILDER = cffi.FFI() +with open(os.path.join(HERE, 'cdefs.h')) as f: + BUILDER.cdef(f.read()) + +HEADERS = [] +if 'LIBYANG_HEADERS' in os.environ: + HEADERS.append(os.environ['LIBYANG_HEADERS']) +LIBRARIES = [] +if 'LIBYANG_LIBRARIES' in os.environ: + LIBRARIES.append(os.environ['LIBYANG_LIBRARIES']) +EXTRA_CFLAGS = ['-Werror', '-std=c99'] +EXTRA_CFLAGS += shlex.split(os.environ.get('LIBYANG_EXTRA_CFLAGS', '')) +EXTRA_LDFLAGS = shlex.split(os.environ.get('LIBYANG_EXTRA_LDFLAGS', '')) + +with open(os.path.join(HERE, 'source.c')) as f: + BUILDER.set_source('_libyang', f.read(), libraries=['yang'], + extra_compile_args=EXTRA_CFLAGS, + extra_link_args=EXTRA_LDFLAGS, + include_dirs=HEADERS, + library_dirs=LIBRARIES, + py_limited_api=False) + +if __name__ == '__main__': + BUILDER.compile() diff --git a/python/cffi/cdefs.h b/python/cffi/cdefs.h new file mode 100644 index 000000000..f6c59b996 --- /dev/null +++ b/python/cffi/cdefs.h @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2018-2019 Robin Jarry + * SPDX-License-Identifier: MIT + */ + +struct ly_ctx; + +#define LY_CTX_ALLIMPLEMENTED ... +#define LY_CTX_TRUSTED ... +#define LY_CTX_NOYANGLIBRARY ... +#define LY_CTX_DISABLE_SEARCHDIRS ... +#define LY_CTX_DISABLE_SEARCHDIR_CWD ... +#define LY_CTX_PREFER_SEARCHDIRS ... + +struct ly_ctx *ly_ctx_new(const char *, int); +int ly_ctx_set_searchdir(struct ly_ctx *, const char *); +void ly_ctx_destroy(struct ly_ctx *, void *); + +typedef enum { + LY_SUCCESS, + ... +} LY_ERR; + +typedef enum { + LYVE_SUCCESS, + LYVE_PATH_EXISTS, + ... +} LY_VECODE; + +typedef enum { + LY_LLERR, + LY_LLWRN, + LY_LLVRB, + LY_LLDBG, + ... +} LY_LOG_LEVEL; + +struct ly_err_item { + char *msg; + char *path; + char *apptag; + struct ly_err_item *next; + ...; +}; + +#define LY_LOLOG ... +#define LY_LOSTORE ... +#define LY_LOSTORE_LAST ... +int ly_log_options(int); + +LY_LOG_LEVEL ly_verb(LY_LOG_LEVEL); +extern "Python" void lypy_log_cb(LY_LOG_LEVEL, const char *, const char *); +void ly_set_log_clb(void (*)(LY_LOG_LEVEL, const char *, const char *), int); +struct ly_err_item *ly_err_first(const struct ly_ctx *); +void ly_err_clean(struct ly_ctx *, struct ly_err_item *); +LY_VECODE ly_vecode(const struct ly_ctx *); + +struct lys_module { + const char *name; + const char *prefix; + const char *dsc; + const char *filepath; + uint8_t rev_size; + uint8_t features_size; + struct lys_revision *rev; + struct lys_feature *features; + ...; +}; + +#define LY_REV_SIZE 11 +struct lys_revision { + char date[LY_REV_SIZE]; + uint8_t ext_size; + struct lys_ext_instance **ext; + const char *dsc; + const char *ref; +}; + +#define LYS_FENABLED ... +struct lys_feature { + const char *name; + const char *dsc; + const char *ref; + uint16_t flags; + uint8_t iffeature_size; + struct lys_iffeature *iffeature; + struct lys_module *module; + ...; +}; + +#define LYS_IFF_NOT ... +#define LYS_IFF_AND ... +#define LYS_IFF_OR ... +#define LYS_IFF_F ... +struct lys_iffeature { + uint8_t *expr; + struct lys_feature **features; + ...; +}; + +int lys_features_enable(const struct lys_module *, const char *); +int lys_features_disable(const struct lys_module *, const char *); +int lys_features_state(const struct lys_module *, const char *); + +struct lys_ext { + const char *name; + struct lys_module *module; + ...; +}; + +struct lys_ext_instance { + struct lys_ext *def; + const char *arg_value; + ...; +}; + +struct lys_restr { + const char *expr; + ...; +}; + +typedef enum { + LY_TYPE_DER, + LY_TYPE_BINARY, + LY_TYPE_BITS, + LY_TYPE_BOOL, + LY_TYPE_DEC64, + LY_TYPE_EMPTY, + LY_TYPE_ENUM, + LY_TYPE_IDENT, + LY_TYPE_INST, + LY_TYPE_LEAFREF, + LY_TYPE_STRING, + LY_TYPE_UNION, + LY_TYPE_INT8, + LY_TYPE_UINT8, + LY_TYPE_INT16, + LY_TYPE_UINT16, + LY_TYPE_INT32, + LY_TYPE_UINT32, + LY_TYPE_INT64, + LY_TYPE_UINT64, + LY_TYPE_UNKNOWN, + ... +} LY_DATA_TYPE; + +struct lys_type_info_binary { + struct lys_restr *length; +}; + +struct lys_type_bit { + const char *name; + const char *dsc; + uint32_t pos; + ...; +}; + +struct lys_type_info_bits { + struct lys_type_bit *bit; + unsigned int count; +}; + +struct lys_type_info_dec64 { + struct lys_restr *range; + ...; +}; + +struct lys_type_enum { + const char *name; + const char *dsc; + ...; +}; + +struct lys_type_info_enums { + struct lys_type_enum *enm; + unsigned int count; +}; + +struct lys_type_info_num { + struct lys_restr *range; +}; + +struct lys_type_info_lref { + const char *path; + struct lys_node_leaf* target; + int8_t req; +}; + +struct lys_type_info_str { + struct lys_restr *length; + struct lys_restr *patterns; + unsigned int pat_count; + ...; +}; + +struct lys_type_info_union { + struct lys_type *types; + unsigned int count; + int has_ptr_type; +}; + +union lys_type_info { + struct lys_type_info_binary binary; + struct lys_type_info_bits bits; + struct lys_type_info_dec64 dec64; + struct lys_type_info_enums enums; + struct lys_type_info_num num; + struct lys_type_info_lref lref; + struct lys_type_info_str str; + struct lys_type_info_union uni; + ...; +}; + +struct lys_type { + LY_DATA_TYPE base; + uint8_t value_flags; + uint8_t ext_size; + struct lys_ext_instance **ext; + struct lys_tpdf *der; + struct lys_tpdf *parent; + union lys_type_info info; + ...; +}; + +struct lys_tpdf { + const char *name; + const char *dsc; + uint8_t ext_size; + struct lys_ext_instance **ext; + const char *units; + struct lys_type type; + const char *dflt; + ...; +}; + +typedef enum lys_nodetype { + LYS_UNKNOWN, + LYS_CONTAINER, + LYS_CHOICE, + LYS_LEAF, + LYS_LEAFLIST, + LYS_LIST, + LYS_ANYXML, + LYS_CASE, + LYS_NOTIF, + LYS_RPC, + LYS_INPUT, + LYS_OUTPUT, + LYS_GROUPING, + LYS_USES, + LYS_AUGMENT, + LYS_ACTION, + LYS_ANYDATA, + LYS_EXT, + ... +} LYS_NODE; + +#define LYS_CONFIG_W ... +#define LYS_CONFIG_R ... +#define LYS_CONFIG_SET ... +#define LYS_USERORDERED ... +#define LYS_MAND_TRUE ... +#define LYS_STATUS_DEPRC ... +#define LYS_STATUS_OBSLT ... + +struct lys_node { + const char *name; + const char *dsc; + uint16_t flags; + uint8_t ext_size; + uint8_t iffeature_size; + struct lys_ext_instance **ext; + struct lys_iffeature *iffeature; + LYS_NODE nodetype; + ...; +}; + +struct lys_node_container { + uint8_t must_size; + struct lys_restr *must; + const char *presence; + ...; +}; + +struct lys_node_leaf { + uint8_t must_size; + struct lys_restr *must; + struct lys_type type; + const char *units; + const char *dflt; + ...; +}; + +struct lys_node_leaflist { + uint8_t must_size; + struct lys_restr *must; + struct lys_type type; + const char *units; + uint32_t min; + uint32_t max; + uint8_t dflt_size; + const char **dflt; + ...; +}; + +struct lys_node_list { + uint8_t must_size; + struct lys_restr *must; + uint8_t keys_size; + struct lys_node_leaf **keys; + uint32_t min; + uint32_t max; + ...; +}; + +union ly_set_set { + struct lys_node **s; + ...; +}; + +struct ly_set { + unsigned int size; + unsigned int number; + union ly_set_set set; +}; + +const struct lys_module *ly_ctx_load_module(struct ly_ctx *, const char *, const char *); +const struct lys_module *ly_ctx_get_module_iter(const struct ly_ctx *, uint32_t *); +const struct lys_module *ly_ctx_get_module(const struct ly_ctx *, const char *, const char *, int); +struct ly_set *ly_ctx_find_path(struct ly_ctx *, const char *); +void ly_set_free(struct ly_set *set); +const struct lys_node_list *lys_is_key(const struct lys_node_leaf *, uint8_t *); + +#define LYS_GETNEXT_WITHCHOICE ... +#define LYS_GETNEXT_WITHCASE ... +#define LYS_GETNEXT_WITHGROUPING ... +#define LYS_GETNEXT_WITHINOUT ... +#define LYS_GETNEXT_WITHUSES ... +#define LYS_GETNEXT_INTOUSES ... +#define LYS_GETNEXT_INTONPCONT ... +#define LYS_GETNEXT_PARENTUSES ... +#define LYS_GETNEXT_NOSTATECHECK ... + +const struct lys_node *lys_getnext(const struct lys_node *, const struct lys_node *, const struct lys_module *, int); +char *lys_data_path(const struct lys_node *); +char *lys_path(const struct lys_node *, int); +char *lys_data_path_pattern(const struct lys_node *, const char *); +struct lys_module *lys_node_module(const struct lys_node *); +struct lys_module *lys_main_module(const struct lys_module *); +struct lys_node *lys_parent(const struct lys_node *); + +typedef enum { + LYS_IN_UNKNOWN, + LYS_IN_YANG, + LYS_IN_YIN, + ... +} LYS_INFORMAT; + +const struct lys_module *lys_parse_mem(struct ly_ctx *, const char *, LYS_INFORMAT); +const struct lys_module *lys_parse_fd(struct ly_ctx *, int, LYS_INFORMAT); + +typedef enum { + LYS_OUT_UNKNOWN, + LYS_OUT_YANG, + LYS_OUT_YIN, + LYS_OUT_TREE, + LYS_OUT_INFO, + LYS_OUT_JSON, + ... +} LYS_OUTFORMAT; + +int lys_print_mem(char **, const struct lys_module *, LYS_OUTFORMAT, const char *, int, int); +int lys_print_fd(int, const struct lys_module *, LYS_OUTFORMAT, const char *, int, int); + +typedef enum { + LYD_XML, + LYD_JSON, + LYD_LYB, + ... +} LYD_FORMAT; + +typedef enum { + ... +} LYD_ANYDATA_VALUETYPE; + +#define LYP_WITHSIBLINGS ... +#define LYP_FORMAT ... +#define LYP_WD_TRIM ... +#define LYP_WD_ALL ... +#define LYP_KEEPEMPTYCONT ... + +#define LYD_ANYDATA_CONSTSTRING ... + +#define LYD_PATH_OPT_UPDATE ... +#define LYD_PATH_OPT_NOPARENTRET ... +#define LYD_PATH_OPT_OUTPUT ... + +#define LYD_OPT_DATA ... +#define LYD_OPT_CONFIG ... +#define LYD_OPT_STRICT ... +#define LYD_OPT_TRUSTED ... +#define LYD_OPT_DATA_NO_YANGLIB ... +#define LYD_OPT_RPC ... + +typedef union lyd_value_u { + int8_t bln; + struct lyd_node *leafref; + ...; +} lyd_val; + +struct lyd_node { + struct lys_node *schema; + struct lyd_node *next; + struct lyd_node *child; + struct lyd_node *parent; + ...; +}; + +struct lyd_node_leaf_list { + struct lys_node *schema; + struct lyd_node *next; + struct lyd_node *parent; + const char *value_str; + lyd_val value; + LY_DATA_TYPE value_type; + ...; +}; + +struct ly_set *lyd_find_instance(const struct lyd_node *, const struct lys_node *); +struct ly_set *lyd_find_path(const struct lyd_node *, const char *); +struct lyd_node *lyd_new_path(struct lyd_node *, const struct ly_ctx *, const char *, void *, LYD_ANYDATA_VALUETYPE, int); +struct lyd_node *lyd_first_sibling(struct lyd_node *); +char *lyd_path(const struct lyd_node *); +struct lys_module *lyd_node_module(struct lyd_node *); +struct lyd_node *lyd_parse_mem(struct ly_ctx *, const char *, LYD_FORMAT, int, ...); +struct lyd_node *lyd_parse_fd(struct ly_ctx *, int, LYD_FORMAT, int, ...); +int lyd_print_fd(int, const struct lyd_node *, LYD_FORMAT, int); +int lyd_print_mem(char **, const struct lyd_node *, LYD_FORMAT, int); +int lyd_node_should_print(const struct lyd_node *, int); +double lyd_dec64_to_double(const struct lyd_node *); +void lyd_free(struct lyd_node *); +void lyd_free_withsiblings(struct lyd_node *); +int lyd_validate(struct lyd_node **, int, void *); + +/* from libc, needed to free allocated strings */ +void free(void *); + +/* extra functions */ +uint8_t lypy_module_implemented(const struct lys_module *); +LY_ERR lypy_get_errno(void); +void lypy_set_errno(LY_ERR); diff --git a/python/cffi/source.c b/python/cffi/source.c new file mode 100644 index 000000000..2df849f43 --- /dev/null +++ b/python/cffi/source.c @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2019 Robin Jarry + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +static LY_ERR lypy_get_errno(void) +{ + return ly_errno; +} + +static void lypy_set_errno(LY_ERR err) +{ + ly_errno = err; +} + +static uint8_t lypy_module_implemented(const struct lys_module *module) +{ + if (module) + return module->implemented; + return 0; +} diff --git a/python/clib b/python/clib new file mode 120000 index 000000000..a96aa0ea9 --- /dev/null +++ b/python/clib @@ -0,0 +1 @@ +.. \ No newline at end of file diff --git a/python/libyang/__init__.py b/python/libyang/__init__.py new file mode 100644 index 000000000..2f8fbf0a7 --- /dev/null +++ b/python/libyang/__init__.py @@ -0,0 +1,298 @@ +# Copyright (c) 2018-2019 Robin Jarry +# Copyright (c) 2020 6WIND S.A. +# SPDX-License-Identifier: BSD-3-Clause + +import os # isort:skip + +# Important: the following *must* remain *before* the import of _libyang +HERE = os.path.dirname(__file__) +LIBDIR = os.path.join(HERE, '_lib') +INCLUDEDIR = os.path.join(HERE, '_include') +if os.path.isdir(LIBDIR): + os.environ.setdefault( + 'LIBYANG_EXTENSIONS_PLUGINS_DIR', os.path.join(LIBDIR, 'extensions')) + os.environ.setdefault( + 'LIBYANG_USER_TYPES_PLUGINS_DIR', os.path.join(LIBDIR, 'user_types')) + +import logging + +from _libyang import ffi +from _libyang import lib + +from .data import DNode +from .data import data_format +from .data import parser_flags +from .data import path_flags +from .schema import Module +from .schema import SNode +from .schema import schema_in_format +from .util import LibyangError +from .util import c2str +from .util import str2c + + +LOG = logging.getLogger(__name__) +LOG.addHandler(logging.NullHandler()) + + +#------------------------------------------------------------------------------ +class Context(object): + + def __init__(self, search_path=None, disable_searchdir_cwd=True, pointer=None): + if pointer is not None: + self._ctx = ffi.cast('struct ly_ctx *', pointer) + return # already initialized + + options = 0 + if disable_searchdir_cwd: + options |= lib.LY_CTX_DISABLE_SEARCHDIR_CWD + + self._ctx = lib.ly_ctx_new(ffi.NULL, options) + if not self._ctx: + raise self.error('cannot create context') + + search_dirs = [] + if 'YANGPATH' in os.environ: + search_dirs.extend( + os.environ['YANGPATH'].strip(': \t\r\n\'"').split(':')) + elif 'YANG_MODPATH' in os.environ: + search_dirs.extend( + os.environ['YANG_MODPATH'].strip(': \t\r\n\'"').split(':')) + if search_path: + search_dirs.extend(search_path.strip(': \t\r\n\'"').split(':')) + + for path in search_dirs: + if not os.path.isdir(path): + continue + if lib.ly_ctx_set_searchdir(self._ctx, str2c(path)) != 0: + raise self.error('cannot set search dir') + + def destroy(self): + if self._ctx is not None: + lib.ly_ctx_destroy(self._ctx, ffi.NULL) + self._ctx = None + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.destroy() + + def error(self, msg, *args): + msg %= args + + if self._ctx: + err = lib.ly_err_first(self._ctx) + if err: + if err.msg: + msg += ': %s' % c2str(err.msg) + if err.path: + msg += ': %s' % c2str(err.path) + lib.ly_err_clean(self._ctx, ffi.NULL) + + return LibyangError(msg) + + def parse_module_file(self, fileobj, fmt='yang'): + if self._ctx is None: + raise RuntimeError('context already destroyed') + fmt = schema_in_format(fmt) + mod = lib.lys_parse_fd(self._ctx, fileobj.fileno(), fmt) + if not mod: + raise self.error('cannot parse module') + + return Module(self, mod) + + def parse_module_str(self, s, fmt='yang'): + if self._ctx is None: + raise RuntimeError('context already destroyed') + fmt = schema_in_format(fmt) + mod = lib.lys_parse_mem(self._ctx, str2c(s), fmt) + if not mod: + raise self.error('cannot parse module') + + return Module(self, mod) + + def load_module(self, name): + if self._ctx is None: + raise RuntimeError('context already destroyed') + mod = lib.ly_ctx_load_module(self._ctx, str2c(name), ffi.NULL) + if not mod: + raise self.error('cannot load module') + + return Module(self, mod) + + def get_module(self, name): + if self._ctx is None: + raise RuntimeError('context already destroyed') + mod = lib.ly_ctx_get_module(self._ctx, str2c(name), ffi.NULL, False) + if not mod: + raise self.error('cannot get module') + + return Module(self, mod) + + def find_path(self, path): + if self._ctx is None: + raise RuntimeError('context already destroyed') + node_set = lib.ly_ctx_find_path(self._ctx, str2c(path)) + if not node_set: + raise self.error('cannot find path') + try: + for i in range(node_set.number): + yield SNode.new(self, node_set.set.s[i]) + finally: + lib.ly_set_free(node_set) + + def create_data_path(self, path, parent=None, value=None, + update=True, no_parent_ret=True, rpc_output=False, + force_return_value=True): + if self._ctx is None: + raise RuntimeError('context already destroyed') + lib.lypy_set_errno(0) + if value is not None: + if isinstance(value, bool): + value = str(value).lower() + elif not isinstance(value, str): + value = str(value) + flags = path_flags( + update=update, no_parent_ret=no_parent_ret, rpc_output=rpc_output) + dnode = lib.lyd_new_path( + parent._node if parent else ffi.NULL, + self._ctx, str2c(path), str2c(value), 0, flags) + if lib.lypy_get_errno() != lib.LY_SUCCESS: + if lib.ly_vecode(self._ctx) != lib.LYVE_PATH_EXISTS: + raise self.error('cannot create data path') + if not dnode and not force_return_value: + return None + + if not dnode and parent: + # This can happen when path points to an already created leaf and + # its value does not change. + # In that case, lookup the existing leaf and return it. + node_set = lib.lyd_find_path(parent._node, str2c(path)) + try: + if not node_set or not node_set.number: + raise self.error('cannot find path') + dnode = node_set.set.s[0] + finally: + lib.ly_set_free(node_set) + + if not dnode: + raise self.error('cannot find created path') + + return DNode.new(self, dnode) + + def parse_data_mem(self, d, fmt, data=False, config=False, strict=False, + trusted=False, no_yanglib=False, rpc=False): + if self._ctx is None: + raise RuntimeError('context already destroyed') + flags = parser_flags( + data=data, config=config, strict=strict, trusted=trusted, + no_yanglib=no_yanglib, rpc=rpc) + fmt = data_format(fmt) + if fmt == lib.LYD_LYB: + d = str2c(d, encode=False) + else: + d = str2c(d, encode=True) + args = [] + if rpc: + args.append(ffi.cast('struct lyd_node *', ffi.NULL)) + dnode = lib.lyd_parse_mem(self._ctx, d, fmt, flags, *args) + if not dnode: + raise self.error('failed to parse data tree') + return DNode.new(self, dnode) + + def parse_data_file(self, fileobj, fmt, data=False, config=False, + strict=False, trusted=False, no_yanglib=False): + if self._ctx is None: + raise RuntimeError('context already destroyed') + flags = parser_flags( + data=data, config=config, strict=strict, trusted=trusted, + no_yanglib=no_yanglib) + fmt = data_format(fmt) + args = [] + if rpc: + args.append(ffi.cast('struct lyd_node *', ffi.NULL)) + dnode = lib.lyd_parse_fd(self._ctx, fileobj.fileno(), fmt, flags, *args) + if not dnode: + raise self.error('failed to parse data tree') + return DNode.new(self, dnode) + + def __iter__(self): + """ + Return an iterator that yields all implemented modules from the context + """ + if self._ctx is None: + raise RuntimeError('context already destroyed') + idx = ffi.new('uint32_t *') + mod = lib.ly_ctx_get_module_iter(self._ctx, idx) + while mod: + yield Module(self, mod) + mod = lib.ly_ctx_get_module_iter(self._ctx, idx) + + +#------------------------------------------------------------------------------ +LOG_LEVELS = { + lib.LY_LLERR: logging.ERROR, + lib.LY_LLWRN: logging.WARNING, + lib.LY_LLVRB: logging.INFO, + lib.LY_LLDBG: logging.DEBUG, +} + + +@ffi.def_extern(name='lypy_log_cb') +def libyang_c_logging_callback(level, msg, path): + args = [c2str(msg)] + if path: + fmt = '%s: %s' + args.append(c2str(path)) + else: + fmt = '%s' + LOG.log(LOG_LEVELS.get(level, logging.NOTSET), fmt, *args) + + +def configure_logging(enable_py_logger, level=logging.ERROR): + """ + Configure libyang logging behaviour. + + :arg bool enable_py_logger: + If False, configure libyang to store the errors in the context until + they are consumed when Context.error() is called. This is the default + behaviour. + + If True, libyang log messages will be sent to the python 'libyang' + logger and will be processed according to the python logging + configuration. Note that by default, the 'libyang' python logger is + created with a NullHandler() which means that all messages are lost + until another handler is configured for that logger. + :arg int level: + Python logging level. By default only ERROR messages are stored/logged. + """ + for ly_lvl, py_lvl in LOG_LEVELS.items(): + if py_lvl == level: + lib.ly_verb(ly_lvl) + break + if enable_py_logger: + lib.ly_log_options(lib.LY_LOLOG |lib.LY_LOSTORE) + lib.ly_set_log_clb(lib.lypy_log_cb, True) + else: + lib.ly_log_options(lib.LY_LOSTORE) + lib.ly_set_log_clb(ffi.NULL, False) + + +configure_logging(False, logging.ERROR) + + +#------------------------------------------------------------------------------ +def lib_dirs(): + dirs = [] + if os.path.isdir(LIBDIR): + dirs.append(LIBDIR) + return dirs + + +#------------------------------------------------------------------------------ +def include_dirs(): + dirs = [] + if os.path.isdir(INCLUDEDIR): + dirs.append(INCLUDEDIR) + return dirs diff --git a/python/libyang/data.py b/python/libyang/data.py new file mode 100644 index 000000000..7b2273b1c --- /dev/null +++ b/python/libyang/data.py @@ -0,0 +1,479 @@ +# Copyright (c) 2020 6WIND S.A. +# SPDX-License-Identifier: BSD-3-Clause + +from _libyang import ffi +from _libyang import lib + +from .schema import Module +from .schema import SContainer +from .schema import SLeaf +from .schema import SLeafList +from .schema import SList +from .schema import SNode +from .schema import SRpc +from .schema import Type +from .util import c2str +from .util import str2c + + +#------------------------------------------------------------------------------ +def printer_flags(with_siblings=False, pretty=False, keep_empty_containers=False, + trim_default_values=False, include_implicit_defaults=False): + flags = 0 + if with_siblings: + flags |= lib.LYP_WITHSIBLINGS + if pretty: + flags |= lib.LYP_FORMAT + if keep_empty_containers: + flags |= lib.LYP_KEEPEMPTYCONT + if trim_default_values: + flags |= lib.LYP_WD_TRIM + if include_implicit_defaults: + flags |= lib.LYP_WD_ALL + return flags + + +#------------------------------------------------------------------------------ +def data_format(fmt_string): + if fmt_string == 'json': + return lib.LYD_JSON + if fmt_string == 'xml': + return lib.LYD_XML + if fmt_string == 'lyb': + return lib.LYD_LYB + raise ValueError('unknown data format: %r' % fmt_string) + + +#------------------------------------------------------------------------------ +def path_flags(update=False, rpc_output=False, no_parent_ret=False): + flags = 0 + if update: + flags |= lib.LYD_PATH_OPT_UPDATE + if rpc_output: + flags |= lib.LYD_PATH_OPT_OUTPUT + if no_parent_ret: + flags |= lib.LYD_PATH_OPT_NOPARENTRET + return flags + + +#------------------------------------------------------------------------------ +def parser_flags(data=False, config=False, strict=False, trusted=False, + no_yanglib=False, rpc=False): + flags = 0 + if data: + flags |= lib.LYD_OPT_DATA + if config: + flags |= lib.LYD_OPT_CONFIG + if strict: + flags |= lib.LYD_OPT_STRICT + if trusted: + flags |= lib.LYD_OPT_TRUSTED + if no_yanglib: + flags |= lib.LYD_OPT_DATA_NO_YANGLIB + if rpc: + flags |= lib.LYD_OPT_RPC + return flags + + +#------------------------------------------------------------------------------ +class DNode(object): + """ + Data tree node. + """ + def __init__(self, context, node_p): + """ + :arg Context context: + The libyang.Context python object. + :arg struct lyd_node * node_p: + The pointer to the C structure allocated by libyang.so. + """ + self.context = context + self._node = ffi.cast('struct lyd_node *', node_p) + + def name(self): + return c2str(self._node.schema.name) + + def module(self): + mod = lib.lyd_node_module(self._node) + if not mod: + raise self.context.error('cannot get module') + return Module(self.context, mod) + + def schema(self): + return SNode.new(self.context, self._node.schema) + + def parent(self): + if not self._node.parent: + return None + return self.new(self.context, self._node.parent) + + def root(self): + node = self + while node.parent() is not None: + node = node.parent() + return node + + def first_sibling(self): + n = lib.lyd_first_sibling(self._node) + if n == self._node: + return self + return self.new(self.context, n) + + def siblings(self, include_self=True): + n = lib.lyd_first_sibling(self._node) + while n: + if n == self._node: + if include_self: + yield self + else: + yield self.new(self.context, n) + n = n.next + + def find_one(self, xpath): + try: + return next(self.find_all(xpath)) + except StopIteration: + return None + + def find_all(self, xpath): + node_set = lib.lyd_find_path(self._node, str2c(xpath)) + if not node_set: + raise self.context.error('cannot find path') + try: + for i in range(node_set.number): + yield DNode.new(self.context, node_set.d[i]) + finally: + lib.ly_set_free(node_set) + + def path(self): + path = lib.lyd_path(self._node) + try: + return c2str(path) + finally: + lib.free(path) + + def validate(self, data=False, config=False, strict=False, trusted=False, + no_yanglib=False): + flags = parser_flags( + data=data, config=config, strict=strict, trusted=trusted, + no_yanglib=no_yanglib) + node_p = ffi.new('struct lyd_node **') + node_p[0] = self._node + ret = lib.lyd_validate(node_p, flags, ffi.NULL) + if ret != 0: + self.context.error('validation failed') + + def print_mem(self, fmt, + with_siblings=False, + pretty=False, + include_implicit_defaults=False, + trim_default_values=False, + keep_empty_containers=False): + flags = printer_flags( + with_siblings=with_siblings, pretty=pretty, + include_implicit_defaults=include_implicit_defaults, + trim_default_values=trim_default_values, + keep_empty_containers=keep_empty_containers) + buf = ffi.new('char **') + fmt = data_format(fmt) + ret = lib.lyd_print_mem(buf, self._node, fmt, flags) + if ret != 0: + raise self.context.error('cannot print node') + try: + if fmt == lib.LYD_LYB: + # binary format, do not convert to unicode + return c2str(buf[0], decode=False) + return c2str(buf[0], decode=True) + finally: + lib.free(buf[0]) + + def print_file(self, fileobj, fmt, + with_siblings=False, + pretty=False, + include_implicit_defaults=False, + trim_default_values=False, + keep_empty_containers=False): + flags = printer_flags( + with_siblings=with_siblings, pretty=pretty, + include_implicit_defaults=include_implicit_defaults, + trim_default_values=trim_default_values, + keep_empty_containers=keep_empty_containers) + fmt = data_format(fmt) + ret = lib.lyd_print_fd(fileobj.fileno(), self._node, fmt, flags) + if ret != 0: + raise self.context.error('cannot print node') + + def print_dict(self, strip_prefixes=True, absolute=True, + with_siblings=False, include_implicit_defaults=False, + trim_default_values=False, keep_empty_containers=False): + """ + Convert a DNode object to a python dictionary. + + :arg DNode dnode: + The data node to convert. + :arg bool strip_prefixes: + If True (the default), module prefixes are stripped from dictionary + keys. If False, dictionary keys are in the form ``:``. + :arg bool absolute: + If True (the default), always return a dictionary containing the + complete tree starting from the root. + :arg bool with_siblings: + If True, include the node's siblings. + :arg bool include_implicit_defaults: + Include implicit default nodes. + :arg bool trim_default_values: + Exclude nodes with the value equal to their default value. + :arg bool keep_empty_containers: + Preserve empty non-presence containers. + """ + flags = printer_flags( + include_implicit_defaults=include_implicit_defaults, + trim_default_values=trim_default_values, + keep_empty_containers=keep_empty_containers) + + def _to_dict(node, parent_dic): + if not lib.lyd_node_should_print(node._node, flags): + return + if strip_prefixes: + name = node.name() + else: + name = '%s:%s' % (node.module().name(), node.name()) + if isinstance(node, DList): + list_element = {} + for child in node: + _to_dict(child, list_element) + parent_dic.setdefault(name, []).append(list_element) + elif isinstance(node, (DContainer, DRpc)): + container = {} + for child in node: + _to_dict(child, container) + parent_dic[name] = container + elif isinstance(node, DLeafList): + parent_dic.setdefault(name, []).append(node.value()) + elif isinstance(node, DLeaf): + parent_dic[name] = node.value() + + dic = {} + dnode = self + if absolute: + dnode = dnode.root() + if with_siblings: + for sib in dnode.siblings(): + _to_dict(sib, dic) + else: + _to_dict(dnode, dic) + return dic + + def free(self, with_siblings=True): + try: + if with_siblings: + lib.lyd_free_withsiblings(self._node) + else: + lib.lyd_free(self._node) + finally: + self._node = None + + def __repr__(self): + cls = self.__class__ + return '<%s.%s: %s>' % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + NODETYPE_CLASS = {} + + @classmethod + def register(cls, *nodetypes): + def _decorator(nodeclass): + for t in nodetypes: + cls.NODETYPE_CLASS[t] = nodeclass + return nodeclass + return _decorator + + @classmethod + def new(cls, context, node_p): + node_p = ffi.cast('struct lyd_node *', node_p) + nodecls = cls.NODETYPE_CLASS.get(node_p.schema.nodetype, DNode) + return nodecls(context, node_p) + + +#------------------------------------------------------------------------------ +@DNode.register(SNode.CONTAINER) +class DContainer(DNode): + + def create_path(self, path, value=None, rpc_output=False): + return self.context.create_data_path( + path, parent=self, value=value, rpc_output=rpc_output) + + def children(self): + child = self._node.child + while child: + yield DNode.new(self.context, child) + child = child.next + + def __iter__(self): + return self.children() + + +#------------------------------------------------------------------------------ +@DNode.register(SNode.RPC) +class DRpc(DContainer): + pass + + +#------------------------------------------------------------------------------ +@DNode.register(SNode.LIST) +class DList(DContainer): + pass + + +#------------------------------------------------------------------------------ +@DNode.register(SNode.LEAF) +class DLeaf(DNode): + + def __init__(self, context, node_p): + DNode.__init__(self, context, node_p) + self._leaf = ffi.cast('struct lyd_node_leaf_list *', node_p) + + def value(self): + if self._leaf.value_type == Type.EMPTY: + return None + if self._leaf.value_type in Type.NUM_TYPES: + return int(c2str(self._leaf.value_str)) + if self._leaf.value_type in ( + Type.STRING, Type.BINARY, Type.ENUM, Type.IDENT, Type.BITS): + return c2str(self._leaf.value_str) + if self._leaf.value_type == Type.DEC64: + return lib.lyd_dec64_to_double(self._node) + if self._leaf.value_type == Type.LEAFREF: + referenced = DNode.new(self.context, self._leaf.value.leafref) + return referenced.value() + if self._leaf.value_type == Type.BOOL: + return bool(self._leaf.value.bln) + return None + + +#------------------------------------------------------------------------------ +@DNode.register(SNode.LEAFLIST) +class DLeafList(DLeaf): + pass + + +#------------------------------------------------------------------------------ +def dict_to_dnode(dic, schema, parent=None, rpc_input=False, rpc_output=False): + """ + Convert a python dictionary to a DNode object given a YANG schema object. + The returned value is always a top-level data node (i.e.: without parent). + + :arg dict dic: + The python dictionary to convert. + :arg SNode or Module schema: + The libyang schema object associated with the dictionary. It must be at + the same "level" than the dictionary (i.e.: the dictionary must have a + key that matches the name of the SNode. In the case schema is a + Module, dic should have keys that are names of root nodes of the + module). + :arg DNode parent: + Optional parent to update. If not specified a new top-level DNode will + be created. + :arg bool rpc_input: + If True, expect schema to be a SRpc object and dic will be parsed + by looking in the rpc input nodes. + :arg bool rpc_output: + If True, expect schema to be a SRpc object and dic will be parsed + by looking in the rpc output nodes. + """ + if not dic: + return parent + + if not isinstance(dic, dict): + raise TypeError('dic argument must be a python dict') + + created = [] + + def _create(_schema, key, value=None): + nonlocal parent + dnode = _schema.context.create_data_path( + _schema.data_path() % key, parent=parent, value=value, + update=False, no_parent_ret=False, + force_return_value=False, rpc_output=rpc_output) + if dnode is not None: + created.append(dnode) + if parent is None: + parent = dnode + + def _to_dnode(_dic, _schema, key=()): + name = _schema.name() + if name not in _dic: + name = _schema.fullname() + if name not in _dic: + return + data = _dic[name] + if isinstance(_schema, SContainer): + if not isinstance(data, dict): + raise TypeError('%s: python value is not a dict: %r' + % (_schema.schema_path(), data)) + _create(_schema, key) + for s in _schema: + _to_dnode(data, s, key) + elif isinstance(_schema, SRpc): + if rpc_input: + _schema = _schema.input() + elif rpc_output: + _schema = _schema.output() + else: + raise ValueError('rpc_input or rpc_output must be specified') + if not _schema: + # there may not be any input or any output node in the rpc + return + for s in _schema: + _to_dnode(data, s, key) + elif isinstance(_schema, SList): + if not isinstance(data, (list, tuple)): + raise TypeError('%s: python value is not a list/tuple: %r' + % (_schema.schema_path(), data)) + for element in data: + if not isinstance(element, dict): + raise TypeError('%s: list element is not a dict: %r' + % (_schema.schema_path(), element)) + try: + next_key = [] + for k in _schema.keys(): + try: + next_key.append(element[k.name()]) + except KeyError as _e: + try: + next_key.append(element[k.fullname()]) + except KeyError: + raise _e + except KeyError as e: + raise KeyError( + "%s: key '%s' not present in list element: %r" + % (_schema.schema_path(), e, element)) + for s in _schema: + _to_dnode(element, s, key + tuple(next_key)) + elif isinstance(_schema, SLeafList): + if not isinstance(data, (list, tuple)): + raise TypeError('%s: python value is not a list/tuple: %r' + % (_schema.schema_path(), data)) + for element in data: + _create(_schema, key, element) + elif isinstance(_schema, SLeaf): + _create(_schema, key, data) + + try: + if isinstance(schema, Module): + for s in schema: + _to_dnode(dic, s) + else: + _to_dnode(dic, schema) + except: + for c in reversed(created): + c.free(with_siblings=False) + raise + + if parent is not None: + # go back to the root of the created tree + parent = parent.root() + + return parent diff --git a/python/libyang/diff.py b/python/libyang/diff.py new file mode 100644 index 000000000..8c9f2fdcd --- /dev/null +++ b/python/libyang/diff.py @@ -0,0 +1,302 @@ +# Copyright (c) 2019 6WIND +# SPDX-License-Identifier: BSD-3-Clause + +from .schema import SContainer +from .schema import SLeaf +from .schema import SLeafList +from .schema import SList +from .schema import SRpc +from .schema import SRpcInOut + + +#------------------------------------------------------------------------------ +def schema_diff(ctx_old, ctx_new, exclude_node_cb=None): + """ + Compare two libyang :cls:`.Context`\s, for a given set of paths and return + all differences. + + :arg Context ctx_old: + The first context. + :arg Context ctx_new: + The second context. + :arg exclude_node_cb: + Optionnal user callback that will be called with each node that is + found in each context. If the callback returns a "trueish" value, the + node will be excluded from the diff (as well as all its children). + + :return: + An iterator that yield :cls:`.SNodeDiff` objects. + """ + if exclude_node_cb is None: + exclude_node_cb = lambda n: False + + def flatten(node, dic): + """ + Flatten a node and all its children into a dict (indexed by their + schema xpath). This function is recursive. + """ + if exclude_node_cb(node): + return + dic[node.schema_path()] = node + if isinstance(node, (SContainer, SList, SRpc, SRpcInOut)): + for child in node: + flatten(child, dic) + + old_dict = {} + new_dict = {} + + for module in ctx_old: + for node in module: + flatten(node, old_dict) + for module in ctx_new: + for node in module: + flatten(node, new_dict) + + diffs = {} + old_paths = frozenset(old_dict.keys()) + new_paths = frozenset(new_dict.keys()) + for path in old_paths - new_paths: + diffs[path] = [SNodeRemoved(old_dict[path])] + for path in new_paths - old_paths: + diffs[path] = [SNodeAdded(new_dict[path])] + for path in old_paths & new_paths: + old = old_dict[path] + new = new_dict[path] + diffs[path] = snode_changes(old, new) + + for path in sorted(diffs.keys()): + yield from diffs[path] + + +#------------------------------------------------------------------------------ +class SNodeDiff: + pass + + +#------------------------------------------------------------------------------ +class SNodeRemoved(SNodeDiff): + + def __init__(self, node): + self.node = node + + def __str__(self): + return '-%s: removed status=%s %s' % ( + self.node.schema_path(), self.node.status(), self.node.keyword()) + + +#------------------------------------------------------------------------------ +class SNodeAdded(SNodeDiff): + + def __init__(self, node): + self.node = node + + def __str__(self): + return '+%s: added %s' % (self.node.schema_path(), self.node.keyword()) + + +#------------------------------------------------------------------------------ +class SNodeAttributeChanged(SNodeDiff): + + def __init__(self, old, new, value=None): + self.old = old + self.new = new + self.value = value + + def __str__(self): + if self.__class__.__name__.endswith('Added'): + sign = '+' + else: + sign = '-' + s = '%s%s: %s' % (sign, self.new.schema_path(), self.__class__.__name__) + if self.value is not None: + str_val = str(self.value).replace('"', '\\"').replace('\n', '\\n') + s += ' "%s"' % str_val + return s + + +#------------------------------------------------------------------------------ +class BaseTypeRemoved(SNodeAttributeChanged): pass +class BaseTypeAdded(SNodeAttributeChanged): pass +class BitRemoved(SNodeAttributeChanged): pass +class BitAdded(SNodeAttributeChanged): pass +class ConfigFalseRemoved(SNodeAttributeChanged): pass +class ConfigFalseAdded(SNodeAttributeChanged): pass +class DefaultRemoved(SNodeAttributeChanged): pass +class DefaultAdded(SNodeAttributeChanged): pass +class DescriptionRemoved(SNodeAttributeChanged): pass +class DescriptionAdded(SNodeAttributeChanged): pass +class EnumRemoved(SNodeAttributeChanged): pass +class EnumAdded(SNodeAttributeChanged): pass +class ExtensionRemoved(SNodeAttributeChanged): pass +class ExtensionAdded(SNodeAttributeChanged): pass +class KeyRemoved(SNodeAttributeChanged): pass +class KeyAdded(SNodeAttributeChanged): pass +class MandatoryRemoved(SNodeAttributeChanged): pass +class MandatoryAdded(SNodeAttributeChanged): pass +class MustRemoved(SNodeAttributeChanged): pass +class MustAdded(SNodeAttributeChanged): pass +class NodeTypeAdded(SNodeAttributeChanged): pass +class NodeTypeRemoved(SNodeAttributeChanged): pass +class RangeRemoved(SNodeAttributeChanged): pass +class RangeAdded(SNodeAttributeChanged): pass +class OrderedByUserRemoved(SNodeAttributeChanged): pass +class OrderedByUserAdded(SNodeAttributeChanged): pass +class PresenceRemoved(SNodeAttributeChanged): pass +class PresenceAdded(SNodeAttributeChanged): pass +class StatusRemoved(SNodeAttributeChanged): pass +class StatusAdded(SNodeAttributeChanged): pass +class LengthRemoved(SNodeAttributeChanged): pass +class LengthAdded(SNodeAttributeChanged): pass +class PatternRemoved(SNodeAttributeChanged): pass +class PatternAdded(SNodeAttributeChanged): pass +class UnitsRemoved(SNodeAttributeChanged): pass +class UnitsAdded(SNodeAttributeChanged): pass # noqa: E302, E701 + + +#------------------------------------------------------------------------------ +def snode_changes(old, new): + + if old.nodetype() != new.nodetype(): + yield NodeTypeRemoved(old, new, old.keyword()) + yield NodeTypeAdded(old, new, new.keyword()) + + if old.description() != new.description(): + if old.description() is not None: + yield DescriptionRemoved(old, new, old.description()) + if new.description() is not None: + yield DescriptionAdded(old, new, new.description()) + + if old.mandatory() and not new.mandatory(): + yield MandatoryRemoved(old, new) + elif not old.mandatory() and new.mandatory(): + yield MandatoryAdded(old, new) + + if old.status() != new.status(): + yield StatusRemoved(old, new, old.status()) + yield StatusAdded(old, new, new.status()) + + if old.config_false() and not new.config_false(): + yield ConfigFalseRemoved(old, new) + elif not old.config_false() and new.config_false(): + yield ConfigFalseAdded(old, new) + + old_musts = frozenset(old.must_conditions()) + new_musts = frozenset(new.must_conditions()) + for m in old_musts - new_musts: + yield MustRemoved(old, new, m) + for m in new_musts - old_musts: + yield MustAdded(old, new, m) + + old_exts = {(e.module().name(), e.name()): e for e in old.extensions()} + new_exts = {(e.module().name(), e.name()): e for e in new.extensions()} + old_ext_keys = frozenset(old_exts.keys()) + new_ext_keys = frozenset(new_exts.keys()) + + for k in old_ext_keys - new_ext_keys: + yield ExtensionRemoved(old, new, old_exts[k]) + for k in new_ext_keys - old_ext_keys: + yield ExtensionAdded(old, new, new_exts[k]) + for k in old_ext_keys & new_ext_keys: + if old_exts[k].argument() != new_exts[k].argument(): + yield ExtensionRemoved(old, new, old_exts[k]) + yield ExtensionAdded(old, new, new_exts[k]) + + if (isinstance(old, SLeaf) and isinstance(new, SLeaf)) or \ + (isinstance(old, SLeafList) and isinstance(new, SLeafList)): + + old_bases = set(old.type().basenames()) + new_bases = set(new.type().basenames()) + for b in old_bases - new_bases: + yield BaseTypeRemoved(old, new, b) + for b in new_bases - old_bases: + yield BaseTypeAdded(old, new, b) + + if old.units() != new.units(): + if old.units() is not None: + yield UnitsRemoved(old, new, old.units()) + if new.units() is not None: + yield UnitsAdded(old, new, new.units()) + + old_lengths = set(old.type().all_lengths()) + new_lengths = set(new.type().all_lengths()) + for l in old_lengths - new_lengths: + yield LengthRemoved(old, new, l) + for l in new_lengths - old_lengths: + yield LengthAdded(old, new, l) + + # Multiple "pattern" statements on a single type are ANDed together + # (i.e. they must all match). However, when a leaf type is an union of + # multiple string typedefs with individual "pattern" statements, the + # patterns are ORed together (i.e. one of them must match). + # + # This is not handled properly here as we flatten all patterns in a + # single set and consider there is a difference if we remove/add one of + # them. + # + # The difference does not hold any information about which union branch + # it relates to. This is way too complex. + old_patterns = set(old.type().all_patterns()) + new_patterns = set(new.type().all_patterns()) + for p in old_patterns - new_patterns: + yield PatternRemoved(old, new, p) + for p in new_patterns - old_patterns: + yield PatternAdded(old, new, p) + + old_ranges = set(old.type().all_ranges()) + new_ranges = set(new.type().all_ranges()) + for r in old_ranges - new_ranges: + yield RangeRemoved(old, new, r) + for r in new_ranges - old_ranges: + yield RangeAdded(old, new, r) + + old_enums = set(e for e, _ in old.type().all_enums()) + new_enums = set(e for e, _ in new.type().all_enums()) + for e in old_enums - new_enums: + yield EnumRemoved(old, new, e) + for e in new_enums - old_enums: + yield EnumAdded(old, new, e) + + old_bits = set(b for b, _ in old.type().all_bits()) + new_bits = set(b for b, _ in new.type().all_bits()) + for b in old_bits - new_bits: + yield BitRemoved(old, new, b) + for b in new_bits - old_bits: + yield BitAdded(old, new, b) + + if isinstance(old, SLeaf) and isinstance(new, SLeaf): + if old.default() != new.default(): + if old.default() is not None: + yield DefaultRemoved(old, new, old.default()) + if new.default() is not None: + yield DefaultAdded(old, new, new.default()) + + elif isinstance(old, SLeafList) and isinstance(new, SLeafList): + old_defaults = frozenset(old.defaults()) + new_defaults = frozenset(new.defaults()) + for d in old_defaults - new_defaults: + yield DefaultRemoved(old, new, d) + for d in new_defaults - old_defaults: + yield DefaultAdded(old, new, d) + if old.ordered() and not new.ordered(): + yield OrderedByUserRemoved(old, new) + elif not old.ordered() and new.ordered(): + yield OrderedByUserAdded(old, new) + + elif isinstance(old, SContainer) and isinstance(new, SContainer): + if old.presence() != new.presence(): + if old.presence() is not None: + yield PresenceRemoved(old, new, old.presence()) + if new.presence() is not None: + yield PresenceAdded(old, new, new.presence()) + + elif isinstance(old, SList) and isinstance(new, SList): + old_keys = frozenset(k.name() for k in old.keys()) + new_keys = frozenset(k.name() for k in new.keys()) + for k in old_keys - new_keys: + yield KeyRemoved(old, new, k) + for k in new_keys - old_keys: + yield KeyAdded(old, new, k) + if old.ordered() and not new.ordered(): + yield OrderedByUserRemoved(old, new) + elif not old.ordered() and new.ordered(): + yield OrderedByUserAdded(old, new) diff --git a/python/libyang/schema.py b/python/libyang/schema.py new file mode 100644 index 000000000..80663a1e5 --- /dev/null +++ b/python/libyang/schema.py @@ -0,0 +1,957 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +from _libyang import ffi +from _libyang import lib + +from .util import c2str +from .util import str2c + + +#------------------------------------------------------------------------------ +def schema_in_format(fmt_string): + if fmt_string == 'yang': + return lib.LYS_IN_YANG + if fmt_string == 'yin': + return lib.LYS_IN_YIN + raise ValueError('unknown schema input format: %r' % fmt_string) + + +#------------------------------------------------------------------------------ +def schema_out_format(fmt_string): + if fmt_string == 'yang': + return lib.LYS_OUT_YANG + if fmt_string == 'yin': + return lib.LYS_OUT_YIN + if fmt_string == 'tree': + return lib.LYS_OUT_TREE + if fmt_string == 'info': + return lib.LYS_OUT_INFO + if fmt_string == 'json': + return lib.LYS_OUT_JSON + raise ValueError('unknown schema output format: %r' % fmt_string) + + +#------------------------------------------------------------------------------ +class Module(object): + + def __init__(self, context, module_p): + self.context = context + self._module = module_p + + def name(self): + return c2str(self._module.name) + + def prefix(self): + return c2str(self._module.prefix) + + def description(self): + return c2str(self._module.dsc) + + def filepath(self): + return c2str(self._module.filepath) + + def implemented(self): + return bool(lib.lypy_module_implemented(self._module)) + + def feature_enable(self, name): + ret = lib.lys_features_enable(self._module, str2c(name)) + if ret != 0: + raise self.context.error('no such feature: %r' % name) + + def feature_enable_all(self): + self.feature_enable('*') + + def feature_disable(self, name): + ret = lib.lys_features_disable(self._module, str2c(name)) + if ret != 0: + raise self.context.error('no such feature: %r' % name) + + def feature_disable_all(self): + self.feature_disable('*') + + def feature_state(self, name): + ret = lib.lys_features_state(self._module, str2c(name)) + if ret < 0: + raise self.context.error('no such feature: %r' % name) + return bool(ret) + + def features(self): + for i in range(self._module.features_size): + yield Feature(self.context, self._module.features[i]) + + def get_feature(self, name): + for f in self.features(): + if f.name() == name: + return f + raise self.context.error('no such feature: %r' % name) + + def revisions(self): + for i in range(self._module.rev_size): + yield Revision(self.context, self._module.rev[i]) + + def __iter__(self): + return self.children() + + def children(self, types=None): + return iter_children(self.context, self._module, types=types) + + def __str__(self): + return self.name() + + def print_mem(self, fmt='tree', path=None): + fmt = schema_out_format(fmt) + buf = ffi.new('char **') + ret = lib.lys_print_mem(buf, self._module, fmt, str2c(path), 0, 0) + if ret != 0: + raise self.context.error('cannot print module') + try: + return c2str(buf[0]) + finally: + lib.free(buf[0]) + + def print_file(self, fileobj, fmt='tree', path=None): + fmt = schema_out_format(fmt) + ret = lib.lys_print_fd( + fileobj.fileno(), self._module, fmt, str2c(path), 0, 0) + if ret != 0: + raise self.context.error('cannot print module') + + def parse_data_dict(self, dic, parent=None, + rpc_input=False, rpc_output=False): + """ + Convert a python dictionary to a DNode object following the schema of + this module. The returned value is always a top-level data node (i.e.: + without parent). + + :arg dict dic: + The python dictionary to convert. + :arg DNode parent: + Optional parent to update. If not specified a new top-level DNode + will be created. + :arg bool rpc_input: + If True, dic will be parsed by looking in the rpc input nodes. + :arg bool rpc_output: + If True, dic will be parsed by looking in the rpc output nodes. + """ + from .data import dict_to_dnode # circular import + return dict_to_dnode(dic, self, parent=parent, + rpc_input=rpc_input, rpc_output=rpc_output) + + +#------------------------------------------------------------------------------ +class Revision(object): + + def __init__(self, context, rev_p): + self.context = context + self._rev = rev_p + + def date(self): + return c2str(self._rev.date) + + def description(self): + return c2str(self._rev.dsc) + + def reference(self): + return c2str(self._rev.ref) + + def extensions(self): + for i in range(self._rev.ext_size): + yield Extension(self.context, self._rev.ext[i]) + + def get_extension(self, name, prefix=None, arg_value=None): + for ext in self.extensions(): + if ext.name() != name: + continue + if prefix is not None and ext.module().name() != prefix: + continue + if arg_value is not None and ext.argument() != arg_value: + continue + return ext + return None + + def __repr__(self): + cls = self.__class__ + return '<%s.%s: %s>' % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.date() + + +#------------------------------------------------------------------------------ +class Extension(object): + + def __init__(self, context, ext_p): + self.context = context + self._ext = ext_p + self._def = getattr(ext_p, 'def') + + def name(self): + return c2str(self._def.name) + + def argument(self): + return c2str(self._ext.arg_value) + + def module(self): + module_p = lib.lys_main_module(self._def.module) + if not module_p: + raise self.context.error('cannot get module') + return Module(self.context, module_p) + + def __repr__(self): + cls = self.__class__ + return '<%s.%s: %s>' % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + +#------------------------------------------------------------------------------ +class Type(object): + + DER = lib.LY_TYPE_DER + BINARY = lib.LY_TYPE_BINARY + BITS = lib.LY_TYPE_BITS + BOOL = lib.LY_TYPE_BOOL + DEC64 = lib.LY_TYPE_DEC64 + EMPTY = lib.LY_TYPE_EMPTY + ENUM = lib.LY_TYPE_ENUM + IDENT = lib.LY_TYPE_IDENT + INST = lib.LY_TYPE_INST + LEAFREF = lib.LY_TYPE_LEAFREF + STRING = lib.LY_TYPE_STRING + UNION = lib.LY_TYPE_UNION + INT8 = lib.LY_TYPE_INT8 + UINT8 = lib.LY_TYPE_UINT8 + INT16 = lib.LY_TYPE_INT16 + UINT16 = lib.LY_TYPE_UINT16 + INT32 = lib.LY_TYPE_INT32 + UINT32 = lib.LY_TYPE_UINT32 + INT64 = lib.LY_TYPE_INT64 + UINT64 = lib.LY_TYPE_UINT64 + BASENAMES = { + DER: 'derived', + BINARY: 'binary', + BITS: 'bits', + BOOL: 'boolean', + DEC64: 'decimal64', + EMPTY: 'empty', + ENUM: 'enumeration', + IDENT: 'identityref', + INST: 'instance-id', + LEAFREF: 'leafref', + STRING: 'string', + UNION: 'union', + INT8: 'int8', + UINT8: 'uint8', + INT16: 'int16', + UINT16: 'uint16', + INT32: 'int32', + UINT32: 'uint32', + INT64: 'int64', + UINT64: 'uint64', + } + + def __init__(self, context, type_p): + self.context = context + self._type = type_p + + def get_bases(self): + if self._type.base == lib.LY_TYPE_DER: + yield from self.derived_type().get_bases() + elif self._type.base == lib.LY_TYPE_LEAFREF: + yield from self.leafref_type().get_bases() + elif self._type.base == lib.LY_TYPE_UNION: + for t in self.union_types(): + yield from t.get_bases() + else: # builtin type + yield self + + def name(self): + if self._type.der: + return c2str(self._type.der.name) + return self.basename() + + def description(self): + if self._type.der: + return c2str(self._type.der.dsc) + return None + + def base(self): + return self._type.base + + def bases(self): + for b in self.get_bases(): + yield b.base() + + def basename(self): + return self.BASENAMES.get(self._type.base, 'unknown') + + def basenames(self): + for b in self.get_bases(): + yield b.basename() + + def derived_type(self): + if not self._type.der: + return None + return Type(self.context, ffi.addressof(self._type.der.type)) + + def leafref_type(self): + if self._type.base != self.LEAFREF: + return None + lref = self._type.info.lref + return Type(self.context, ffi.addressof(lref.target.type)) + + def union_types(self): + if self._type.base != self.UNION: + return + t = self._type + while t.info.uni.count == 0: + t = ffi.addressof(t.der.type) + for i in range(t.info.uni.count): + yield Type(self.context, t.info.uni.types[i]) + + def enums(self): + if self._type.base != self.ENUM: + return + t = self._type + while t.info.enums.count == 0: + t = ffi.addressof(t.der.type) + for i in range(t.info.enums.count): + e = t.info.enums.enm[i] + yield c2str(e.name), c2str(e.dsc) + + def all_enums(self): + for b in self.get_bases(): + yield from b.enums() + + def bits(self): + if self._type.base != self.BITS: + return + t = self._type + while t.info.bits.count == 0: + t = ffi.addressof(t.der.type) + for i in range(t.info.bits.count): + b = t.info.bits.bit[i] + yield c2str(b.name), c2str(b.dsc) + + def all_bits(self): + for b in self.get_bases(): + yield from b.bits() + + NUM_TYPES = frozenset( + (INT8, INT16, INT32, INT64, UINT8, UINT16, UINT32, UINT64)) + + def range(self): + if self._type.base in self.NUM_TYPES and self._type.info.num.range: + return c2str(self._type.info.num.range.expr) + elif self._type.base == self.DEC64 and self._type.info.dec64.range: + return c2str(self._type.info.dec64.range.expr) + elif self._type.der: + return self.derived_type().range() + return None + + def all_ranges(self): + if self._type.base == lib.LY_TYPE_UNION: + for t in self.union_types(): + yield from t.all_ranges() + else: + rng = self.range() + if rng is not None: + yield rng + + def length(self): + if self._type.base == self.STRING and self._type.info.str.length: + return c2str(self._type.info.str.length.expr) + elif self._type.base == self.BINARY and self._type.info.binary.length: + return c2str(self._type.info.binary.length.expr) + elif self._type.der: + return self.derived_type().length() + return None + + def all_lengths(self): + if self._type.base == lib.LY_TYPE_UNION: + for t in self.union_types(): + yield from t.all_lengths() + else: + length = self.length() + if length is not None: + yield length + + def patterns(self): + if self._type.base != self.STRING: + return + for i in range(self._type.info.str.pat_count): + p = self._type.info.str.patterns[i] + if not p: + continue + # in case of pattern restriction, the first byte has a special + # meaning: 0x06 (ACK) for regular match and 0x15 (NACK) for + # invert-match + invert_match = p.expr[0] == 0x15 + # yield tuples like: + # ('[a-zA-Z_][a-zA-Z0-9\-_.]*', False) + # ('[xX][mM][lL].*', True) + yield c2str(p.expr + 1), invert_match + if self._type.der: + yield from self.derived_type().patterns() + + def all_patterns(self): + if self._type.base == lib.LY_TYPE_UNION: + for t in self.union_types(): + yield from t.all_patterns() + else: + yield from self.patterns() + + def module(self): + module_p = lib.lys_main_module(self._type.der.module) + if not module_p: + raise self.context.error('cannot get module') + return Module(self.context, module_p) + + def extensions(self): + for i in range(self._type.ext_size): + yield Extension(self.context, self._type.ext[i]) + if self._type.parent: + for i in range(self._type.parent.ext_size): + yield Extension(self.context, self._type.parent.ext[i]) + + def get_extension(self, name, prefix=None, arg_value=None): + for ext in self.extensions(): + if ext.name() != name: + continue + if prefix is not None and ext.module().name() != prefix: + continue + if arg_value is not None and ext.argument() != arg_value: + continue + return ext + return None + + def __repr__(self): + cls = self.__class__ + return '<%s.%s: %s>' % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + +#------------------------------------------------------------------------------ +class Feature(object): + + def __init__(self, context, feature_p): + self.context = context + self._feature = feature_p + + def name(self): + return c2str(self._feature.name) + + def description(self): + return c2str(self._feature.dsc) + + def reference(self): + return c2str(self._feature.ref) + + def state(self): + return bool(self._feature.flags & lib.LYS_FENABLED) + + def deprecated(self): + return bool(self._feature.flags & lib.LYS_STATUS_DEPRC) + + def obsolete(self): + return bool(self._feature.flags & lib.LYS_STATUS_OBSLT) + + def if_features(self): + for i in range(self._feature.iffeature_size): + yield IfFeatureExpr(self.context, self._feature.iffeature[i]) + + def module(self): + module_p = lib.lys_main_module(self._feature.module) + if not module_p: + raise self.context.error('cannot get module') + return Module(self.context, module_p) + + def __str__(self): + return self.name() + + +#------------------------------------------------------------------------------ +class IfFeatureExpr(object): + + def __init__(self, context, iffeature_p): + self.context = context + self._iffeature = iffeature_p + + def _get_operator(self, position): + # the ->exp field is a 2bit array of operator values stored under + # a uint8_t C array. + mask = 0x3 # 2bits mask + shift = 2 * (position % 4) + item = self._iffeature.expr[position // 4] + result = item & (mask << shift) + return result >> shift + + def _operands(self): + op_index = 0 + ft_index = 0 + expected = 1 + while expected > 0: + operator = self._get_operator(op_index) + op_index += 1 + if operator == lib.LYS_IFF_F: + yield IfFeature(self.context, self._iffeature.features[ft_index]) + ft_index += 1 + expected -= 1 + elif operator == lib.LYS_IFF_NOT: + yield IfNotFeature + elif operator == lib.LYS_IFF_AND: + yield IfAndFeatures + expected += 1 + elif operator == lib.LYS_IFF_OR: + yield IfOrFeatures + expected += 1 + + def tree(self): + def _tree(operands): + op = next(operands) + if op is IfNotFeature: + return op(self.context, _tree(operands)) + elif op in (IfAndFeatures, IfOrFeatures): + return op(self.context, _tree(operands), _tree(operands)) + else: + return op + return _tree(self._operands()) + + def dump(self): + return self.tree().dump() + + def __str__(self): + return str(self.tree()).strip('()') + + +#------------------------------------------------------------------------------ +class IfFeatureExprTree(object): + + def dump(self, indent=0): + raise NotImplementedError() + + def __str__(self): + raise NotImplementedError() + + +#------------------------------------------------------------------------------ +class IfFeature(IfFeatureExprTree): + + def __init__(self, context, feature_p): + self.context = context + self._feature = feature_p + + def feature(self): + return Feature(self.context, self._feature) + + def dump(self, indent=0): + feat = self.feature() + return '%s%s [%s]\n' % (' ' * indent, feat.name(), feat.description()) + + def __str__(self): + return self.feature().name() + + +#------------------------------------------------------------------------------ +class IfNotFeature(IfFeatureExprTree): + + def __init__(self, context, child): + self.context = context + self.child = child + + def dump(self, indent=0): + return ' ' * indent + 'NOT\n' + self.child.dump(indent + 1) + + def __str__(self): + return 'NOT %s' % self.child + + +#------------------------------------------------------------------------------ +class IfAndFeatures(IfFeatureExprTree): + + def __init__(self, context, a, b): + self.context = context + self.a = a + self.b = b + + def dump(self, indent=0): + s = ' ' * indent + 'AND\n' + s += self.a.dump(indent + 1) + s += self.b.dump(indent + 1) + return s + + def __str__(self): + return '%s AND %s' % (self.a, self.b) + + +#------------------------------------------------------------------------------ +class IfOrFeatures(IfFeatureExprTree): + + def __init__(self, context, a, b): + self.context = context + self.a = a + self.b = b + + def dump(self, indent=0): + s = ' ' * indent + 'OR\n' + s += self.a.dump(indent + 1) + s += self.b.dump(indent + 1) + return s + + def __str__(self): + return '(%s OR %s)' % (self.a, self.b) + + +#------------------------------------------------------------------------------ +class SNode(object): + + CONTAINER = lib.LYS_CONTAINER + LEAF = lib.LYS_LEAF + LEAFLIST = lib.LYS_LEAFLIST + LIST = lib.LYS_LIST + RPC = lib.LYS_RPC + INPUT = lib.LYS_INPUT + OUTPUT = lib.LYS_OUTPUT + KEYWORDS = { + CONTAINER: 'container', + LEAF: 'leaf', + LEAFLIST: 'leaf-list', + LIST: 'list', + RPC: 'rpc', + INPUT: 'input', + OUTPUT: 'output', + } + + def __init__(self, context, node_p): + self.context = context + self._node = node_p + + def nodetype(self): + return self._node.nodetype + + def keyword(self): + return self.KEYWORDS.get(self._node.nodetype, '???') + + def name(self): + return c2str(self._node.name) + + def fullname(self): + return '%s:%s' % (self.module().name(), self.name()) + + def description(self): + return c2str(self._node.dsc) + + def config_set(self): + return bool(self._node.flags & lib.LYS_CONFIG_SET) + + def config_false(self): + return bool(self._node.flags & lib.LYS_CONFIG_R) + + def mandatory(self): + return bool(self._node.flags & lib.LYS_MAND_TRUE) + + def deprecated(self): + return bool(self._node.flags & lib.LYS_STATUS_DEPRC) + + def obsolete(self): + return bool(self._node.flags & lib.LYS_STATUS_OBSLT) + + def status(self): + if self._node.flags & lib.LYS_STATUS_DEPRC: + return 'deprecated' + elif self._node.flags & lib.LYS_STATUS_OBSLT: + return 'obsolete' + return 'current' + + def module(self): + module_p = lib.lys_node_module(self._node) + if not module_p: + raise self.context.error('cannot get module') + return Module(self.context, module_p) + + def schema_path(self): + try: + s = lib.lys_path(self._node, 0) + return c2str(s) + finally: + lib.free(s) + + def data_path(self, key_placeholder="'%s'"): + try: + s = lib.lys_data_path_pattern(self._node, str2c(key_placeholder)) + return c2str(s) + finally: + lib.free(s) + + def extensions(self): + for i in range(self._node.ext_size): + yield Extension(self.context, self._node.ext[i]) + + def get_extension(self, name, prefix=None, arg_value=None): + for ext in self.extensions(): + if ext.name() != name: + continue + if prefix is not None and ext.module().name() != prefix: + continue + if arg_value is not None and ext.argument() != arg_value: + continue + return ext + return None + + def if_features(self): + for i in range(self._node.iffeature_size): + yield IfFeatureExpr(self.context, self._node.iffeature[i]) + + def parent(self): + parent_p = lib.lys_parent(self._node) + while parent_p and parent_p.nodetype not in SNode.NODETYPE_CLASS: + parent_p = lib.lys_parent(parent_p) + if parent_p: + return SNode.new(self.context, parent_p) + return None + + def __repr__(self): + cls = self.__class__ + return '<%s.%s: %s>' % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + def parse_data_dict(self, dic, parent=None, + rpc_input=False, rpc_output=False): + """ + Convert a python dictionary to a DNode object following the schema of + this module. The returned value is always a top-level data node (i.e.: + without parent). + + :arg dict dic: + The python dictionary to convert. + :arg DNode parent: + Optional parent to update. If not specified a new top-level DNode + will be created. + :arg bool rpc_input: + If True, dic will be parsed by looking in the rpc input nodes. + :arg bool rpc_output: + If True, dic will be parsed by looking in the rpc output nodes. + """ + from .data import dict_to_dnode # circular import + return dict_to_dnode(dic, self, parent=parent, + rpc_input=rpc_input, rpc_output=rpc_output) + + NODETYPE_CLASS = {} + + @classmethod + def register(cls, nodetype): + def _decorator(nodeclass): + cls.NODETYPE_CLASS[nodetype] = nodeclass + return nodeclass + return _decorator + + @classmethod + def new(cls, context, node_p): + nodecls = cls.NODETYPE_CLASS.get(node_p.nodetype, SNode) + return nodecls(context, node_p) + + +#------------------------------------------------------------------------------ +@SNode.register(SNode.LEAF) +class SLeaf(SNode): + + def __init__(self, context, node_p): + SNode.__init__(self, context, node_p) + self._leaf = ffi.cast('struct lys_node_leaf *', node_p) + + def default(self): + return c2str(self._leaf.dflt) + + def units(self): + return c2str(self._leaf.units) + + def type(self): + return Type(self.context, ffi.addressof(self._leaf.type)) + + def is_key(self): + if lib.lys_is_key(self._leaf, ffi.NULL): + return True + return False + + def must_conditions(self): + for i in range(self._leaf.must_size): + yield c2str(self._leaf.must[i].expr) + + def __str__(self): + return '%s %s' % (self.name(), self.type().name()) + + +#------------------------------------------------------------------------------ +@SNode.register(SNode.LEAFLIST) +class SLeafList(SNode): + + def __init__(self, context, node_p): + SNode.__init__(self, context, node_p) + self._leaflist = ffi.cast('struct lys_node_leaflist *', node_p) + + def ordered(self): + return bool(self._node.flags & lib.LYS_USERORDERED) + + def units(self): + return c2str(self._leaflist.units) + + def type(self): + return Type(self.context, ffi.addressof(self._leaflist.type)) + + def defaults(self): + for i in range(self._leaflist.dflt_size): + yield c2str(self._leaflist.dflt[i]) + + def must_conditions(self): + for i in range(self._leaflist.must_size): + yield c2str(self._leaflist.must[i].expr) + + def __str__(self): + return '%s %s' % (self.name(), self.type().name()) + + +#------------------------------------------------------------------------------ +@SNode.register(SNode.CONTAINER) +class SContainer(SNode): + + def __init__(self, context, node_p): + SNode.__init__(self, context, node_p) + self._container = ffi.cast('struct lys_node_container *', node_p) + + def presence(self): + return c2str(self._container.presence) + + def must_conditions(self): + for i in range(self._container.must_size): + yield c2str(self._container.must[i].expr) + + def __iter__(self): + return self.children() + + def children(self, types=None): + return iter_children(self.context, self._node, types=types) + + +#------------------------------------------------------------------------------ +@SNode.register(SNode.LIST) +class SList(SNode): + + def __init__(self, context, node_p): + SNode.__init__(self, context, node_p) + self._list = ffi.cast('struct lys_node_list *', node_p) + + def ordered(self): + return bool(self._node.flags & lib.LYS_USERORDERED) + + def __iter__(self): + return self.children() + + def children(self, skip_keys=False, types=None): + return iter_children( + self.context, self._node, skip_keys=skip_keys, types=types) + + def keys(self): + for i in range(self._list.keys_size): + node = ffi.cast('struct lys_node *', self._list.keys[i]) + yield SLeaf(self.context, node) + + def must_conditions(self): + for i in range(self._list.must_size): + yield c2str(self._list.must[i].expr) + + def __str__(self): + return '%s [%s]' % ( + self.name(), ', '.join(k.name() for k in self.keys())) + + +#------------------------------------------------------------------------------ +@SNode.register(SNode.INPUT) +@SNode.register(SNode.OUTPUT) +class SRpcInOut(SNode): + + def __iter__(self): + return self.children() + + def must_conditions(self): + return () + + def children(self, types=None): + return iter_children(self.context, self._node, types=types) + + +#------------------------------------------------------------------------------ +@SNode.register(SNode.RPC) +class SRpc(SNode): + + def must_conditions(self): + return () + + def input(self): + try: + return next(iter_children( + self.context, self._node, types=(self.INPUT,), + options=lib.LYS_GETNEXT_WITHINOUT)) + except StopIteration: + return None + + def output(self): + try: + return next(iter_children( + self.context, self._node, types=(self.OUTPUT,), + options=lib.LYS_GETNEXT_WITHINOUT)) + except StopIteration: + return None + + def __iter__(self): + return self.children() + + def children(self, types=None): + return iter_children(self.context, self._node, types=types) + + +#------------------------------------------------------------------------------ +def iter_children(context, parent, skip_keys=False, types=None, options=0): + if types is None: + types = (lib.LYS_CONTAINER, lib.LYS_LIST, lib.LYS_RPC, + lib.LYS_LEAF, lib.LYS_LEAFLIST) + + def _skip(node): + if node.nodetype not in types: + return True + if not skip_keys: + return False + if node.nodetype != lib.LYS_LEAF: + return False + leaf = ffi.cast('struct lys_node_leaf *', node) + if lib.lys_is_key(leaf, ffi.NULL): + return True + return False + + if ffi.typeof(parent) == ffi.typeof('struct lys_module *'): + module = parent + parent = ffi.NULL + else: + module = ffi.NULL + + child = lib.lys_getnext(ffi.NULL, parent, module, options) + while child: + if not _skip(child): + yield SNode.new(context, child) + child = lib.lys_getnext(child, parent, module, options) + + +#------------------------------------------------------------------------------ +# compat +Container = SContainer +Leaf = SLeaf +LeafList = SLeafList +List = SList +Node = SNode +Rpc = SRpc +RpcInOut = SRpcInOut diff --git a/python/libyang/util.py b/python/libyang/util.py new file mode 100644 index 000000000..213d8d5ec --- /dev/null +++ b/python/libyang/util.py @@ -0,0 +1,28 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +from _libyang import ffi + + +#------------------------------------------------------------------------------ +class LibyangError(Exception): + pass + + +#------------------------------------------------------------------------------ +def str2c(s, encode=True): + if s is None: + return ffi.NULL + if hasattr(s, 'encode'): + s = s.encode('utf-8') + return ffi.new('char []', s) + + +#------------------------------------------------------------------------------ +def c2str(c, decode=True): + if c == ffi.NULL: + return None + s = ffi.string(c) + if hasattr(s, 'decode'): + s = s.decode('utf-8') + return s diff --git a/python/pydistutils.cfg.in b/python/pydistutils.cfg.in new file mode 100644 index 000000000..79440e422 --- /dev/null +++ b/python/pydistutils.cfg.in @@ -0,0 +1,13 @@ +[build] +build-base=@CMAKE_BINARY_DIR@/python +[egg_info] +egg-base=@CMAKE_BINARY_DIR@/python +[install] +prefix=@CMAKE_INSTALL_PREFIX@ +install-lib=@PYTHON_MODULE_PATH@ +compile=false +skip-build=true +[sdist] +dist-dir=@CMAKE_BINARY_DIR@/python/dist +[easy_install] +allow_hosts=None diff --git a/python/run_python.sh.in b/python/run_python.sh.in new file mode 100755 index 000000000..6ab68488f --- /dev/null +++ b/python/run_python.sh.in @@ -0,0 +1,10 @@ +#!/bin/sh + +PYTHON_EXECUTABLE="@PYTHON_EXECUTABLE@" +CMAKE_BINARY_DIR="@CMAKE_BINARY_DIR@" + +export HOME="${CMAKE_BINARY_DIR}/python" +export LIBYANG_HEADERS="${CMAKE_BINARY_DIR}/python/include" +export LIBYANG_LIBRARIES="${CMAKE_BINARY_DIR}" + +exec "${PYTHON_EXECUTABLE}" -B "$@" diff --git a/python/setup.cfg b/python/setup.cfg new file mode 100644 index 000000000..bebf363ae --- /dev/null +++ b/python/setup.cfg @@ -0,0 +1,55 @@ +[sdist] +formats = gztar +owner = root +group = root + +[bdist_wheel] +universal = false + +[coverage:run] +omit = */tests/* + +[coverage:report] +skip_covered = False +ignore_errors = True +sort = Cover + + +[flake8] +max_line_length = 100 +# E265 block comment should start with '# ' +# E722 do not use bare except' +# W503 line break before binary operator +ignore = + E265, + E722, + W503, +# E226 missing whitespace around arithmetic operator +# E261 at least two spaces before inline comment +# E3 blank lines +# E713 test for membership should be 'not in' +# Q0 quotes +# C801 Copyright notice not present +select = + E226, + E261, + E3, + E713, + Q0, + C801, +inline-quotes = single +multiline-quotes = single +docstring-quotes = double +copyright-check = True +copyright-min-file-size = 1 +copyright-regexp = Copyright \(c\) \d{4}(-\d{4})?.*\n.*SPDX-License-Identifier: BSD-3-Clause + +[isort] +force_single_line = True +lines_after_imports = 2 +force_sort_within_sections = True +known_third_party = cffi +known_first_party = libyang,_libyang +not_skip = __init__.py +default_section = FIRSTPARTY +no_lines_before = LOCALFOLDER diff --git a/python/setup.py b/python/setup.py new file mode 100755 index 000000000..de83fd542 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018-2020 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +from distutils import dir_util +from distutils import log +from distutils.command.build_clib import build_clib +import os +import re +import subprocess +import sys + +import setuptools +from setuptools.command.build_ext import build_ext +from setuptools.command.sdist import sdist + + +INSTALL_REQS = [] +SETUP_REQS = [] +if '_cffi_backend' not in sys.builtin_module_names: + INSTALL_REQS.append('cffi') + SETUP_REQS.append('cffi') +HERE = os.path.abspath(os.path.dirname(__file__)) + + +class SDist(sdist): + + def make_distribution(self): + base_name = self.distribution.get_fullname() + base_dir = os.path.join(self.dist_dir, base_name) + self.make_release_tree(base_dir, self.filelist.files) + archive_files = [] + if 'tar' in self.formats: + self.formats.append(self.formats.pop(self.formats.index('tar'))) + for fmt in self.formats: + file = self.make_archive(base_dir, fmt, base_dir=base_name, + root_dir=self.dist_dir, + owner=self.owner, group=self.group) + archive_files.append(file) + self.distribution.dist_files.append(('sdist', '', file)) + self.archive_files = archive_files + if not self.keep_temp: + dir_util.remove_tree(base_dir, dry_run=self.dry_run) + + +class BuildCLib(build_clib): + + def run(self): + if not self.libraries: + return + log.info('Building libyang C library ...') + tmp = os.path.abspath(self.build_temp) + staging = os.path.join(tmp, 'staging') + cmd = [ + os.path.join(HERE, 'build-so.sh'), + os.path.join(HERE, 'clib'), + tmp, + staging, + ] + log.info('+ %s' % ' '.join(cmd)) + subprocess.check_call(cmd) + + +class BuildExt(build_ext): + + def run(self): + if self.distribution.has_c_libraries(): + if 'build_clib' not in self.distribution.have_run or \ + not self.distribution.have_run['build_clib']: + self.run_command('build_clib') + tmp = os.path.abspath( + self.get_finalized_command('build_clib').build_temp) + self.include_dirs.append(os.path.join(tmp, 'staging/_include')) + self.library_dirs.append(os.path.join(tmp, 'staging/_lib')) + self.rpath.append('$ORIGIN/libyang/_lib') + + build_ext.run(self) + + if self.distribution.has_c_libraries(): + if self.inplace: + build_py = self.get_finalized_command('build_py') + dest = build_py.get_package_dir('libyang') + else: + dest = os.path.join(self.build_lib, 'libyang') + if os.path.isdir(os.path.join(dest, '_lib')): + # Work around dir_util.copy_tree() that fails when a symlink + # already exists. + for f in os.listdir(os.path.join(dest, '_lib')): + if os.path.islink(os.path.join(dest, '_lib', f)): + os.unlink(os.path.join(dest, '_lib', f)) + tmp = self.get_finalized_command('build_clib').build_temp + dir_util.copy_tree( + os.path.join(tmp, 'staging'), dest, + preserve_symlinks=True, update=True) + + +LIBRARIES = [] +if os.environ.get('LIBYANG_INSTALL', 'system') == 'embed': + LIBRARIES.append(('yang', {'sources': ['clib']})) + + +def _version(): + forced_version = os.getenv('LIBYANG_PYTHON_VERSION') + if forced_version is not None: + return forced_version + + suffix = os.getenv('LIBYANG_PYTHON_VERSION_SUFFIX', '') + + try: + with open('clib/CMakeLists.txt', 'rb') as f: + buf = f.read().decode('utf-8') + flags = re.MULTILINE + major = int(re.search( + r'^set\(LIBYANG_MAJOR_VERSION\s+(\d+)\)$', buf, flags).group(1)) + minor = int(re.search( + r'^set\(LIBYANG_MINOR_VERSION\s+(\d+)\)$', buf, flags).group(1)) + micro = int(re.search( + r'^set\(LIBYANG_MICRO_VERSION\s+(\d+)\)$', buf, flags).group(1)) + return '%d.%d.%d%s' % (major, minor, micro, suffix) + except Exception: + return '0.dev0' + suffix + + +with open('README.md', 'rb') as f: + LONG_DESC = f.read().decode('utf-8') + + +setuptools.setup( + name='libyang', + version=_version(), + description='CFFI bindings to libyang', + long_description=LONG_DESC, + long_description_content_type='text/markdown', + url='https://github.com/CESNET/libyang', + license='BSD 3 clause', + author='Robin Jarry', + author_email='robin@jarry.cc', + keywords=['libyang', 'cffi'], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: Unix', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Topic :: Software Development :: Libraries', + ], + packages=['libyang'], + zip_safe=False, + include_package_data=True, + python_requires='>=3.5', + setup_requires=SETUP_REQS, + install_requires=INSTALL_REQS, + cffi_modules=['cffi/build.py:BUILDER'], + libraries=LIBRARIES, + cmdclass={ + 'build_clib': BuildCLib, + 'build_ext': BuildExt, + 'sdist': SDist, + }, +) diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 000000000..6b4870d11 --- /dev/null +++ b/python/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause diff --git a/python/tests/test_context.py b/python/tests/test_context.py new file mode 100644 index 000000000..0c70e2fb6 --- /dev/null +++ b/python/tests/test_context.py @@ -0,0 +1,81 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +import os +import unittest + +from libyang import Context +from libyang.schema import Module +from libyang.schema import SRpc +from libyang.util import LibyangError + + +YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang') + + +#------------------------------------------------------------------------------ +class ContextTest(unittest.TestCase): + + def test_ctx_no_dir(self): + with Context() as ctx: + self.assertIsNot(ctx, None) + + def test_ctx_dir(self): + with Context(YANG_DIR) as ctx: + self.assertIsNot(ctx, None) + + def test_ctx_invalid_dir(self): + with Context('/does/not/exist') as ctx: + self.assertIsNot(ctx, None) + + def test_ctx_missing_dir(self): + with Context(os.path.join(YANG_DIR, 'yolo')) as ctx: + self.assertIsNot(ctx, None) + with self.assertRaises(LibyangError): + ctx.load_module('yolo-system') + + def test_ctx_env_search_dir(self): + try: + os.environ['YANGPATH'] = ':'.join([ + os.path.join(YANG_DIR, 'omg'), + os.path.join(YANG_DIR, 'wtf'), + ]) + with Context(os.path.join(YANG_DIR, 'yolo')) as ctx: + mod = ctx.load_module('yolo-system') + self.assertIsInstance(mod, Module) + finally: + del os.environ['YANGPATH'] + + def test_ctx_load_module(self): + with Context(YANG_DIR) as ctx: + mod = ctx.load_module('yolo-system') + self.assertIsInstance(mod, Module) + + def test_ctx_get_module(self): + with Context(YANG_DIR) as ctx: + ctx.load_module('yolo-system') + mod = ctx.get_module('wtf-types') + self.assertIsInstance(mod, Module) + + def test_ctx_get_invalid_module(self): + with Context(YANG_DIR) as ctx: + ctx.load_module('wtf-types') + with self.assertRaises(LibyangError): + ctx.get_module('yolo-system') + + def test_ctx_load_invalid_module(self): + with Context(YANG_DIR) as ctx: + with self.assertRaises(LibyangError): + ctx.load_module('invalid-module') + + def test_ctx_find_path(self): + with Context(YANG_DIR) as ctx: + ctx.load_module('yolo-system') + node = next(ctx.find_path('/yolo-system:format-disk')) + self.assertIsInstance(node, SRpc) + + def test_ctx_iter_modules(self): + with Context(YANG_DIR) as ctx: + ctx.load_module('yolo-system') + modules = list(iter(ctx)) + self.assertGreater(len(modules), 0) diff --git a/python/tests/test_data.py b/python/tests/test_data.py new file mode 100644 index 000000000..3a3f27e8b --- /dev/null +++ b/python/tests/test_data.py @@ -0,0 +1,347 @@ +# Copyright (c) 2020 6WIND S.A. +# SPDX-License-Identifier: BSD-3-Clause + +import json +import os +import unittest +from unittest.mock import patch + +from libyang import Context +from libyang import LibyangError +from libyang.data import DContainer +from libyang.data import DNode +from libyang.data import DRpc +from libyang.schema import SContainer +from libyang.schema import SLeaf +from libyang.schema import SRpc + + +YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang') + + +#------------------------------------------------------------------------------ +class DataTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + mod = self.ctx.load_module('yolo-system') + mod.feature_enable_all() + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + JSON_CONFIG = '''{ + "yolo-system:conf": { + "hostname": "foo", + "speed": 1234, + "number": [ + 1000, + 2000, + 3000 + ], + "url": [ + { + "proto": "https", + "host": "github.com", + "path": "/rjarry/libyang-cffi", + "enabled": false + }, + { + "proto": "http", + "host": "foobar.com", + "port": 8080, + "path": "/index.html", + "enabled": true + } + ] + } +} +''' + + def test_data_parse_config_json(self): + dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, 'json', config=True) + self.assertIsInstance(dnode, DContainer) + try: + j = dnode.print_mem('json', pretty=True) + self.assertEqual(j, self.JSON_CONFIG) + finally: + dnode.free() + + JSON_STATE = '''{ + "yolo-system:state": { + "hostname": "foo", + "speed": 1234, + "number": [ + 1000, + 2000, + 3000 + ], + "url": [ + { + "proto": "https", + "host": "github.com", + "path": "/rjarry/libyang-cffi", + "enabled": false + }, + { + "proto": "http", + "host": "foobar.com", + "port": 8080, + "path": "/index.html", + "enabled": true + } + ] + } +} +''' + + def test_data_parse_state_json(self): + dnode = self.ctx.parse_data_mem( + self.JSON_STATE, 'json', data=True, no_yanglib=True) + self.assertIsInstance(dnode, DContainer) + try: + j = dnode.print_mem('json', pretty=True) + self.assertEqual(j, self.JSON_STATE) + finally: + dnode.free() + + XML_CONFIG = ''' + foo + 1234 + 1000 + 2000 + 3000 + + https + github.com + /rjarry/libyang-cffi + false + + + http + foobar.com + 8080 + /index.html + true + + +''' + + def test_data_parse_config_xml(self): + dnode = self.ctx.parse_data_mem(self.XML_CONFIG, 'xml', config=True) + self.assertIsInstance(dnode, DContainer) + try: + xml = dnode.print_mem('xml', pretty=True) + self.assertEqual(xml, self.XML_CONFIG) + finally: + dnode.free() + + XML_STATE = ''' + foo + 1234 + 1000 + 2000 + 3000 + + https + github.com + /rjarry/libyang-cffi + false + + + http + foobar.com + 8080 + /index.html + true + + +''' + + def test_data_parse_data_xml(self): + dnode = self.ctx.parse_data_mem( + self.XML_STATE, 'xml', data=True, no_yanglib=True) + self.assertIsInstance(dnode, DContainer) + try: + xml = dnode.print_mem('xml', pretty=True) + self.assertEqual(xml, self.XML_STATE) + finally: + dnode.free() + + def test_data_create_paths(self): + state = self.ctx.create_data_path('/yolo-system:state') + try: + state.create_path('hostname', 'foo') + state.create_path('speed', 1234) + state.create_path('number', 1000) + state.create_path('number', 2000) + state.create_path('number', 3000) + u = state.create_path('url[proto="https"][host="github.com"]') + u.create_path('path', '/rjarry/libyang-cffi') + u.create_path('enabled', False) + u = state.create_path('url[proto="http"][host="foobar.com"]') + u.create_path('port', 8080) + u.create_path('path', '/index.html') + u.create_path('enabled', True) + state.validate(strict=True) + self.assertEqual(state.print_mem('json', pretty=True), self.JSON_STATE) + finally: + state.free() + + def test_data_create_invalid_type(self): + s = self.ctx.create_data_path('/yolo-system:state') + try: + with self.assertRaises(LibyangError): + s.create_path('speed', 1234000000000000000000000000) + finally: + s.free() + + def test_data_create_invalid_regexp(self): + s = self.ctx.create_data_path('/yolo-system:state') + try: + with self.assertRaises(LibyangError): + s.create_path('hostname', 'INVALID.HOST') + finally: + s.free() + + DICT_CONFIG = { + 'conf': { + 'hostname': 'foo', + 'speed': 1234, + 'number': [1000, 2000, 3000], + 'url': [ + { + 'proto': 'https', + 'host': 'github.com', + 'path': '/rjarry/libyang-cffi', + 'enabled': False, + }, + { + 'proto': 'http', + 'host': 'foobar.com', + 'port': 8080, + 'path': '/index.html', + 'enabled': True, + }, + ], + }, + } + + def test_data_to_dict_config(self): + dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, 'json', config=True) + self.assertIsInstance(dnode, DContainer) + try: + dic = dnode.print_dict() + finally: + dnode.free() + self.assertEqual(dic, self.DICT_CONFIG) + + def test_data_to_dict_rpc_input(self): + dnode = self.ctx.parse_data_mem( + '{"yolo-system:format-disk": {"disk": "/dev/sda"}}', 'json', rpc=True) + self.assertIsInstance(dnode, DRpc) + try: + dic = dnode.print_dict() + finally: + dnode.free() + self.assertEqual(dic, {'format-disk': {'disk': '/dev/sda'}}) + + def test_data_from_dict_module(self): + module = self.ctx.get_module('yolo-system') + dnode = module.parse_data_dict(self.DICT_CONFIG) + self.assertIsInstance(dnode, DContainer) + try: + j = dnode.print_mem('json', pretty=True) + finally: + dnode.free() + self.assertEqual(json.loads(j), json.loads(self.JSON_CONFIG)) + + def test_data_from_dict_invalid(self): + module = self.ctx.get_module('yolo-system') + orig_create = Context.create_data_path + orig_free = DNode.free + created = [] + freed = [] + + def wrapped_create(self, *args, **kwargs): + c = orig_create(self, *args, **kwargs) + if c is not None: + created.append(c) + return c + + def wrapped_free(self, *args, **kwargs): + freed.append(self) + orig_free(self, *args, **kwargs) + + root = module.parse_data_dict({ + 'conf': { + 'hostname': 'foo', + 'speed': 1234, + 'number': [1000, 2000, 3000], + } + }) + + invalid_dict = { + 'conf': { + 'url': [ + { + 'proto': 'https', + 'host': 'github.com', + 'path': '/rjarry/libyang-cffi', + 'enabled': False, + }, + { + 'proto': 'http', + 'host': 'foobar.com', + 'port': 'INVALID.PORT', + 'path': '/index.html', + 'enabled': True, + }, + ], + }, + } + + try: + with patch.object(Context, 'create_data_path', wrapped_create), \ + patch.object(DNode, 'free', wrapped_free): + with self.assertRaises(LibyangError): + module.parse_data_dict(invalid_dict, parent=root) + self.assertGreater(len(created), 0) + self.assertGreater(len(freed), 0) + self.assertEqual(freed, list(reversed(created))) + finally: + root.free() + + def test_data_from_dict_container(self): + snode = next(self.ctx.find_path('/yolo-system:conf')) + self.assertIsInstance(snode, SContainer) + dnode = snode.parse_data_dict(self.DICT_CONFIG) + self.assertIsInstance(dnode, DContainer) + try: + j = dnode.print_mem('json', pretty=True) + finally: + dnode.free() + self.assertEqual(json.loads(j), json.loads(self.JSON_CONFIG)) + + def test_data_from_dict_leaf(self): + snode = next(self.ctx.find_path('/yolo-system:state/yolo-system:hostname')) + self.assertIsInstance(snode, SLeaf) + dnode = snode.parse_data_dict({'hostname': 'foo'}) + try: + j = dnode.print_mem('json') + finally: + dnode.free() + self.assertEqual(j, '{"yolo-system:state":{"hostname":"foo"}}') + + def test_data_from_dict_rpc(self): + snode = next(self.ctx.find_path('/yolo-system:format-disk')) + self.assertIsInstance(snode, SRpc) + dnode = snode.parse_data_dict({'format-disk': {'duration': 42}}, + rpc_output=True) + self.assertIsInstance(dnode, DRpc) + try: + j = dnode.print_mem('json') + finally: + dnode.free() + self.assertEqual(j, '{"yolo-system:format-disk":{"duration":42}}') diff --git a/python/tests/test_diff.py b/python/tests/test_diff.py new file mode 100644 index 000000000..129b8f433 --- /dev/null +++ b/python/tests/test_diff.py @@ -0,0 +1,59 @@ +# Copyright (c) 2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +import os +import unittest + +from libyang import Context +from libyang.diff import BaseTypeAdded +from libyang.diff import BaseTypeRemoved +from libyang.diff import NodeTypeAdded +from libyang.diff import NodeTypeRemoved +from libyang.diff import SNodeAdded +from libyang.diff import SNodeRemoved +from libyang.diff import StatusAdded +from libyang.diff import StatusRemoved +from libyang.diff import schema_diff + + +OLD_YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang-old') +NEW_YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang') + + +#------------------------------------------------------------------------------ +class DiffTest(unittest.TestCase): + + expected_diffs = frozenset(( + (BaseTypeAdded, '/yolo-system:conf/yolo-system:speed'), + (BaseTypeAdded, '/yolo-system:state/yolo-system:speed'), + (BaseTypeRemoved, '/yolo-system:conf/yolo-system:speed'), + (BaseTypeRemoved, '/yolo-system:state/yolo-system:speed'), + (NodeTypeAdded, '/yolo-system:conf/yolo-system:number'), + (NodeTypeAdded, '/yolo-system:state/yolo-system:number'), + (NodeTypeRemoved, '/yolo-system:conf/yolo-system:number'), + (NodeTypeRemoved, '/yolo-system:state/yolo-system:number'), + (SNodeAdded, '/yolo-system:conf/yolo-system:url/yolo-system:enabled'), + (SNodeAdded, '/yolo-system:state/yolo-system:url/yolo-system:enabled'), + (StatusAdded, '/yolo-system:conf/yolo-system:deprecated-leaf'), + (StatusAdded, '/yolo-system:conf/yolo-system:obsolete-leaf'), + (StatusAdded, '/yolo-system:state/yolo-system:deprecated-leaf'), + (StatusAdded, '/yolo-system:state/yolo-system:obsolete-leaf'), + (StatusRemoved, '/yolo-system:conf/yolo-system:deprecated-leaf'), + (StatusRemoved, '/yolo-system:conf/yolo-system:obsolete-leaf'), + (StatusRemoved, '/yolo-system:state/yolo-system:deprecated-leaf'), + (StatusRemoved, '/yolo-system:state/yolo-system:obsolete-leaf'), + )) + + def test_diff(self): + with Context(OLD_YANG_DIR) as ctx_old, Context(NEW_YANG_DIR) as ctx_new: + mod = ctx_old.load_module('yolo-system') + mod.feature_enable_all() + mod = ctx_new.load_module('yolo-system') + mod.feature_enable_all() + diffs = [] + for d in schema_diff(ctx_old, ctx_new): + if isinstance(d, (SNodeAdded, SNodeRemoved)): + diffs.append((d.__class__, d.node.schema_path())) + else: + diffs.append((d.__class__, d.new.schema_path())) + self.assertEqual(frozenset(diffs), self.expected_diffs) diff --git a/python/tests/test_schema.py b/python/tests/test_schema.py new file mode 100644 index 000000000..e6f4a946f --- /dev/null +++ b/python/tests/test_schema.py @@ -0,0 +1,363 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: BSD-3-Clause + +import os +import unittest + +from libyang import Context +from libyang.schema import Extension +from libyang.schema import IfFeature +from libyang.schema import IfOrFeatures +from libyang.schema import Module +from libyang.schema import Revision +from libyang.schema import SContainer +from libyang.schema import SLeaf +from libyang.schema import SLeafList +from libyang.schema import SList +from libyang.schema import SNode +from libyang.schema import SRpc +from libyang.schema import Type +from libyang.util import LibyangError + + +YANG_DIR = os.path.join(os.path.dirname(__file__), 'yang') + + +#------------------------------------------------------------------------------ +class ModuleTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + self.module = self.ctx.load_module('yolo-system') + + def tearDown(self): + self.module = None + self.ctx.destroy() + self.ctx = None + + def test_mod_print_mem(self): + s = self.module.print_mem('tree') + self.assertGreater(len(s), 0) + + def test_mod_attrs(self): + self.assertEqual(self.module.name(), 'yolo-system') + self.assertEqual(self.module.description(), 'YOLO.') + self.assertEqual(self.module.prefix(), 'sys') + + def test_mod_filepath(self): + self.assertEqual(self.module.filepath(), + os.path.join(YANG_DIR, 'yolo/yolo-system.yang')) + + def test_mod_iter(self): + children = list(iter(self.module)) + self.assertEqual(len(children), 4) + + def test_mod_children_rpcs(self): + rpcs = list(self.module.children(types=(SNode.RPC,))) + self.assertEqual(len(rpcs), 2) + + def test_mod_enable_features(self): + self.assertFalse(self.module.feature_state('turbo-boost')) + self.module.feature_enable('turbo-boost') + self.assertTrue(self.module.feature_state('turbo-boost')) + self.module.feature_disable('turbo-boost') + self.assertFalse(self.module.feature_state('turbo-boost')) + self.module.feature_enable_all() + self.assertTrue(self.module.feature_state('turbo-boost')) + self.module.feature_disable_all() + + def test_mod_features(self): + features = list(self.module.features()) + self.assertEqual(len(features), 2) + + def test_mod_get_feature(self): + self.module.feature_enable('turbo-boost') + feature = self.module.get_feature('turbo-boost') + self.assertEqual(feature.name(), 'turbo-boost') + self.assertEqual(feature.description(), 'Goes faster.') + self.assertIsNone(feature.reference()) + self.assertTrue(feature.state()) + self.assertFalse(feature.deprecated()) + self.assertFalse(feature.obsolete()) + + def test_mod_get_feature_not_found(self): + with self.assertRaises(LibyangError): + self.module.get_feature('does-not-exist') + + def test_mod_revisions(self): + revisions = list(self.module.revisions()) + self.assertEqual(len(revisions), 2) + self.assertIsInstance(revisions[0], Revision) + self.assertEqual(revisions[0].date(), '1999-04-01') + self.assertEqual(revisions[1].date(), '1990-04-01') + + +#------------------------------------------------------------------------------ +class RevisionTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + mod = self.ctx.load_module('yolo-system') + revisions = list(mod.revisions()) + self.revision = revisions[0] + + def tearDown(self): + self.revision = None + self.ctx.destroy() + self.ctx = None + + def test_rev_date(self): + self.assertEqual(self.revision.date(), '1999-04-01') + + def test_rev_reference(self): + self.assertEqual(self.revision.reference(), + 'RFC 2549 - IP over Avian Carriers with Quality of Service.') + + def test_rev_description(self): + self.assertEqual(self.revision.description(), 'Version update.') + + def test_rev_extensions(self): + exts = list(self.revision.extensions()) + self.assertEqual(len(exts), 1) + ext = self.revision.get_extension('human-name', prefix='omg-extensions') + self.assertIsInstance(ext, Extension) + + +#------------------------------------------------------------------------------ +class IfFeatureTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + mod = self.ctx.load_module('yolo-system') + mod.feature_enable_all() + self.leaf = next(self.ctx.find_path( + '/yolo-system:conf/yolo-system:isolation-level')) + + def tearDown(self): + self.container = None + self.ctx.destroy() + self.ctx = None + + def test_iffeatures(self): + iffeatures = list(self.leaf.if_features()) + self.assertEqual(len(iffeatures), 1) + + def test_iffeature_tree(self): + iff = next(self.leaf.if_features()) + tree = iff.tree() + self.assertIsInstance(tree, IfOrFeatures) + self.assertIsInstance(tree.a, IfFeature) + self.assertIsInstance(tree.b, IfFeature) + self.assertEqual(tree.a.feature().name(), 'turbo-boost') + self.assertEqual(tree.b.feature().name(), 'networking') + + def test_iffeature_str(self): + iff = next(self.leaf.if_features()) + self.assertEqual(str(iff), 'turbo-boost OR networking') + + def test_iffeature_dump(self): + iff = next(self.leaf.if_features()) + self.assertEqual(iff.dump(), '''OR + turbo-boost [Goes faster.] + networking [Supports networking.] +''') + + +#------------------------------------------------------------------------------ +class ContainerTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + mod = self.ctx.load_module('yolo-system') + mod.feature_enable_all() + self.container = next(self.ctx.find_path('/yolo-system:conf')) + + def tearDown(self): + self.container = None + self.ctx.destroy() + self.ctx = None + + def test_cont_attrs(self): + self.assertIsInstance(self.container, SContainer) + self.assertEqual(self.container.nodetype(), SNode.CONTAINER) + self.assertEqual(self.container.keyword(), 'container') + self.assertEqual(self.container.name(), 'conf') + self.assertEqual(self.container.fullname(), 'yolo-system:conf') + self.assertEqual(self.container.description(), 'Configuration.') + self.assertEqual(self.container.config_set(), False) + self.assertEqual(self.container.config_false(), False) + self.assertEqual(self.container.mandatory(), False) + self.assertIsInstance(self.container.module(), Module) + self.assertEqual(self.container.schema_path(), '/yolo-system:conf') + self.assertEqual(self.container.data_path(), '/yolo-system:conf') + self.assertIs(self.container.presence(), None) + + def test_cont_iter(self): + children = list(iter(self.container)) + self.assertEqual(len(children), 7) + + def test_cont_children_leafs(self): + leafs = list(self.container.children(types=(SNode.LEAF,))) + self.assertEqual(len(leafs), 5) + + def test_cont_parent(self): + self.assertIsNone(self.container.parent()) + + +#------------------------------------------------------------------------------ +class ListTest(unittest.TestCase): + + SCHEMA_PATH = '/yolo-system:conf/yolo-system:url' + DATA_PATH = "/yolo-system:conf/url[proto='%s'][host='%s']" + + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module('yolo-system') + self.list = next(self.ctx.find_path(self.SCHEMA_PATH)) + + def tearDown(self): + self.list = None + self.ctx.destroy() + self.ctx = None + + def test_list_attrs(self): + self.assertIsInstance(self.list, SList) + self.assertEqual(self.list.nodetype(), SNode.LIST) + self.assertEqual(self.list.keyword(), 'list') + self.assertEqual(self.list.schema_path(), self.SCHEMA_PATH) + self.assertEqual(self.list.data_path(), self.DATA_PATH) + self.assertFalse(self.list.ordered()) + + def test_list_keys(self): + keys = list(self.list.keys()) + self.assertEqual(len(keys), 2) + + def test_list_iter(self): + children = list(iter(self.list)) + self.assertEqual(len(children), 5) + + def test_list_children_skip_keys(self): + children = list(self.list.children(skip_keys=True)) + self.assertEqual(len(children), 3) + + def test_list_parent(self): + parent = self.list.parent() + self.assertIsNotNone(parent) + self.assertIsInstance(parent, SContainer) + self.assertEqual(parent.name(), 'conf') + + +#------------------------------------------------------------------------------ +class RpcTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module('yolo-system') + self.rpc = next(self.ctx.find_path('/yolo-system:format-disk')) + + def tearDown(self): + self.rpc = None + self.ctx.destroy() + self.ctx = None + + def test_rpc_attrs(self): + self.assertIsInstance(self.rpc, SRpc) + self.assertEqual(self.rpc.nodetype(), SNode.RPC) + self.assertEqual(self.rpc.keyword(), 'rpc') + self.assertEqual(self.rpc.schema_path(), '/yolo-system:format-disk') + + def test_rpc_extensions(self): + ext = list(self.rpc.extensions()) + self.assertEqual(len(ext), 1) + ext = self.rpc.get_extension('require-admin', prefix='omg-extensions') + self.assertIsInstance(ext, Extension) + + def test_rpc_params(self): + leaf = next(self.rpc.children()) + self.assertIsInstance(leaf, SLeaf) + self.assertEqual(leaf.data_path(), '/yolo-system:format-disk/disk') + leaf = next(self.rpc.input().children()) + self.assertIsInstance(leaf, SLeaf) + + def test_rpc_no_parent(self): + self.assertIsNone(self.rpc.parent()) + + +#------------------------------------------------------------------------------ +class LeafTypeTest(unittest.TestCase): + + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module('yolo-system') + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_leaf_type_derived(self): + leaf = next(self.ctx.find_path('/yolo-system:conf/yolo-system:hostname')) + self.assertIsInstance(leaf, SLeaf) + t = leaf.type() + self.assertIsInstance(t, Type) + self.assertEqual(t.name(), 'host') + self.assertEqual(t.base(), Type.STRING) + d = t.derived_type() + self.assertEqual(d.name(), 'str') + dd = d.derived_type() + self.assertEqual(dd.name(), 'string') + + def test_leaf_type_status(self): + leaf = next(self.ctx.find_path('/yolo-system:conf/yolo-system:hostname')) + self.assertIsInstance(leaf, SLeaf) + self.assertEqual(leaf.deprecated(), False) + self.assertEqual(leaf.obsolete(), False) + leaf = next(self.ctx.find_path('/yolo-system:conf/yolo-system:deprecated-leaf')) + self.assertIsInstance(leaf, SLeaf) + self.assertEqual(leaf.deprecated(), True) + self.assertEqual(leaf.obsolete(), False) + leaf = next(self.ctx.find_path('/yolo-system:conf/yolo-system:obsolete-leaf')) + self.assertIsInstance(leaf, SLeaf) + self.assertEqual(leaf.deprecated(), False) + self.assertEqual(leaf.obsolete(), True) + + def test_leaf_type_union(self): + leaf = next(self.ctx.find_path('/yolo-system:conf/yolo-system:number')) + self.assertIsInstance(leaf, SLeafList) + t = leaf.type() + self.assertIsInstance(t, Type) + self.assertEqual(t.name(), 'number') + self.assertEqual(t.base(), Type.UNION) + types = set(u.name() for u in t.union_types()) + self.assertEqual(types, set(['signed', 'unsigned'])) + bases = set(t.basenames()) + self.assertEqual(bases, set(['int16', 'int32', 'uint16', 'uint32'])) + + def test_leaf_type_enum(self): + leaf = next(self.ctx.find_path( + '/yolo-system:conf/yolo-system:url/yolo-system:proto')) + self.assertIsInstance(leaf, SLeaf) + t = leaf.type() + self.assertIsInstance(t, Type) + self.assertEqual(t.name(), 'protocol') + self.assertEqual(t.base(), Type.ENUM) + enums = [e for e, _ in t.enums()] + self.assertEqual(enums, ['http', 'https', 'ftp', 'sftp', 'tftp']) + + def test_leaf_type_bits(self): + leaf = next(self.ctx.find_path( + '/yolo-system:chmod/yolo-system:input/yolo-system:perms')) + self.assertIsInstance(leaf, SLeaf) + t = leaf.type() + self.assertIsInstance(t, Type) + self.assertEqual(t.name(), 'permissions') + self.assertEqual(t.base(), Type.BITS) + bits = [b for b, _ in t.bits()] + self.assertEqual(bits, ['read', 'write', 'execute']) + + def test_leaf_parent(self): + leaf = next(self.ctx.find_path( + '/yolo-system:conf/yolo-system:url/yolo-system:proto')) + parent = leaf.parent() + self.assertIsNotNone(parent) + self.assertIsInstance(parent, SList) + self.assertEqual(parent.name(), 'url') diff --git a/python/tests/yang-old/omg/omg-extensions.yang b/python/tests/yang-old/omg/omg-extensions.yang new file mode 100644 index 000000000..bc10ff65a --- /dev/null +++ b/python/tests/yang-old/omg/omg-extensions.yang @@ -0,0 +1,15 @@ +module omg-extensions { + namespace "urn:yang:omg:extensions"; + prefix e; + + extension require-admin { + description + "Extend an rpc or node to require admin permissions."; + } + + extension human-name { + description + "Extend a node to provide a human readable name."; + argument name; + } +} diff --git a/python/tests/yang-old/wtf/wtf-types.yang b/python/tests/yang-old/wtf/wtf-types.yang new file mode 100644 index 000000000..d61ee7234 --- /dev/null +++ b/python/tests/yang-old/wtf/wtf-types.yang @@ -0,0 +1,53 @@ +module wtf-types { + namespace "urn:yang:wtf:types"; + prefix t; + + typedef str { + type string; + } + + typedef host { + type str { + pattern "[a-z]+"; + } + } + + typedef unsigned { + type union { + type uint16; + type uint32; + } + } + + typedef signed { + type union { + type int16; + type int32; + } + } + + typedef number { + type union { + type unsigned; + type signed; + } + } + + typedef protocol { + type enumeration { + enum http; + enum https; + enum ftp; + enum sftp; + enum tftp; + } + } + + typedef permissions { + type bits { + bit read; + bit write; + bit execute; + } + } +} diff --git a/python/tests/yang-old/yolo/yolo-system.yang b/python/tests/yang-old/yolo/yolo-system.yang new file mode 100644 index 000000000..a0525ad6f --- /dev/null +++ b/python/tests/yang-old/yolo/yolo-system.yang @@ -0,0 +1,137 @@ +module yolo-system { + yang-version 1.1; + namespace "urn:yang:yolo:system"; + prefix sys; + + import omg-extensions { prefix ext; } + import wtf-types { prefix types; } + + description + "YOLO."; + + revision 1999-04-01 { + description + "Version update."; + reference "RFC 2549 - IP over Avian Carriers with Quality of Service."; + ext:human-name "2549"; + } + + revision 1990-04-01 { + description + "Initial version."; + reference "RFC 1149 - A Standard for the Transmission of IP Datagrams on Avian Carriers."; + ext:human-name "1149"; + } + + feature turbo-boost { + description + "Goes faster."; + } + + feature networking { + description + "Supports networking."; + } + + grouping tree { + leaf hostname { + description + "The hostname."; + type types:host; + } + + leaf deprecated-leaf { + description + "A deprecated leaf."; + type string; + } + + leaf obsolete-leaf { + description + "An obsolete leaf."; + type string; + } + + list url { + description + "An URL."; + key "proto host"; + leaf proto { + type types:protocol; + } + leaf host { + type string; + } + leaf port { + type uint16; + } + leaf path { + type string; + } + } + + leaf number { + description + "A number."; + type types:number; + } + + leaf speed { + description + "The speed."; + if-feature turbo-boost; + type uint64; + } + + leaf isolation-level { + description + "The level of isolation."; + if-feature "turbo-boost or networking"; + type uint32; + } + } + + container conf { + description + "Configuration."; + uses tree; + } + + container state { + description + "State."; + config false; + uses tree; + } + + rpc chmod { + description + "Change permissions on path."; + ext:require-admin; + input { + leaf path { + type string; + } + leaf perms { + type types:permissions; + } + } + } + + rpc format-disk { + description + "Format disk."; + ext:require-admin; + input { + leaf disk { + ext:human-name "Disk"; + type types:str; + } + } + output { + leaf duration { + type uint32; + } + } + } +} diff --git a/python/tests/yang/omg/omg-extensions.yang b/python/tests/yang/omg/omg-extensions.yang new file mode 100644 index 000000000..bc10ff65a --- /dev/null +++ b/python/tests/yang/omg/omg-extensions.yang @@ -0,0 +1,15 @@ +module omg-extensions { + namespace "urn:yang:omg:extensions"; + prefix e; + + extension require-admin { + description + "Extend an rpc or node to require admin permissions."; + } + + extension human-name { + description + "Extend a node to provide a human readable name."; + argument name; + } +} diff --git a/python/tests/yang/wtf/wtf-types.yang b/python/tests/yang/wtf/wtf-types.yang new file mode 100644 index 000000000..d61ee7234 --- /dev/null +++ b/python/tests/yang/wtf/wtf-types.yang @@ -0,0 +1,53 @@ +module wtf-types { + namespace "urn:yang:wtf:types"; + prefix t; + + typedef str { + type string; + } + + typedef host { + type str { + pattern "[a-z]+"; + } + } + + typedef unsigned { + type union { + type uint16; + type uint32; + } + } + + typedef signed { + type union { + type int16; + type int32; + } + } + + typedef number { + type union { + type unsigned; + type signed; + } + } + + typedef protocol { + type enumeration { + enum http; + enum https; + enum ftp; + enum sftp; + enum tftp; + } + } + + typedef permissions { + type bits { + bit read; + bit write; + bit execute; + } + } +} diff --git a/python/tests/yang/yolo/yolo-system.yang b/python/tests/yang/yolo/yolo-system.yang new file mode 100644 index 000000000..61ea48bca --- /dev/null +++ b/python/tests/yang/yolo/yolo-system.yang @@ -0,0 +1,142 @@ +module yolo-system { + yang-version 1.1; + namespace "urn:yang:yolo:system"; + prefix sys; + + import omg-extensions { prefix ext; } + import wtf-types { prefix types; } + + description + "YOLO."; + + revision 1999-04-01 { + description + "Version update."; + reference "RFC 2549 - IP over Avian Carriers with Quality of Service."; + ext:human-name "2549"; + } + + revision 1990-04-01 { + description + "Initial version."; + reference "RFC 1149 - A Standard for the Transmission of IP Datagrams on Avian Carriers."; + ext:human-name "1149"; + } + + feature turbo-boost { + description + "Goes faster."; + } + + feature networking { + description + "Supports networking."; + } + + grouping tree { + leaf hostname { + description + "The hostname."; + type types:host; + } + + leaf deprecated-leaf { + status deprecated; + description + "A deprecated leaf."; + type string; + } + + leaf obsolete-leaf { + status obsolete; + description + "An obsolete leaf."; + type string; + } + + list url { + description + "An URL."; + key "proto host"; + leaf proto { + type types:protocol; + } + leaf host { + type string; + } + leaf port { + type uint16; + } + leaf path { + type string; + } + leaf enabled { + type boolean; + } + } + + leaf-list number { + description + "A number."; + type types:number; + } + + leaf speed { + description + "The speed."; + if-feature turbo-boost; + type uint32; + } + + leaf isolation-level { + description + "The level of isolation."; + if-feature "turbo-boost or networking"; + type uint32; + } + } + + container conf { + description + "Configuration."; + uses tree; + } + + container state { + description + "State."; + config false; + uses tree; + } + + rpc chmod { + description + "Change permissions on path."; + ext:require-admin; + input { + leaf path { + type string; + } + leaf perms { + type types:permissions; + } + } + } + + rpc format-disk { + description + "Format disk."; + ext:require-admin; + input { + leaf disk { + ext:human-name "Disk"; + type types:str; + } + } + output { + leaf duration { + type uint32; + } + } + } +} diff --git a/swig/CMakeLists.txt b/swig/CMakeLists.txt index cbd694db0..2819a5d48 100644 --- a/swig/CMakeLists.txt +++ b/swig/CMakeLists.txt @@ -18,32 +18,6 @@ if(GEN_LANGUAGE_BINDINGS) endif() endif() -# find Python package -if(GEN_PYTHON_BINDINGS AND SWIG_FOUND) - message(STATUS "Python version ${GEN_PYTHON_VERSION} was selected") - unset(PYTHON_LIBRARY CACHE) - unset(PYTHON_EXECUTABLE CACHE) - unset(PYTHON_INCLUDE_DIR CACHE) - unset(PYTHON_LIBRARY_DEBUG CACHE) - if(${GEN_PYTHON_VERSION} STREQUAL "2") - find_package(PythonInterp 2 REQUIRED) - find_package(PythonLibs 2 REQUIRED) - if(NOT PYTHONLIBS_FOUND) - message(WARNING "Did not found Python version 2.x") - message(STATUS "Libyang supports Python 2.x and Python 3.x") - endif() - elseif(${GEN_PYTHON_VERSION} STREQUAL "3") - find_package(PythonInterp 3 REQUIRED) - find_package(PythonLibs 3 REQUIRED) - if(NOT PYTHONLIBS_FOUND) - message(WARNING "Did not found Python version 3.x") - message(STATUS "Libyang supports Python 2.x and Python 3.x") - endif() - else() - message(WARNING "Libyang supports Python 2.x and Python 3.x") - endif() -endif() - set(LIBYANG_CPP_SOURCES ${CMAKE_SOURCE_DIR}/swig/cpp/src/Xml.cpp ${CMAKE_SOURCE_DIR}/swig/cpp/src/Libyang.cpp @@ -99,12 +73,6 @@ if (GEN_CPP_BINDINGS) endif() endif() -if(ENABLE_STATIC AND PYTHONLIBS_FOUND AND PYTHONINTERP_FOUND AND (${GEN_PYTHON_VERSION} STREQUAL "2" OR ${GEN_PYTHON_VERSION} STREQUAL "3")) - message(WARNING "Can't create a static Python module") -elseif(PYTHONLIBS_FOUND AND PYTHONINTERP_FOUND AND (${GEN_PYTHON_VERSION} STREQUAL "2" OR ${GEN_PYTHON_VERSION} STREQUAL "3")) - add_subdirectory(python) -endif() - if(NOT ENABLE_STATIC AND GEN_JAVASCRIPT_BINDINGS) message(WARNING "Can't create Javascript bindings with a shared library, please use -DENABLE_STATIC") elseif(ENABLE_STATIC AND GEN_JAVASCRIPT_BINDINGS) diff --git a/swig/python/CMakeLists.txt b/swig/python/CMakeLists.txt deleted file mode 100644 index 994b12349..000000000 --- a/swig/python/CMakeLists.txt +++ /dev/null @@ -1,61 +0,0 @@ -set(PYTHON_SWIG_BINDING yang) -include_directories(${PYTHON_INCLUDE_PATH}) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) - -set(CMAKE_SWIG_FLAGS "-c++") -set(CMAKE_SWIG_FLAGS "-I${PROJECT_SOURCE_DIR}") -set(CMAKE_SWIG_OUTDIR ${CMAKE_CURRENT_BINARY_DIR}) - -set_source_files_properties(${PYTHON_SWIG_BINDING}.i PROPERTIES CPLUSPLUS ON PREFIX "") - -if(${CMAKE_VERSION} VERSION_LESS "3.8.0") - swig_add_module(${PYTHON_SWIG_BINDING} python ${PYTHON_SWIG_BINDING}.i) -else() - swig_add_library(${PYTHON_SWIG_BINDING} LANGUAGE python SOURCES ${PYTHON_SWIG_BINDING}.i) -endif() -swig_link_libraries(${PYTHON_SWIG_BINDING} ${PYTHON_LIBRARIES} libyang-cpp) - -# Generate header with SWIG run-time functions -execute_process(COMMAND ${SWIG_EXECUTABLE} -python -external-runtime ${CMAKE_CURRENT_BINARY_DIR}/swigpyrun.h) - -file(COPY "examples" DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) - -execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(plat_specific=True))" - OUTPUT_VARIABLE PYTHON_MODULE_PATH - OUTPUT_STRIP_TRAILING_WHITESPACE ) - -install( TARGETS _${PYTHON_SWIG_BINDING} DESTINATION ${PYTHON_MODULE_PATH}) -install( FILES "${CMAKE_CURRENT_BINARY_DIR}/${PYTHON_SWIG_BINDING}.py" DESTINATION ${PYTHON_MODULE_PATH}) -install( FILES "${CMAKE_CURRENT_BINARY_DIR}/swigpyrun.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/libyang) - -if(ENABLE_BUILD_TESTS) - set(PY2_SWIG_DIR ${CMAKE_CURRENT_BINARY_DIR}) - set(PY2_TEST_DIR ${PY2_SWIG_DIR}/tests) - - file(COPY "tests" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") - file(COPY "run_python_test.sh" DESTINATION "${PY2_SWIG_DIR}") - - macro(ADD_PYTHON_TEST TEST_NAME) - add_test(NAME python_${TEST_NAME} - COMMAND sh ${PY2_SWIG_DIR}/run_python_test.sh - "${CMAKE_BINARY_DIR}/src:${CMAKE_BINARY_DIR}/tests:${PY2_TEST_DIR}" - "${PY2_SWIG_DIR}:${PROJECT_SOURCE_DIR}/swig/python:${PYTHON_BUILD_DIR}" - "${PYTHON_EXECUTABLE}" - "${PY2_SWIG_DIR}/tests/${TEST_NAME}.py" - "${CMAKE_BINARY_DIR}" - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - ) - endmacro(ADD_PYTHON_TEST) - - ADD_PYTHON_TEST(test_libyang) - ADD_PYTHON_TEST(test_tree_data) - ADD_PYTHON_TEST(test_tree_schema) - - add_custom_command(TARGET ${SWIG_MODULE_${PYTHON_SWIG_BINDING}_REAL_NAME} POST_BUILD - COMMAND cp "${CMAKE_CURRENT_BINARY_DIR}/_${PYTHON_SWIG_BINDING}.so" ${PY2_SWIG_DIR}/tests - COMMAND cp "${CMAKE_CURRENT_BINARY_DIR}/${PYTHON_SWIG_BINDING}.py" ${PY2_SWIG_DIR}/tests - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - ) - - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/config.py.in" "${CMAKE_CURRENT_BINARY_DIR}/tests/config.py" ESCAPE_QUOTES @ONLY) -endif() diff --git a/swig/python/README.md b/swig/python/README.md deleted file mode 100644 index f54edd865..000000000 --- a/swig/python/README.md +++ /dev/null @@ -1,20 +0,0 @@ -## Requirements - -* cmake >= 2.8.12 -* swig -* python3-dev (for python3 bindings) -* python-dev (for python2 bindings) - -## Install - -``` -$ cd libyang/build -$ cmake -DGEN_LANGUAGE_BINDINGS=ON -DGEN_CPP_BINDINGS=ON -DGEN_PYTHON_BINDINGS=ON .. -$ # for python2 bindings add -DGEN_PYTHON_VERSION=2 -$ make -$ make install -``` - -## Recommendations - -To avoid problems it is recommended to use separate build directories for Python 2 and Python 3 bindings. diff --git a/swig/python/config.py.in b/swig/python/config.py.in deleted file mode 100755 index d659735f7..000000000 --- a/swig/python/config.py.in +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function - -__author__ = "Mislav Novakovic " -__copyright__ = "Copyright 2018, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -# This source code is licensed under BSD 3-Clause License (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://opensource.org/licenses/BSD-3-Clause - -TESTS_DIR = "@CMAKE_SOURCE_DIR@/tests" -BUILD_DIR = "@PROJECT_BINARY_DIR@" diff --git a/swig/python/examples/context.py b/swig/python/examples/context.py deleted file mode 100644 index f25f7d51e..000000000 --- a/swig/python/examples/context.py +++ /dev/null @@ -1,40 +0,0 @@ -__author__ = "Mislav Novakovic " -__copyright__ = "Copyright 2017, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -import yang as ly - -ctx = None -try: - ctx = ly.Context("/etc/sysrepo2/yang") -except Exception as e: - print(e) - errors = ly.get_ly_errors(ctx) - for err in errors: - print("err: %d" % err.err()) - print("vecode: %d" % err.vecode()) - print("errmsg: "+err.errmsg()) - print("errpath:"+err.errpath()) - print("errapptag:"+err.errapptag()) - -try: - ctx = ly.Context("/etc/sysrepo/yang") -except Exception as e: - print(e) - -folders = ctx.get_searchdirs() -for folder in folders: - print(folder) -print("\n") - -module = ctx.get_module("ietf-interfaces") -if module is not None: - print(module.name()) -else: - module = ctx.load_module("ietf-interfaces") - if module is not None: - print(module.name()) - -modules = ctx.get_module_iter() -for module in modules: - print("module: " + module.name() + " prefix: " + module.prefix() + " type: " + str(module.type())) diff --git a/swig/python/examples/imp_cb.py b/swig/python/examples/imp_cb.py deleted file mode 100644 index fe0ac0d2a..000000000 --- a/swig/python/examples/imp_cb.py +++ /dev/null @@ -1,24 +0,0 @@ -__author__ = "Mislav Novakovic " -__copyright__ = "Copyright 2017, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -import yang as ly - -mod_a = "module a {namespace urn:a; prefix a; import b { prefix b; } leaf a { type b:mytype; }}" -mod_b = "module b {namespace urn:b; prefix b; typedef mytype { type string; }}" - -def module_imp_clb(mod_name, mod_rev, submod_name, sub_rev, user_data): - print("module_imp_cb called") - print("mod_name: ", mod_name, " mod_rev: ", mod_rev, " submod_name: ", submod_name, " sub_rev: ", sub_rev) - return (ly.LYS_IN_YANG, mod_b) - - -ctx = None -try: - ctx = ly.Context() -except Exception as e: - print(e) - -ctx.set_module_imp_clb(module_imp_clb, None) -module = ctx.parse_module_mem(mod_a, ly.LYS_IN_YANG) -print(module.print_mem(ly.LYS_OUT_TREE, 0)) diff --git a/swig/python/examples/process_tree.py b/swig/python/examples/process_tree.py deleted file mode 100644 index 743b34309..000000000 --- a/swig/python/examples/process_tree.py +++ /dev/null @@ -1,67 +0,0 @@ -__author__ = "Mislav Novakovic " -__copyright__ = "Copyright 2017, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -import yang as ly - -ctx = None -try: - ctx = ly.Context("/etc/sysrepo/yang") -except Exception as e: - print(e) - errors = ly.get_ly_errors(ctx) - for err in errors: - print("err: %d" % err.err()) - print("vecode: %d" % err.vecode()) - print("errmsg: " + err.errmsg()) - print("errpath:" + err.errpath()) - print("errapptag:" + err.errapptag()) - -module = ctx.get_module("turing-machine") -if module is not None: - print(module.name()) -else: - module = ctx.load_module("turing-machine") - -node = None -try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/turing-machine.startup", ly.LYD_LYB, ly.LYD_OPT_CONFIG) -except Exception as e: - print(e) -try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/turing-machine.startup", ly.LYD_XML, ly.LYD_OPT_CONFIG) -except Exception as e: - print(e) -try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/turing-machine.startup", ly.LYD_JSON, ly.LYD_OPT_CONFIG) -except Exception as e: - print(e) - -if node is None: - sys.exit() - -if node is None: - print("parse_data_path did not return any nodes") -else: - print("tree_dfs\n") - data_list = node.tree_dfs() - for elem in data_list: - schema = elem.schema() - print("name: " + schema.name() + " type: " + str(schema.nodetype())) - if (ly.LYS_LEAF == schema.nodetype() or ly.LYS_LEAFLIST == schema.nodetype()): - casted = elem.subtype() - if casted is None: - continue - print("node " + casted.schema().name() + " has value " + casted.value_str()) - - print("\nChild of " + node.schema().name() + " is " + node.child().schema().name() + " \n ") - - print("tree_for\n") - data_list = node.child().child().tree_for() - for elem in data_list: - print("child of " + node.child().schema().name() + " is: " + elem.schema().name() + " type: " + str(elem.schema().nodetype())) - - print("\nschema tree_dfs\n") - schema_list = node.schema().tree_dfs() - for elem in schema_list: - print("schema name: " + elem.name() + " type: " + str(elem.nodetype())) diff --git a/swig/python/examples/subtype.py b/swig/python/examples/subtype.py deleted file mode 100644 index a09375f8d..000000000 --- a/swig/python/examples/subtype.py +++ /dev/null @@ -1,47 +0,0 @@ -__author__ = "Mislav Novakovic " -__copyright__ = "Copyright 2017, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -import yang as ly -import sys - -try: - ctx = ly.Context("/etc/sysrepo/yang") -except Exception as e: - print(e) - sys.exit() - -ctx.load_module("iana-if-type", None) -ctx.load_module("ietf-inet-types", None) -ctx.load_module("ietf-yang-types", None) -ctx.load_module("ietf-interfaces", None) -ctx.load_module("ietf-ip", None) - -node = None -try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/ietf-interfaces.startup", ly.LYD_LYB, ly.LYD_OPT_CONFIG) -except Exception as e: - print(e) -try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/ietf-interfaces.startup", ly.LYD_XML, ly.LYD_OPT_CONFIG) -except Exception as e: - print(e) -try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/ietf-interfaces.startup", ly.LYD_JSON, ly.LYD_OPT_CONFIG) -except Exception as e: - print(e) - -if node is None: - sys.exit() - -node_set = node.find_path("/ietf-interfaces:interfaces//*") -if node_set is None: - print("could not find data for xpath") - sys.exit() - -for data_set in node_set.data(): - schema = data_set.schema() - print("name: " + schema.name() + " type: " + str(schema.nodetype()) + " path: " + data_set.path()) - if (schema.nodetype() == ly.LYS_LIST): - if_list = schema.subtype() - print("list's key names are: " + if_list.keys_str()) diff --git a/swig/python/examples/xpath.py b/swig/python/examples/xpath.py deleted file mode 100644 index 2196f022f..000000000 --- a/swig/python/examples/xpath.py +++ /dev/null @@ -1,51 +0,0 @@ -__author__ = "Mislav Novakovic " -__copyright__ = "Copyright 2017, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -import yang as ly -import sys - -ctx = None -try: - ctx = ly.Context("/etc/sysrepo/yang") - - module = ctx.load_module("turing-machine", None) - if module is None: - print("module not loaded") - sys.exit() - - node = None - try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/turing-machine.startup", ly.LYD_LYB, ly.LYD_OPT_CONFIG) - except Exception as e: - print(e) - try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/turing-machine.startup", ly.LYD_XML, ly.LYD_OPT_CONFIG) - except Exception as e: - print(e) - try: - if node is None : node = ctx.parse_data_path("/etc/sysrepo/data/turing-machine.startup", ly.LYD_JSON, ly.LYD_OPT_CONFIG) - except Exception as e: - print(e) - - if node is None: - sys.exit() - - node_set = node.find_path("/turing-machine:turing-machine/transition-function/delta[label='left summand']/*") - if node_set is None: - print("could not find data for xpath") - sys.exit() - - for data_set in node_set.data(): - schema = data_set.schema() - print("name: " + schema.name() + " type: " + str(schema.nodetype()) + " path: " + data_set.path()) - -except Exception as e: - print(e) - errors = ly.get_ly_errors(ctx) - for err in errors: - print("err: %d" % err.err()) - print("vecode: %d" % err.vecode()) - print("errmsg: " + err.errmsg()) - print("errpath:" + err.errpath()) - print("errapptag:" + err.errapptag()) diff --git a/swig/python/run_python_test.sh b/swig/python/run_python_test.sh deleted file mode 100755 index 0df508c4f..000000000 --- a/swig/python/run_python_test.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh -# -# @file run_python_test.c -# @author: Mislav Novakovic -# @brief unit tests for functions from libyang.h header -# -# Copyright (C) 2018 Deutsche Telekom AG. -# -# Author: Mislav Novakovic -# -# This source code is licensed under BSD 3-Clause License (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://opensource.org/licenses/BSD-3-Clause -# - -if [ "$#" -ne 5 ] ; then - echo "Usage: $0 PATH PythonPath PythonExecutable Test" >&2 - exit 1 -fi -PTH="$1" -PYTHON_EXE="$3" -TEST_FILE="$4" -export PATH=$PTH:$PATH -export PYTHONPATH="$2" -export LIBYANG_EXTENSIONS_PLUGINS_DIR="$5/src/extensions" - -$PYTHON_EXE $TEST_FILE diff --git a/swig/python/tests/test_libyang.py b/swig/python/tests/test_libyang.py deleted file mode 100755 index 1061815f3..000000000 --- a/swig/python/tests/test_libyang.py +++ /dev/null @@ -1,561 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function - -__author__ = "Matija Amidzic " -__copyright__ = "Copyright 2018, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -# This source code is licensed under BSD 3-Clause License (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://opensource.org/licenses/BSD-3-Clause - -import yang as ly -import unittest -import sys - -import config - -class UnexpectedError(Exception): - """Exception raised for errors that are not expected. - - Attributes: - message -- explanation of the error - """ - - def __init__(self, message): - self.message = message - -class TestUM(unittest.TestCase): - def test_ly_ctx_new(self): - yang_folder1 = config.TESTS_DIR + "/data/files" - yang_folder2 = config.TESTS_DIR + "/data:" + config.TESTS_DIR + "/data/files" - - try: - # Tests - ctx = ly.Context(yang_folder1) - self.assertIsNotNone(ctx) - list = ctx.get_searchdirs() - self.assertIsNotNone(list) - self.assertEqual(1, len(list)) - - ctx = ly.Context(yang_folder2) - self.assertIsNotNone(ctx) - list = ctx.get_searchdirs() - self.assertIsNotNone(list) - self.assertEqual(2, len(list)) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_new_invalid(self): - yang_folder = "INVALID_PATH" - - try: - ctx = ly.Context(yang_folder) - raise UnexpectedError("exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_searchdirs(self): - yang_folder = config.TESTS_DIR + "/data/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - list = ctx.get_searchdirs() - self.assertEqual(1, len(list)) - self.assertEqual(yang_folder, list[0]) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_set_searchdir(self): - yang_folder = config.TESTS_DIR + "/data/files" - new_yang_folder = config.TESTS_DIR + "/schema/yin" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - list = ctx.get_searchdirs() - self.assertEqual(1, len(list)) - self.assertEqual(yang_folder, list[0]) - - ctx.set_searchdir(new_yang_folder) - list = ctx.get_searchdirs() - self.assertEqual(2, len(list)) - self.assertEqual(new_yang_folder, list[1]) - - ctx.unset_searchdirs(0) - list = ctx.get_searchdirs() - self.assertEqual(1, len(list)) - self.assertEqual(new_yang_folder, list[0]) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_set_searchdir_invalid(self): - yang_folder = config.TESTS_DIR + "/data/files" - new_yang_folder = "INVALID_PATH" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - ctx.set_searchdir(new_yang_folder) - raise UnexpectedError("exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_ctx_info(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - info = ctx.info() - self.assertIsNotNone(info) - self.assertEqual(ly.LYD_VAL_OK, info.validity()) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_load_module_invalid(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.load_module("invalid", None) - raise UnexpectedError("exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_ctx_load_get_module(self): - yang_folder = config.TESTS_DIR + "/api/files" - name1 = "a" - name2 = "b" - revision = "2016-03-01" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.get_module("invalid") - self.assertIsNone(module) - - # module needs to be loaded first - module = ctx.get_module(name1) - self.assertIsNone(module) - - module = ctx.load_module(name1) - self.assertIsNotNone(module) - self.assertEqual(name1, module.name(), "Module names don't match") - - module = ctx.load_module(name2, revision) - self.assertIsNotNone(module) - self.assertEqual(name2, module.name(), "Module names don't match") - self.assertEqual(revision, module.rev().date(), "Module revisions don't match") - - module = ctx.get_module(name2, "INVALID_REVISION") - self.assertIsNone(module) - - module = ctx.get_module(name1) - self.assertIsNotNone(module) - self.assertEqual(name1, module.name(), "Module names don't match") - - module = ctx.get_module(name2, revision) - self.assertIsNotNone(module) - self.assertEqual(name2, module.name(), "Module names don't match") - self.assertEqual(revision, module.rev().date(), "Module revisions don't match") - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_module_older(self): - yang_folder = config.TESTS_DIR + "/api/files" - name = "b" - revision = "2016-03-01" - revision_older = "2015-01-01" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.load_module("c") - self.assertIsNotNone(module) - self.assertEqual("c", module.name(), "Module names don't match") - - module = ctx.load_module(name, revision) - self.assertIsNotNone(module) - self.assertEqual(name, module.name(), "Module names don't match") - self.assertEqual(revision, module.rev().date(), "Module revisions don't match") - - module_older = ctx.get_module_older(module) - self.assertIsNotNone(module_older) - self.assertEqual(name, module_older.name(), "Module names don't match") - self.assertEqual(revision_older, module_older.rev().date(), "Module revisions don't match") - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_module_by_ns(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "a" - ns = "urn:a" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name(), "Module names don't match") - - # Tests - module = ctx.get_module_by_ns(ns) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name(), "Module names don't match") - - except Exception as e: - self.fail(e) - - def test_ly_ctx_clean(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "a" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name(), "Module names don't match") - - # Tests - # confirm module is loaded - module = ctx.get_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name(), "Module names don't match") - - ctx.clean() - - # confirm ctx is cleaned - module = ctx.get_module(module_name) - self.assertIsNone(module) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_module_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - yang_file = config.TESTS_DIR + "/api/files/b.yang" - module_name1 = "a" - module_name2 = "b" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - self.assertEqual(module_name1, module.name(), "Module names don't match") - - module = ctx.parse_module_path(yang_file, ly.LYS_IN_YANG) - self.assertIsNotNone(module) - self.assertEqual(module_name2, module.name(), "Module names don't match") - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_module_path_invalid(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.parse_module_path("INVALID_YANG_FILE", ly.LYS_IN_YANG) - raise UnexpectedError("exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_submodule(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - module_name = "a" - sub_name = "asub" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - - # Tests - submodule = ctx.get_submodule(module_name, None, sub_name, None) - self.assertIsNotNone(submodule) - self.assertEqual(sub_name, submodule.name(), "Module names don't match") - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_submodule2(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - config_file = config.TESTS_DIR + "/api/files/a.xml" - sub_name = "asub" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - self.assertIsNotNone(root.schema().module()) - - # Tests - submodule = ctx.get_submodule2(root.schema().module(), sub_name) - self.assertIsNotNone(submodule) - self.assertEqual(sub_name, submodule.name(), "Module names don't match") - - except Exception as e: - self.fail(e) - - def test_ly_ctx_find_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - yang_file = config.TESTS_DIR + "/api/files/b.yang" - schema_path1 = "/b:x/b:bubba" - schema_path2 = "/a:x/a:bubba" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - ctx.parse_module_path(yang_file, ly.LYS_IN_YANG) - set = ctx.find_path(schema_path1) - self.assertIsNotNone(set) - - ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - set = ctx.find_path(schema_path2) - self.assertIsNotNone(set) - ly.Set() - - except Exception as e: - self.fail(e) - - def test_ly_set(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - set = ly.Set() - self.assertIsNotNone(set) - self.assertEqual(0, set.number()) - - set.add(root.child().schema()) - self.assertEqual(1, set.number()) - - set.add(root.schema()) - self.assertEqual(2, set.number()) - - set.rm(root.schema()) - self.assertEqual(1, set.number()) - - set.add(root.schema()) - self.assertEqual(2, set.number()) - - set.rm_index(1) - self.assertEqual(1, set.number()) - - set.clean() - self.assertEqual(0, set.number()) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_new_ylpath(self): - yang_folder = config.TESTS_DIR + "/api/files" - path = config.TESTS_DIR + "/api/files/ylpath.xml" - - try: - ctx = ly.Context(yang_folder, path, ly.LYD_XML, 0) - self.assertIsNotNone(ctx) - list = ctx.get_searchdirs() - self.assertIsNotNone(list) - self.assertEqual(1, len(list)) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_new_ylmem(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - info = ctx.info() - self.assertIsNotNone(info) - self.assertEqual(ly.LYD_VAL_OK, info.validity()) - - mem = info.print_mem(ly.LYD_XML, 0) - self.assertIsNotNone(mem) - - new_ctx = ly.Context(yang_folder, ly.LYD_XML, mem, 0) - self.assertIsNotNone(new_ctx) - list = ctx.get_searchdirs() - self.assertIsNotNone(list) - self.assertEqual(1, len(list)) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_module_iter(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "a" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name()) - itr = ctx.get_module_iter() - self.assertIsNotNone(itr) - self.assertEqual(7, len(itr)) - - except Exception as e: - self.fail(e) - - - def test_ly_ctx_get_disabled_module_iter(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "x" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name()) - # FIXME no way to disable module from here - - itr = ctx.get_disabled_module_iter() - self.assertIsNotNone(itr) - self.assertEqual(0, len(itr)) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_data_instantiables(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "b" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name()) - - instantiables = ctx.data_instantiables(0) - self.assertIsNotNone(instantiables) - self.assertEqual(5, len(instantiables)) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_get_node(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "b" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name()) - - instantiables = ctx.data_instantiables(0) - self.assertIsNotNone(instantiables) - self.assertEqual(5, len(instantiables)) - - schema = instantiables[0]; - self.assertIsNotNone(schema) - node = ctx.get_node(schema, "/b:x/b:bubba", 0) - self.assertIsNotNone(node) - - except Exception as e: - self.fail(e) - -if __name__ == '__main__': - unittest.main() diff --git a/swig/python/tests/test_tree_data.py b/swig/python/tests/test_tree_data.py deleted file mode 100755 index 531a4b03e..000000000 --- a/swig/python/tests/test_tree_data.py +++ /dev/null @@ -1,747 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function - -__author__ = "Matija Amidzic " -__copyright__ = "Copyright 2018, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -# This source code is licensed under BSD 3-Clause License (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://opensource.org/licenses/BSD-3-Clause - -import yang as ly -import unittest -import sys - -import config - -lys_module_a = \ -" \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -" - -a_data_xml = "\ -\n\ - test\n\ - \n" - -result_xml = "test" - -result_xml_format ="\ -\n\ - test\n\ -\n\ -" - -result_json = "\ -{\n\ - \"a:x\": {\n\ - \"bubba\": \"test\"\n\ - }\n\ -}\n\ -" - -class UnexpectedError(Exception): - """Exception raised for errors that are not expected. - - Attributes: - message -- explanation of the error - """ - - def __init__(self, message): - self.message = message - -class TestUM(unittest.TestCase): - def test_ly_ctx_parse_data_mem(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - - # Tests - root = ctx.parse_data_mem(a_data_xml, ly.LYD_XML, ly.LYD_OPT_NOSIBLINGS | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - self.assertEqual("x", root.schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_data_fd(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - - # Tests - f = open(config_file, 'r') - fd = f.fileno() - root = ctx.parse_data_fd(fd, ly.LYD_XML, ly.LYD_OPT_NOSIBLINGS | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - self.assertEqual("x", root.schema().name()) - - except Exception as e: - self.fail(e) - - finally: - f.close() - - def test_ly_ctx_parse_data_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - config_file = config.TESTS_DIR + "/api/files/a.xml" - module_name = "a" - schema_name = "x" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name(), "Module names don't match") - - # Tests - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - self.assertEqual(schema_name, root.schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_data_path_invalid(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - root = ctx.parse_data_path("INVALID_PATH", ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - raise UnexpectedError("exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_data_node(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - new_node = ly.Data_Node(root, root.schema().module(), "number32", "100") - self.assertIsNotNone(new_node) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_new_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - mod = ctx.get_module("a", None, 1) - self.assertIsNotNone(mod) - - # Tests - root = ly.Data_Node(ctx, "/a:x/bar-gggg", "a", 0, 0) - self.assertIsNotNone(root) - self.assertEqual("x", root.schema().name()) - self.assertEqual("bar-gggg", root.child().schema().name()) - - node = root.new_path(ctx, "def-leaf", "def", 0, ly.LYD_PATH_OPT_DFLT) - self.assertIsNotNone(node) - self.assertEqual("def-leaf", node.schema().name()) - self.assertEqual(1, node.dflt()) - - node = root.new_path(ctx, "def-leaf", "def", 0, 0) - self.assertIsNotNone(node) - self.assertEqual("def-leaf", node.schema().name()) - self.assertEqual(0, node.dflt()) - - node = root.new_path(ctx, "bubba", "b", 0, 0) - self.assertIsNotNone(node) - self.assertEqual("bubba", node.schema().name()) - - node = root.new_path(ctx, "/a:x/number32", "3", 0, 0) - self.assertIsNotNone(node) - self.assertEqual("number32", node.schema().name()) - - node = root.new_path(ctx, "/a:l[key1='1'][key2='2']/value", None, 0, 0) - self.assertIsNotNone(node) - self.assertEqual("l", node.schema().name()) - self.assertEqual("key1", node.child().schema().name()) - self.assertEqual("key2", node.child().next().schema().name()) - self.assertEqual("value", node.child().next().next().schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_insert(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - new_node = ly.Data_Node(root, root.schema().module(), "number32", "200") - self.assertIsNotNone(new_node) - rc = root.insert(new_node) - self.assertEqual(0, rc) - self.assertEqual("number32", root.child().prev().schema().name()) - - - except Exception as e: - self.fail(e) - - def test_ly_data_node_insert_sibling(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - last = root.prev() - new_node = ly.Data_Node(None, root.schema().module(), "y", "test") - self.assertIsNotNone(new_node) - rc = root.insert_sibling(new_node) - self.assertEqual(0, rc) - self.assertNotEqual(last.schema().name(), root.prev().schema().name()) - self.assertEqual("y", root.prev().schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_insert_before(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - last = root.prev() - new_node = ly.Data_Node(None, root.schema().module(), "y", "test") - self.assertIsNotNone(new_node) - rc = root.insert_before(new_node) - self.assertEqual(0, rc) - self.assertNotEqual(last.schema().name(), root.prev().schema().name()) - self.assertEqual("y", root.prev().schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_insert_after(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - last = root.next() - new_node = ly.Data_Node(None, root.schema().module(), "y", "test") - self.assertIsNotNone(new_node) - rc = root.insert_after(new_node) - self.assertEqual(0, rc) - self.assertNotEqual(last.schema().name(), root.next().schema().name()) - self.assertEqual("y", root.next().schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_schema_sort(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - mod = ctx.get_module("a", None, 1) - self.assertIsNotNone(mod) - - # Tests - root = ly.Data_Node(None, mod, "l") - self.assertIsNotNone(root) - node = ly.Data_Node(root, mod, "key1", "1") - self.assertIsNotNone(node) - node = ly.Data_Node(root, mod, "key2", "2") - self.assertIsNotNone(node) - - node = ly.Data_Node(None, mod, "x") - self.assertIsNotNone(node) - rc = root.insert_after(node) - self.assertEqual(0, rc) - node = root.next() - - node2 = ly.Data_Node(node, mod, "bubba", "a") - self.assertIsNotNone(node2) - node2 = ly.Data_Node(node, mod, "bar-gggg", "b") - self.assertIsNotNone(node2) - node2 = ly.Data_Node(node, mod, "number64", "64") - self.assertIsNotNone(node2) - node2 = ly.Data_Node(node, mod, "number32", "32") - self.assertIsNotNone(node2) - - rc = root.schema_sort(1) - self.assertEqual(0, rc) - - root = node - self.assertEqual("x", root.schema().name()) - self.assertEqual("l", root.next().schema().name()) - - self.assertEqual("bar-gggg", root.child().schema().name()) - self.assertEqual("bubba", root.child().next().schema().name()) - self.assertEqual("number32", root.child().next().next().schema().name()) - self.assertEqual("number64", root.child().next().next().next().schema().name()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_find_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - node = root.child() - self.assertIsNotNone(node) - set = node.find_path("/a:x/bubba") - self.assertIsNotNone(set) - self.assertEqual(1, set.number()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_find_instance(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - node = root.child() - self.assertIsNotNone(node) - set = node.find_instance(node.schema()) - self.assertIsNotNone(set) - self.assertEqual(1, set.number()) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_validate(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - rc = root.validate(ly.LYD_OPT_CONFIG, ctx) - self.assertEqual(0, rc) - new = ly.Data_Node(root, root.schema().module(), "number32", "1") - self.assertIsNotNone(new) - rc = root.insert(new) - self.assertEqual(0, rc) - rc = root.validate(ly.LYD_OPT_CONFIG, ctx) - self.assertEqual(0, rc) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_unlink(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - node = root.child() - new = ly.Data_Node(root, node.schema().module(), "number32", "1") - self.assertIsNotNone(new) - rc = root.insert(new) - self.assertEqual(0, rc) - - schema = node.prev().schema() - if (ly.LYS_LEAF == schema.nodetype() or ly.LYS_LEAFLIST == schema.nodetype()): - casted = node.prev().subtype() - self.assertEqual("1", casted.value_str()) - else: - self.fail() - - rc = node.prev().unlink() - self.assertEqual(0, rc) - schema = node.prev().schema() - if (ly.LYS_LEAF == schema.nodetype() or ly.LYS_LEAFLIST == schema.nodetype()): - self.fail() - else: - return - - except Exception as e: - self.fail(e) - - def test_ly_data_node_print_mem_xml(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - result = root.print_mem(ly.LYD_XML, 0) - self.assertEqual(result_xml, result) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_print_mem_xml_format(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - result = root.print_mem(ly.LYD_XML, ly.LYP_FORMAT) - self.assertEqual(result_xml_format, result) - - - except Exception as e: - self.fail(e) - - def test_ly_data_node_print_mem_json(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - result = root.print_mem(ly.LYD_JSON, ly.LYP_FORMAT) - self.assertEqual(result_json, result) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - # Tests - str = root.path() - self.assertIsNotNone(str) - self.assertEqual("/a:x", str) - str = root.child().path() - self.assertIsNotNone(str) - self.assertEqual("/a:x/bubba", str) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_leaf(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - new_node = ly.Data_Node(root, root.schema().module(), "number32", "100") - self.assertIsNotNone(new_node) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_anydata(self): - yang_folder = config.TESTS_DIR + "/api/files" - config_file = config.TESTS_DIR + "/api/files/a.xml" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - mod = ctx.get_module("a", None, 1) - - new_node = ly.Data_Node(root, mod, "any-data", "100", ly.LYD_ANYDATA_CONSTSTRING) - self.assertIsNotNone(new_node) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_dup(self): - yang_folder = config.TESTS_DIR + "/api/files"; - config_file = config.TESTS_DIR + "/api/files/a.xml"; - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - new_node = ly.Data_Node(root, root.child().schema().module(), "bar-y") - self.assertIsNotNone(new_node) - dup_node = new_node.dup(0); - self.assertIsNotNone(dup_node) - - except Exception as e: - self.fail(e) - - - def test_ly_data_node_dup_to_ctx(self): - sch = "module x {\ - namespace urn:x;\ - prefix x;\ - leaf x { type string; }}" - data = "hello" - - try: - ctx1 = ly.Context(None) - self.assertIsNotNone(ctx1); - ctx1.parse_module_mem(sch, ly.LYS_IN_YANG) - data1 = ctx1.parse_data_mem(data, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(data1) - - ctx2 = ly.Context(None) - self.assertIsNotNone(ctx2) - # we expect NULL due to missing schema in the second ctx - dup_node = data1.dup_to_ctx(1, ctx2) - self.assertIsNone(dup_node) - - ctx2.parse_module_mem(sch, ly.LYS_IN_YANG) - # now we expect success due to schema being added to the second ctx - dup_node = data1.dup_to_ctx(1, ctx2) - self.assertIsNotNone(dup_node) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_validate_node(self): - yang_folder = config.TESTS_DIR + "/api/files"; - config_file = config.TESTS_DIR + "/api/files/a.xml"; - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - rc = root.validate(ly.LYD_OPT_CONFIG, ctx) - self.assertEqual(0, rc) - new_node = ly.Data_Node(root, root.schema().module(), "number32", "1") - self.assertIsNotNone(new_node) - rc = root.validate(ly.LYD_OPT_CONFIG, new_node) - self.assertEqual(0, rc) - - except Exception as e: - self.fail(e) - - def test_ly_data_node_validate_value(self): - yang_folder = config.TESTS_DIR + "/api/files"; - config_file = config.TESTS_DIR + "/api/files/a.xml"; - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - root = ctx.parse_data_path(config_file, ly.LYD_XML, ly.LYD_OPT_CONFIG | ly.LYD_OPT_STRICT) - self.assertIsNotNone(root) - - rc = root.validate(ly.LYD_OPT_CONFIG, ctx) - self.assertEqual(0, rc) - new_node = ly.Data_Node(root, root.schema().module(), "number32", "1") - self.assertIsNotNone(new_node) - self.assertEqual(0, new_node.validate_value("1")) - self.assertEqual(0, new_node.validate_value("100")) - self.assertEqual(0, new_node.validate_value("110000000")) - - except Exception as e: - self.fail(e) - -if __name__ == '__main__': - unittest.main() diff --git a/swig/python/tests/test_tree_schema.py b/swig/python/tests/test_tree_schema.py deleted file mode 100755 index 8a1188537..000000000 --- a/swig/python/tests/test_tree_schema.py +++ /dev/null @@ -1,591 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function - -__author__ = "Matija Amidzic " -__copyright__ = "Copyright 2018, Deutsche Telekom AG" -__license__ = "BSD 3-Clause" - -# This source code is licensed under BSD 3-Clause License (the "License"). -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://opensource.org/licenses/BSD-3-Clause - -import yang as ly -import unittest -import sys - -import config - -lys_module_a = \ -" \ - \ - \ - \ - \ - \ - \ - \ - version 1 \ - \ - \ - RFC XXXX \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -" - -lys_module_b = \ -"module b {\ - namespace \"urn:b\";\ - prefix b_mod;\ - include bsub;\ - include btop;\ - feature foo;\ - grouping gg {\ - leaf bar-gggg {\ - type string;\ - }\ - }\ - container x {\ - leaf bar-leaf {\ - if-feature \"bar\";\ - type string;\ - }\ - uses gg {\ - if-feature \"bar\";\ - }\ - leaf baz {\ - if-feature \"foo\";\ - type string;\ - }\ - leaf bubba {\ - type string;\ - }\ - }\ - augment \"/x\" {\ - if-feature \"bar\";\ - container bar-y;\ - }\ - rpc bar-rpc {\ - if-feature \"bar\";\ - }\ - rpc foo-rpc {\ - if-feature \"foo\";\ - }\ -}" - -lys_module_a_with_typo = \ -" \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -" - -result_tree = "\ -module: a\n\ - +--rw top\n\ - | +--rw bar-sub2\n\ - +--rw x\n\ - +--rw bubba? string\n" - -result_yang = "\ -module a {\n\ - namespace \"urn:a\";\n\ - prefix a_mod;\n\ -\n\ - include \"asub\";\n\ -\n\ - include \"atop\";\n\ -\n\ - revision 2015-01-01 {\n\ - description\n\ - \"version 1\";\n\ - reference\n\ - \"RFC XXXX\";\n\ - }\n\ -\n\ - feature foo;\n\ -\n\ - grouping gg {\n\ - leaf bar-gggg {\n\ - type string;\n\ - }\n\ - }\n\ -\n\ - container x {\n\ - leaf bar-leaf {\n\ - if-feature \"bar\";\n\ - type string;\n\ - }\n\n\ - uses gg {\n\ - if-feature \"bar\";\n\ - }\n\n\ - leaf baz {\n\ - if-feature \"foo\";\n\ - type string;\n\ - }\n\n\ - leaf bubba {\n\ - type string;\n\ - }\n\ - }\n\ -\n\ - augment \"/x\" {\n\ - if-feature \"bar\";\n\ - container bar-y {\n\ - leaf ll {\n\ - type string;\n\ - }\n\ - }\n\ - }\n\ -\n\ - rpc bar-rpc {\n\ - if-feature \"bar\";\n\ - }\n\ -\n\ - rpc foo-rpc {\n\ - if-feature \"foo\";\n\ - }\n\ -}\n" - -result_yin = "\ -\n\ -\n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - version 1\n\ - \n\ - \n\ - RFC XXXX\n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ - \n\ -\n" - -result_info ="\ -Feature: foo\n\ -Module: a\n\ -Desc: \n\ -Reference: \n\ -Status: current\n\ -Enabled: no\n\ -If-feats: \n" - -class UnexpectedError(Exception): - """Exception raised for errors that are not expected. - - Attributes: - message -- explanation of the error - """ - - def __init__(self, message): - self.message = message - -class TestUM(unittest.TestCase): - def test_ly_ctx_parse_module_mem(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - self.assertEqual("a", module.name()) - - module = ctx.parse_module_mem(lys_module_b, ly.LYS_IN_YANG) - self.assertIsNotNone(module) - self.assertEqual("b", module.name()) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_module_mem_invalid(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - ctx.parse_module_mem(lys_module_a_with_typo, ly.LYS_IN_YIN) - raise UnexpectedError("Exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_module_fd(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - yang_file = config.TESTS_DIR + "/api/files/b.yang" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - f = open(yin_file, 'r') - fd = f.fileno() - module = ctx.parse_module_fd(fd, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - self.assertEqual("a", module.name()) - - f.close() - f = open(yang_file, 'r') - fd = f.fileno() - module = ctx.parse_module_fd(fd, ly.LYS_IN_YANG) - self.assertIsNotNone(module) - self.assertEqual("b", module.name()) - - except Exception as e: - self.fail(e) - - finally: - f.close() - - def test_ly_ctx_parse_module_fd_invalid(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - f = open(yin_file, 'r') - fd = f.fileno() - # parsing with wrong format should raise runtime exception - module = ctx.parse_module_fd(fd, ly.LYS_IN_YANG) - raise UnexpectedError("Exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - finally: - f.close() - - def test_ly_ctx_parse_module_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - yang_file = config.TESTS_DIR + "/api/files/b.yang" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - module = ctx.parse_module_path(yin_file, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - self.assertEqual("a", module.name()) - - module = ctx.parse_module_path(yang_file, ly.LYS_IN_YANG) - self.assertIsNotNone(module) - self.assertEqual("b", module.name()) - - except Exception as e: - self.fail(e) - - def test_ly_ctx_parse_module_path_invalid(self): - yang_folder = config.TESTS_DIR + "/api/files" - yin_file = config.TESTS_DIR + "/api/files/a.yin" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - # Tests - # parsing with wrong format should raise runtime exception - module = ctx.parse_module_path(yin_file, ly.LYS_IN_YANG) - raise UnexpectedError("Exception not thrown") - - except UnexpectedError as e: - self.fail(e) - - except RuntimeError as e: - return - - except Exception as e: - self.fail(e) - - def test_ly_module_print_mem_tree(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - - # Tests - result = module.print_mem(ly.LYS_OUT_TREE, 0) - self.assertEqual(result_tree, result) - - except Exception as e: - self.fail(e) - - def test_ly_module_print_mem_yang(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - - # Tests - result = module.print_mem(ly.LYS_OUT_YANG, 0) - self.assertEqual(result_yang, result) - - except Exception as e: - self.fail(e) - - def test_ly_module_print_mem_yin(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - - # Tests - result = module.print_mem(ly.LYS_OUT_YIN, 0) - self.assertEqual(result_yin, result) - - except Exception as e: - self.fail(e) - - def test_ly_schema_node_find_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - schema_node = module.data() - self.assertIsNotNone(schema_node) - - # Tests - set = schema_node.find_path("/a:x/*") - self.assertIsNotNone(set) - self.assertEqual(5, set.number()) - set = schema_node.find_path("/a:x//*") - self.assertIsNotNone(set) - self.assertEqual(6, set.number()) - set = schema_node.find_path("/a:x//.") - self.assertIsNotNone(set) - self.assertEqual(7, set.number()) - - except Exception as e: - self.fail(e) - - def test_ly_schema_node_path(self): - yang_folder = config.TESTS_DIR + "/api/files" - - try: - # Setup - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - module = ctx.parse_module_mem(lys_module_a, ly.LYS_IN_YIN) - self.assertIsNotNone(module) - schema_node = module.data() - self.assertIsNotNone(schema_node) - - # Tests - template = "/a:x/a:bar-gggg" - set = schema_node.find_path(template) - self.assertIsNotNone(set) - schema = set.schema()[0] - path = schema.path(0) - self.assertEqual(template, path) - - except Exception as e: - self.fail(e) - - def test_ly_module_data_instatiables(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "b" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name()) - - instantiables = module.data_instantiables(0) - self.assertIsNotNone(instantiables) - self.assertEqual(1, len(instantiables)) - - except Exception as e: - self.fail(e) - - def test_ly_schema_child_instatiables(self): - yang_folder = config.TESTS_DIR + "/api/files" - module_name = "b" - - try: - ctx = ly.Context(yang_folder) - self.assertIsNotNone(ctx) - - module = ctx.load_module(module_name) - self.assertIsNotNone(module) - self.assertEqual(module_name, module.name()) - - instantiables = module.data_instantiables(0) - self.assertIsNotNone(instantiables) - self.assertEqual(1, len(instantiables)) - child_instantiables = instantiables[0].child_instantiables(0) - self.assertIsNotNone(child_instantiables) - self.assertEqual(3, len(child_instantiables)) - - except Exception as e: - self.fail(e) - -if __name__ == '__main__': - unittest.main() diff --git a/swig/python/yang.i b/swig/python/yang.i deleted file mode 100644 index 32e39deab..000000000 --- a/swig/python/yang.i +++ /dev/null @@ -1,166 +0,0 @@ -%module yang - -%{ - extern "C" { - #include "libyang.h" - #include "tree_data.h" - #include "tree_schema.h" - } -%} - -%include exception.i -%include -%include -%include -%catches(std::runtime_error, std::exception, std::string); - -%inline %{ -#include -#include "libyang.h" -#include -#include -#include - -#include "Libyang.hpp" -#include "Tree_Data.hpp" - -class Wrap_cb { -public: - Wrap_cb(PyObject *callback): _callback(nullptr) { - - if (!PyCallable_Check(callback)) { - throw std::runtime_error("Python Object is not callable.\n"); - } - else { - _callback = callback; - Py_XINCREF(_callback); - } - } - ~Wrap_cb() { - if(_callback) - Py_XDECREF(_callback); - } - - - std::pair ly_module_imp_clb(const char *mod_name, const char *mod_rev, const char *submod_name, const char *sub_rev, PyObject *user_data) { - PyObject *arglist = Py_BuildValue("(ssssO)", mod_name, mod_rev, submod_name, sub_rev, user_data); - PyObject *my_result = PyEval_CallObject(_callback, arglist); - Py_DECREF(arglist); - if (my_result == nullptr) { - throw std::runtime_error("Python callback ly_module_imp_clb failed.\n"); - } else { - LYS_INFORMAT format; - char *data; - - if (!PyArg_ParseTuple(my_result, "is", &format, &data)) { - Py_DECREF(my_result); - std::runtime_error("failed to parse ly_module_imp_clb"); - } - - Py_DECREF(my_result); - return std::make_pair(data,format); - } - } - - PyObject *private_ctx; -private: - PyObject *_callback; -}; - -static const char *g_ly_module_imp_clb(const char *mod_name, const char *mod_rev, const char *submod_name, const char *sub_rev, - void *user_data, LYS_INFORMAT *format, void (**free_module_data)(void *model_data, void *user_data)) { - Wrap_cb *ctx = (Wrap_cb *) user_data; - (void)free_module_data; - auto pair = ctx->ly_module_imp_clb(mod_name, mod_rev, submod_name, sub_rev, ctx->private_ctx); - *format = pair.second; - return pair.first; -} -%} - -%extend libyang::Context { - - void set_module_imp_clb(PyObject *clb, PyObject *user_data = nullptr) { - /* create class */ - Wrap_cb *class_ctx = nullptr; - class_ctx = new Wrap_cb(clb); - - self->wrap_cb_l.push_back(class_ctx); - if (user_data) { - class_ctx->private_ctx = user_data; - } else { - Py_INCREF(Py_None); - class_ctx->private_ctx = Py_None; - } - - ly_ctx_set_module_imp_clb(self->swig_ctx(), g_ly_module_imp_clb, class_ctx); - }; -} - -%extend libyang::Data_Node { - PyObject *subtype() { - PyObject *casted = 0; - - auto type = self->swig_node()->schema->nodetype; - if (LYS_LEAF == type || LYS_LEAFLIST == type) { - auto node_leaf_list = new std::shared_ptr(new libyang::Data_Node_Leaf_List(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node_leaf_list), SWIGTYPE_p_std__shared_ptrT_libyang__Data_Node_Leaf_List_t, SWIG_POINTER_OWN); - } else if (LYS_ANYDATA == type || LYS_ANYXML == type) { - auto node_anydata = new std::shared_ptr(new libyang::Data_Node_Anydata(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node_anydata), SWIGTYPE_p_std__shared_ptrT_libyang__Data_Node_Anydata_t, SWIG_POINTER_OWN); - } else { - auto node = new std::shared_ptr(new libyang::Data_Node(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Data_Node_t, SWIG_POINTER_OWN); - } - - return casted; - } -}; - -%extend libyang::Schema_Node { - PyObject *subtype() { - PyObject *casted = 0; - - auto type = self->swig_node()->nodetype; - if (LYS_CONTAINER == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Container(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Container_t, SWIG_POINTER_OWN); - } else if (LYS_CHOICE == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Choice(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Choice_t, SWIG_POINTER_OWN); - } else if (LYS_LEAF == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Leaf(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Leaf_t, SWIG_POINTER_OWN); - } else if (LYS_LEAFLIST == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Leaflist(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Leaflist_t, SWIG_POINTER_OWN); - } else if (LYS_LIST == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_List(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_List_t, SWIG_POINTER_OWN); - } else if (LYS_ANYDATA == type || LYS_ANYXML == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Anydata(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Anydata_t, SWIG_POINTER_OWN); - } else if (LYS_USES == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Uses(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Uses_t, SWIG_POINTER_OWN); - } else if (LYS_GROUPING == type || LYS_RPC == type || LYS_ACTION == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Grp(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Grp_t, SWIG_POINTER_OWN); - } else if (LYS_CASE == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Case(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Case_t, SWIG_POINTER_OWN); - } else if (LYS_INPUT == type || LYS_OUTPUT == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Inout(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Inout_t, SWIG_POINTER_OWN); - } else if (LYS_NOTIF == type) { - auto node = new std::shared_ptr(new libyang::Schema_Node_Notif(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_Notif_t, SWIG_POINTER_OWN); - } else { - auto node = new std::shared_ptr(new libyang::Schema_Node(self->swig_node(), self->swig_deleter())); - casted = SWIG_NewPointerObj(SWIG_as_voidptr(node), SWIGTYPE_p_std__shared_ptrT_libyang__Schema_Node_t, SWIG_POINTER_OWN); - } - - return casted; - } -}; - -%include "../swig_base/python_base.i" diff --git a/swig/swig_base/python_base.i b/swig/swig_base/python_base.i deleted file mode 100644 index 59f877e2a..000000000 --- a/swig/swig_base/python_base.i +++ /dev/null @@ -1,4 +0,0 @@ -%module python_base - -%include "../swig_base/base.i" -%include "../swig_base/libyangEnums.i"