diff --git a/.editorconfig b/.editorconfig index b5343938..37f96c4f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,13 +21,13 @@ indent_style = space indent_size = 3 [Makefile] -indent_style = tabs +indent_style = tab indent_size = tab [*.sh] -indent_style = tabs +indent_style = tab indent_size = tab [*.{h,c}] -indent_style = tabs +indent_style = tab indent_size = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed936040..2177a27b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,19 @@ --- name: CI -on: [push, pull_request] +on: + pull_request: + branches: + - master + push: + branches: + - master + tags: + - v* jobs: lint: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -22,28 +30,41 @@ jobs: - run: python -m pip install --upgrade tox - run: python -m tox -e lint + check-commits: + if: ${{ github.event.pull_request.commits }} + runs-on: ubuntu-24.04 + env: + LYPY_COMMIT_RANGE: "HEAD~${{ github.event.pull_request.commits }}.." + steps: + - run: sudo apt-get install git make jq curl + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - run: make check-commits + test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: include: - - python: 3.6 - toxenv: py36 - - python: 3.7 - toxenv: py37 - - python: 3.8 - toxenv: py38 - - python: 3.9 + - python: "3.9" toxenv: py39 - python: "3.10" toxenv: py310 + - python: "3.11" + toxenv: py311 + - python: "3.12" + toxenv: py312 + - python: "3.13" + toxenv: py313 - python: pypy3.9 toxenv: pypy3 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: "${{ matrix.python }}" - uses: actions/cache@v3 with: path: ~/.cache/pip @@ -73,7 +94,7 @@ jobs: deploy: needs: [lint, test] if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/Makefile b/Makefile index fcc52d83..fc9a6ea3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2020 Robin Jarry +# Copyright (c) 2018-2023 Robin Jarry # SPDX-License-Identifier: MIT all: lint tests @@ -7,9 +7,14 @@ lint: tox -e lint tests: - tox -e py37 + tox -e py3 format: tox -e format -.PHONY: lint tests format +LYPY_COMMIT_RANGE ?= origin/master.. + +check-commits: + ./check-commits.sh $(LYPY_COMMIT_RANGE) + +.PHONY: lint tests format check-commits diff --git a/README.rst b/README.rst index 8caf69e5..4ea8977f 100644 --- a/README.rst +++ b/README.rst @@ -185,7 +185,7 @@ Data Tree ... {'name': 'lo', 'address': '127.0.0.1'}, ... ], ... }, - ... }, config=True) + ... }) >>> print(node.print_mem('xml', pretty=True)) @@ -232,7 +232,7 @@ Here are the steps for submitting a change in the code base: #. Create a new branch named after what your are working on:: - git checkout -b my-topic + git checkout -b my-topic -t origin/master #. Edit the code and call ``make format`` to ensure your modifications comply with the `coding style`__. @@ -251,21 +251,60 @@ Here are the steps for submitting a change in the code base: your changes do not break anything. You can also run ``make`` which will run both. -#. Create commits by following these simple guidelines: - - - Solve only one problem per commit. - - Use a short (less than 72 characters) title on the first line followed by - an blank line and a more thorough description body. - - Wrap the body of the commit message should be wrapped at 72 characters too - unless it breaks long URLs or code examples. - - If the commit fixes a Github issue, include the following line:: - - Fixes: #NNNN - - Inspirations: - - https://chris.beams.io/posts/git-commit/ - https://wiki.openstack.org/wiki/GitCommitMessages +#. Once you are happy with your work, you can create a commit (or several + commits). Follow these general rules: + + - Address only one issue/topic per commit. + - Describe your changes in imperative mood, e.g. *"make xyzzy do frotz"* + instead of *"[This patch] makes xyzzy do frotz"* or *"[I] changed xyzzy to + do frotz"*, as if you are giving orders to the codebase to change its + behaviour. + - Limit the first line (title) of the commit message to 60 characters. + - Use a short prefix for the commit title for readability with ``git log + --oneline``. Do not use the `fix:` nor `feature:` prefixes. See recent + commits for inspiration. + - Only use lower case letters for the commit title except when quoting + symbols or known acronyms. + - Use the body of the commit message to actually explain what your patch + does and why it is useful. Even if your patch is a one line fix, the + description is not limited in length and may span over multiple + paragraphs. Use proper English syntax, grammar and punctuation. + - If you are fixing an issue, use appropriate ``Closes: `` or + ``Fixes: `` trailers. + - If you are fixing a regression introduced by another commit, add a + ``Fixes: ("")`` trailer. + - When in doubt, follow the format and layout of the recent existing + commits. + - The following trailers are accepted in commits. If you are using multiple + trailers in a commit, it's preferred to also order them according to this + list. + + * ``Closes: <URL>``: close the referenced issue or pull request. + * ``Fixes: <SHA> ("<TITLE>")``: reference the commit that introduced + a regression. + * ``Link: <URL>``: any useful link to provide context for your commit. + * ``Suggested-by`` + * ``Requested-by`` + * ``Reported-by`` + * ``Co-authored-by`` + * ``Tested-by`` + * ``Reviewed-by`` + * ``Acked-by`` + * ``Signed-off-by``: Compulsory! + + There is a great reference for commit messages in the `Linux kernel + documentation`__. + + __ https://www.kernel.org/doc/html/latest/process/submitting-patches.html#describe-your-changes + + IMPORTANT: you must sign-off your work using ``git commit --signoff``. Follow + the `Linux kernel developer's certificate of origin`__ for more details. All + contributions are made under the MIT license. If you do not want to disclose + your real name, you may sign-off using a pseudonym. Here is an example:: + + Signed-off-by: Robin Jarry <robin@jarry.cc> + + __ https://www.kernel.org/doc/html/latest/process/submitting-patches.html#sign-your-work-the-developer-s-certificate-of-origin #. Push your topic branch in your forked repository:: diff --git a/cffi/cdefs.h b/cffi/cdefs.h index 0ccb9772..82ec9748 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -14,6 +14,10 @@ struct ly_ctx; #define LY_CTX_EXPLICIT_COMPILE ... #define LY_CTX_REF_IMPLEMENTED ... #define LY_CTX_SET_PRIV_PARSED ... +#define LY_CTX_LEAFREF_EXTENDED ... +#define LY_CTX_LEAFREF_LINKING ... +#define LY_CTX_BUILTIN_PLUGINS_ONLY ... +#define LY_CTX_COMPILE_OBSOLETE ... typedef enum { @@ -171,17 +175,22 @@ enum ly_stmt { LY_STMT_ARG_VALUE }; +#define LY_STMT_OP_MASK ... +#define LY_STMT_DATA_NODE_MASK ... +#define LY_STMT_NODE_MASK ... + #define LY_LOLOG ... #define LY_LOSTORE ... #define LY_LOSTORE_LAST ... int ly_log_options(int); +uint32_t *ly_temp_log_options(uint32_t *); LY_LOG_LEVEL ly_log_level(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 *); +extern "Python" void lypy_log_cb(LY_LOG_LEVEL, const char *, const char *, const char *, uint64_t); +void ly_set_log_clb(void (*)(LY_LOG_LEVEL, const char *, const char *, const char *, uint64_t)); +const struct ly_err_item *ly_err_first(const struct ly_ctx *); +const struct ly_err_item *ly_err_last(const struct ly_ctx *); void ly_err_clean(struct ly_ctx *, struct ly_err_item *); -LY_VECODE ly_vecode(const struct ly_ctx *); #define LYS_UNKNOWN ... #define LYS_CONTAINER ... @@ -208,6 +217,7 @@ struct lys_module* ly_ctx_get_module_latest(const struct ly_ctx *, const char *) LY_ERR ly_ctx_compile(struct ly_ctx *); LY_ERR lys_find_xpath(const struct ly_ctx *, const struct lysc_node *, const char *, uint32_t, struct ly_set **); +LY_ERR lys_find_xpath_atoms(const struct ly_ctx *, const struct lysc_node *, const char *, uint32_t, struct ly_set **); void ly_set_free(struct ly_set *, void(*)(void *obj)); struct ly_set { @@ -237,14 +247,15 @@ struct lysc_node { struct ly_err_item { LY_LOG_LEVEL level; - LY_ERR no; + LY_ERR err; LY_VECODE vecode; char *msg; - char *path; + char *data_path; + char *schema_path; + uint64_t line; char *apptag; struct ly_err_item *next; struct ly_err_item *prev; - ...; }; struct lyd_node { @@ -260,13 +271,17 @@ struct lyd_node { LY_ERR lys_set_implemented(struct lys_module *, const char **); +#define LYD_NEW_VAL_OUTPUT ... +#define LYD_NEW_VAL_STORE_ONLY ... +#define LYD_NEW_VAL_BIN ... +#define LYD_NEW_VAL_CANON ... +#define LYD_NEW_META_CLEAR_DFLT ... #define LYD_NEW_PATH_UPDATE ... -#define LYD_NEW_PATH_OUTPUT ... -#define LYD_NEW_PATH_OPAQ ... -#define LYD_NEW_PATH_BIN_VALUE ... -#define LYD_NEW_PATH_CANON_VALUE ... +#define LYD_NEW_PATH_OPAQ ... LY_ERR lyd_new_path(struct lyd_node *, const struct ly_ctx *, const char *, const char *, uint32_t, struct lyd_node **); LY_ERR lyd_find_xpath(const struct lyd_node *, const char *, struct ly_set **); +void lyd_unlink_siblings(struct lyd_node *node); +void lyd_unlink_tree(struct lyd_node *node); void lyd_free_all(struct lyd_node *node); void lyd_free_tree(struct lyd_node *node); @@ -284,33 +299,39 @@ enum lyd_type { LYD_TYPE_REPLY_YANG, LYD_TYPE_RPC_NETCONF, LYD_TYPE_NOTIF_NETCONF, - LYD_TYPE_REPLY_NETCONF + LYD_TYPE_REPLY_NETCONF, + LYD_TYPE_RPC_RESTCONF, + LYD_TYPE_NOTIF_RESTCONF, + LYD_TYPE_REPLY_RESTCONF }; -#define LYD_PRINT_KEEPEMPTYCONT ... #define LYD_PRINT_SHRINK ... +#define LYD_PRINT_EMPTY_CONT ... #define LYD_PRINT_WD_ALL ... #define LYD_PRINT_WD_ALL_TAG ... #define LYD_PRINT_WD_EXPLICIT ... #define LYD_PRINT_WD_IMPL_TAG ... #define LYD_PRINT_WD_MASK ... -#define LYD_PRINT_WITHSIBLINGS ... +#define LYD_PRINT_SIBLINGS ... #define LYD_PRINT_WD_TRIM ... LY_ERR lyd_print_mem(char **, const struct lyd_node *, LYD_FORMAT, uint32_t); LY_ERR lyd_print_tree(struct ly_out *, const struct lyd_node *, LYD_FORMAT, uint32_t); LY_ERR lyd_print_all(struct ly_out *, const struct lyd_node *, LYD_FORMAT, uint32_t); -#define LYD_PARSE_LYB_MOD_UPDATE ... #define LYD_PARSE_NO_STATE ... +#define LYD_PARSE_STORE_ONLY ... +#define LYD_PARSE_JSON_NULL ... #define LYD_PARSE_ONLY ... #define LYD_PARSE_OPAQ ... #define LYD_PARSE_OPTS_MASK ... #define LYD_PARSE_ORDERED ... #define LYD_PARSE_STRICT ... +#define LYD_PARSE_JSON_STRING_DATATYPES ... #define LYD_VALIDATE_NO_STATE ... #define LYD_VALIDATE_PRESENT ... #define LYD_VALIDATE_OPTS_MASK ... +#define LYD_VALIDATE_MULTI_ERROR ... LY_ERR lyd_parse_data_mem(const struct ly_ctx *, const char *, LYD_FORMAT, uint32_t, uint32_t, struct lyd_node **); @@ -330,7 +351,7 @@ LY_ERR ly_out_new_file(FILE *, struct ly_out **); LY_ERR ly_out_new_fd(int, struct ly_out **); LY_ERR lyd_parse_data(const struct ly_ctx *, struct lyd_node *, struct ly_in *, LYD_FORMAT, uint32_t, uint32_t, struct lyd_node **); -LY_ERR lyd_parse_op(const struct ly_ctx *, struct lyd_node *, struct ly_in *, LYD_FORMAT, enum lyd_type, struct lyd_node **, struct lyd_node **); +LY_ERR lyd_parse_op(const struct ly_ctx *, struct lyd_node *, struct ly_in *, LYD_FORMAT, enum lyd_type, uint32_t, struct lyd_node **, struct lyd_node **); typedef enum { LYS_OUT_UNKNOWN, @@ -346,7 +367,17 @@ LY_ERR lys_print_module(struct ly_out *, const struct lys_module *, LYS_OUTFORMA #define LYS_PRINT_NO_SUBSTMT ... #define LYS_PRINT_SHRINK ... +struct lysc_module { + struct lys_module *mod; + const char **features; + struct lysc_node *data; + struct lysc_node_action *rpcs; + struct lysc_node_notif *notifs; + struct lysc_ext_instance *exts; +}; + struct lys_module { + struct ly_ctx *ctx; const char *name; const char *revision; const char *ns; @@ -358,13 +389,15 @@ struct lys_module { const char *ref; struct lysp_module *parsed; struct lysc_module *compiled; + struct lysc_ext *extensions; struct lysc_ident *identities; + struct lysc_submodule *submodules; struct lys_module **augmented_by; struct lys_module **deviated_by; ly_bool implemented; ly_bool to_compile; - uint8_t latest_revision; - ...; + uint8_t version : 2; + uint8_t latest_revision : 4; }; struct lysp_module { @@ -416,20 +449,36 @@ struct lysc_node_container { struct lysc_node_notif *notifs; }; +struct lysp_stmt { + const char *stmt; + const char *arg; + LY_VALUE_FORMAT format; + void *prefix_data; + struct lysp_stmt *next; + struct lysp_stmt *child; + uint16_t flags; + enum ly_stmt kw; +}; + +struct lysp_ext_substmt { + enum ly_stmt stmt; + ...; +}; + struct lysp_ext_instance { const char *name; const char *argument; LY_VALUE_FORMAT format; void *prefix_data; - struct lysp_ext *def; + uintptr_t plugin_ref; void *parent; enum ly_stmt parent_stmt; uint64_t parent_stmt_index; uint16_t flags; - const struct lyplg_ext_record *record; struct lysp_ext_substmt *substmts; void *parsed; struct lysp_stmt *child; + struct lysp_ext_instance *exts; }; struct lysp_import { @@ -443,6 +492,16 @@ struct lysp_ext_instance { char rev[LY_REV_SIZE]; }; +struct lysp_ident { + const char *name; + struct lysp_qname *iffeatures; + const char **bases; + const char *dsc; + const char *ref; + struct lysp_ext_instance *exts; + uint16_t flags; +}; + struct lysp_feature { const char *name; struct lysp_qname *iffeatures; @@ -522,6 +581,25 @@ typedef enum { char* lysc_path(const struct lysc_node *, LYSC_PATH_TYPE, char *, size_t); +struct lysp_when { + const char *cond; + ...; +}; + +struct lysp_refine { + const char *nodeid; + const char *dsc; + const char *ref; + struct lysp_qname *iffeatures; + struct lysp_restr *musts; + const char *presence; + struct lysp_qname *dflts; + uint32_t min; + uint32_t max; + struct lysp_ext_instance *exts; + uint16_t flags; +}; + struct lysp_node_container { struct lysp_restr *musts; struct lysp_when *when; @@ -534,6 +612,11 @@ struct lysp_node_container { ...; }; +struct lysc_value { + const char *str; + struct lysc_prefix *prefixes; +}; + struct lysc_node_leaf { union { struct lysc_node node; @@ -547,7 +630,7 @@ struct lysc_node_leaf { struct lysc_when **when; struct lysc_type *type; const char *units; - struct lyd_value *dflt; + struct lysc_value dflt; ...; }; @@ -577,7 +660,7 @@ struct lysc_node_leaflist { struct lysc_when **when; struct lysc_type *type; const char *units; - struct lyd_value **dflts; + struct lysc_value *dflts; uint32_t min; uint32_t max; ...; @@ -609,13 +692,119 @@ struct lysp_node_list { ...; }; +struct lysp_node_choice { + struct lysp_node *child; + struct lysp_when *when; + struct lysp_qname dflt; + ...; +}; + +struct lysp_node_case { + struct lysp_node *child; + struct lysp_when *when; + ...; +}; + +struct lysp_node_anydata { + struct lysp_restr *musts; + struct lysp_when *when; + ...; +}; + +struct lysp_node_uses { + struct lysp_refine *refines; + struct lysp_node_augment *augments; + struct lysp_when *when; + ...; +}; + +struct lysp_node_action_inout { + struct lysp_restr *musts; + struct lysp_tpdf *typedefs; + struct lysp_node_grp *groupings; + struct lysp_node *child; + ...; +}; + +struct lysp_node_action { + union { + struct lysp_node node; + struct { + struct lysp_node_action *next; + ...; + }; + }; + struct lysp_tpdf *typedefs; + struct lysp_node_grp *groupings; + struct lysp_node_action_inout input; + struct lysp_node_action_inout output; + ...; +}; + +struct lysp_node_notif { + union { + struct lysp_node node; + struct { + struct lysp_node_notif *next; + ...; + }; + }; + struct lysp_restr *musts; + struct lysp_tpdf *typedefs; + struct lysp_node_grp *groupings; + struct lysp_node *child; + ...; +}; + +struct lysp_node_grp { + union { + struct lysp_node node; + struct { + struct lysp_node_grp *next; + ...; + }; + }; + struct lysp_tpdf *typedefs; + struct lysp_node_grp *groupings; + struct lysp_node *child; + struct lysp_node_action *actions; + struct lysp_node_notif *notifs; + ...; +}; + +struct lysp_node_augment { + union { + struct lysp_node node; + struct { + struct lysp_node_augment *next; + ...; + }; + }; + struct lysp_node *child; + struct lysp_when *when; + struct lysp_node_action *actions; + struct lysp_node_notif *notifs; + ...; +}; + struct lysc_type { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; }; +struct lysp_type_enum { + const char *name; + const char *dsc; + const char *ref; + int64_t value; + struct lysp_qname *iffeatures; + struct lysp_ext_instance *exts; + uint16_t flags; +}; + struct lysp_type { const char *name; struct lysp_restr *range; @@ -637,6 +826,7 @@ struct lysp_type { struct lysp_qname { const char *str; const struct lysp_module *mod; + ...; }; struct lysp_node { @@ -676,9 +866,8 @@ struct lysc_ext { const char *name; const char *argname; struct lysc_ext_instance *exts; - struct lyplg_ext *plugin; + uintptr_t plugin_ref; struct lys_module *module; - uint32_t refcount; uint16_t flags; }; @@ -687,22 +876,24 @@ struct lysc_ext { #define LYS_GETNEXT_WITHCASE ... #define LYS_GETNEXT_INTONPCONT ... #define LYS_GETNEXT_OUTPUT ... +#define LYS_GETNEXT_WITHSCHEMAMOUNT ... const struct lysc_node* lys_find_child(const struct lysc_node *, const struct lys_module *, const char *, size_t, uint16_t, uint32_t); const struct lysc_node* lysc_node_child(const struct lysc_node *); const struct lysc_node_action* lysc_node_actions(const struct lysc_node *); const struct lysc_node_notif* lysc_node_notifs(const struct lysc_node *); +LY_ERR lysc_node_lref_targets(const struct lysc_node *, struct ly_set **); +LY_ERR lysc_node_lref_backlinks(const struct ly_ctx *, const struct lysc_node *, ly_bool, struct ly_set **); typedef enum { LYD_PATH_STD, LYD_PATH_STD_NO_LAST_PRED } LYD_PATH_TYPE; -LY_ERR lyd_new_term(struct lyd_node *, const struct lys_module *, const char *, const char *, ly_bool, struct lyd_node **); +LY_ERR lyd_new_term(struct lyd_node *, const struct lys_module *, const char *, const char *, uint32_t, struct lyd_node **); char* lyd_path(const struct lyd_node *, LYD_PATH_TYPE, char *, size_t); LY_ERR lyd_new_inner(struct lyd_node *, const struct lys_module *, const char *, ly_bool, struct lyd_node **); -LY_ERR lyd_new_list(struct lyd_node *, const struct lys_module *, const char *, ly_bool, struct lyd_node **, ...); -LY_ERR lyd_new_list2(struct lyd_node *, const struct lys_module *, const char *, const char *, ly_bool, struct lyd_node **); +LY_ERR lyd_new_list(struct lyd_node *, const struct lys_module *, const char *, uint32_t, struct lyd_node **node, ...); struct lyd_node_inner { union { @@ -719,7 +910,6 @@ struct lyd_node_inner { }; }; struct lyd_node *child; - struct hash_table *children_ht; ...; }; @@ -767,9 +957,14 @@ struct lyd_value { ...; }; +struct lyd_value_union { + struct lyd_value value; + ...; +}; + const char * lyd_get_value(const struct lyd_node *); struct lyd_node* lyd_child(const struct lyd_node *); -LY_ERR lyd_value_validate(const struct ly_ctx *, const struct lysc_node *, const char *, size_t, const struct lyd_node *, const struct lysc_type **, const char **); +ly_bool lyd_is_default(const struct lyd_node *); LY_ERR lyd_find_path(const struct lyd_node *, const char *, ly_bool, struct lyd_node **); void lyd_free_siblings(struct lyd_node *); struct lyd_node* lyd_first_sibling(const struct lyd_node *); @@ -789,6 +984,20 @@ struct lysc_must { struct lysc_ext_instance *exts; }; +struct pcre2_real_code; +typedef struct pcre2_real_code pcre2_code; + +struct lysc_pattern { + const char *expr; + const char *dsc; + const char *ref; + const char *emsg; + const char *eapptag; + struct lysc_ext_instance *exts; + uint32_t inverted : 1; + uint32_t refcount : 31; +}; + #define LYSP_RESTR_PATTERN_ACK ... #define LYSP_RESTR_PATTERN_NACK ... @@ -801,17 +1010,29 @@ struct lysp_restr { struct lysp_ext_instance *exts; }; +struct lysc_ident { + const char *name; + const char *dsc; + const char *ref; + struct lys_module *module; + struct lysc_ident **derived; + struct lysc_ext_instance *exts; + uint16_t flags; +}; + struct lysc_type_num { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_range *range; }; struct lysc_type_dec { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; uint8_t fraction_digits; @@ -819,8 +1040,9 @@ struct lysc_type_dec { }; struct lysc_type_str { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_range *length; @@ -840,60 +1062,66 @@ struct lysc_type_bitenum_item { }; struct lysc_type_enum { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_type_bitenum_item *enums; }; struct lysc_type_bits { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_type_bitenum_item *bits; }; struct lysc_type_leafref { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lyxp_expr *path; struct lysc_prefix *prefixes; - const struct lys_module *cur_mod; struct lysc_type *realtype; uint8_t require_instance; }; struct lysc_type_identityref { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_ident **bases; }; struct lysc_type_instanceid { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; uint8_t require_instance; }; struct lysc_type_union { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_type **types; }; struct lysc_type_bin { + const char *name; struct lysc_ext_instance *exts; - struct lyplg_type *plugin; + uintptr_t plugin_ref; LY_DATA_TYPE basetype; uint32_t refcount; struct lysc_range *length; @@ -916,6 +1144,14 @@ typedef enum { LY_ERR lys_parse(struct ly_ctx *, struct ly_in *, LYS_INFORMAT, const char **, struct lys_module **); LY_ERR ly_ctx_new_ylpath(const char *, const char *, LYD_FORMAT, int, struct ly_ctx **); LY_ERR ly_ctx_get_yanglib_data(const struct ly_ctx *, struct lyd_node **, const char *, ...); +typedef void (*ly_module_imp_data_free_clb)(void *, void *); +typedef LY_ERR (*ly_module_imp_clb)(const char *, const char *, const char *, const char *, void *, LYS_INFORMAT *, const char **, ly_module_imp_data_free_clb *); +void ly_ctx_set_module_imp_clb(struct ly_ctx *, ly_module_imp_clb, void *); +extern "Python" void lypy_module_imp_data_free_clb(void *, void *); +extern "Python" LY_ERR lypy_module_imp_clb(const char *, const char *, const char *, const char *, void *, LYS_INFORMAT *, const char **, ly_module_imp_data_free_clb *); + +LY_ERR lydict_insert(const struct ly_ctx *, const char *, size_t, const char **); +LY_ERR lydict_remove(const struct ly_ctx *, const char *); struct lyd_meta { struct lyd_node *parent; @@ -929,8 +1165,7 @@ typedef enum { LYD_ANYDATA_DATATREE, LYD_ANYDATA_STRING, LYD_ANYDATA_XML, - LYD_ANYDATA_JSON, - LYD_ANYDATA_LYB + LYD_ANYDATA_JSON } LYD_ANYDATA_VALUETYPE; union lyd_any_value { @@ -938,7 +1173,6 @@ union lyd_any_value { const char *str; const char *xml; const char *json; - char *mem; }; struct lyd_node_any { @@ -968,6 +1202,9 @@ LY_ERR lyd_any_value_str(const struct lyd_node *, char **); LY_ERR lyd_merge_tree(struct lyd_node **, const struct lyd_node *, uint16_t); LY_ERR lyd_merge_siblings(struct lyd_node **, const struct lyd_node *, uint16_t); LY_ERR lyd_insert_child(struct lyd_node *, struct lyd_node *); +LY_ERR lyd_insert_sibling(struct lyd_node *, struct lyd_node *, struct lyd_node **); +LY_ERR lyd_insert_after(struct lyd_node *, struct lyd_node *); +LY_ERR lyd_insert_before(struct lyd_node *, struct lyd_node *); LY_ERR lyd_diff_apply_all(struct lyd_node **, const struct lyd_node *); #define LYD_DUP_NO_META ... @@ -979,6 +1216,17 @@ LY_ERR lyd_dup_siblings(const struct lyd_node *, struct lyd_node_inner *, uint32 LY_ERR lyd_dup_single(const struct lyd_node *, struct lyd_node_inner *, uint32_t, struct lyd_node **); void lyd_free_meta_single(struct lyd_meta *); +struct lysp_tpdf { + const char *name; + const char *units; + struct lysp_qname dflt; + const char *dsc; + const char *ref; + struct lysp_ext_instance *exts; + struct lysp_type type; + uint16_t flags; +}; + struct lysc_when { struct lyxp_expr *cond; struct lysc_node *context; @@ -992,6 +1240,19 @@ struct lysc_when { struct lysc_when** lysc_node_when(const struct lysc_node *); +struct lysc_node_case { + struct lysc_node *child; + struct lysc_when **when; + ...; +}; + +struct lysc_node_choice { + struct lysc_node_case *cases; + struct lysc_when **when; + struct lysc_node_case *dflt; + ...; +}; + #define LYD_DEFAULT ... #define LYD_WHEN_TRUE ... #define LYD_NEW ... @@ -1006,9 +1267,110 @@ LY_ERR lyd_merge_module(struct lyd_node **, const struct lyd_node *, const struc #define LYD_IMPLICIT_OUTPUT ... #define LYD_IMPLICIT_NO_DEFAULTS ... +LY_ERR lyd_new_implicit_tree(struct lyd_node *, uint32_t, struct lyd_node **); +LY_ERR lyd_new_implicit_module(struct lyd_node **, const struct lys_module *, uint32_t, struct lyd_node **); LY_ERR lyd_new_implicit_all(struct lyd_node **, const struct ly_ctx *, uint32_t, struct lyd_node **); -LY_ERR lyd_new_meta(const struct ly_ctx *, struct lyd_node *, const struct lys_module *, const char *, const char *, ly_bool, struct lyd_meta **); +LY_ERR lyd_new_meta(const struct ly_ctx *, struct lyd_node *, const struct lys_module *, const char *, const char *, uint32_t, struct lyd_meta **); + +struct ly_opaq_name { + const char *name; + const char *prefix; + + union { + const char *module_ns; + const char *module_name; + }; +}; + +struct lyd_node_opaq { + union { + struct lyd_node node; + + struct { + uint32_t hash; + uint32_t flags; + const struct lysc_node *schema; + struct lyd_node_inner *parent; + struct lyd_node *next; + struct lyd_node *prev; + struct lyd_meta *meta; + void *priv; + }; + }; + + struct lyd_node *child; + + struct ly_opaq_name name; + const char *value; + uint32_t hints; + LY_VALUE_FORMAT format; + void *val_prefix_data; + + struct lyd_attr *attr; + const struct ly_ctx *ctx; +}; + +struct lyd_attr { + struct lyd_node_opaq *parent; + struct lyd_attr *next; + struct ly_opaq_name name; + const char *value; + uint32_t hints; + LY_VALUE_FORMAT format; + void *val_prefix_data; +}; + +LY_ERR lyd_new_attr(struct lyd_node *, const char *, const char *, const char *, struct lyd_attr **); +void lyd_free_attr_single(const struct ly_ctx *ctx, struct lyd_attr *attr); + +LY_ERR lyd_value_validate_dflt(const struct lysc_node *, const char *, struct lysc_prefix *, const struct lyd_node *, const struct lysc_type **, const char **); + +struct lyd_leafref_links_rec { + const struct lyd_node_term *node; + const struct lyd_node_term **leafref_nodes; + const struct lyd_node_term **target_nodes; +}; + +LY_ERR lyd_leafref_get_links(const struct lyd_node_term *, const struct lyd_leafref_links_rec **); +LY_ERR lyd_leafref_link_node_tree(struct lyd_node *); +struct lyplg_ext *lysc_get_ext_plugin(uintptr_t); +const char *lyplg_ext_stmt2str(enum ly_stmt stmt); +const struct lysp_module *lyplg_ext_parse_get_cur_pmod(const struct lysp_ctx *); +struct ly_ctx *lyplg_ext_compile_get_ctx(const struct lysc_ctx *); +void lyplg_ext_parse_log(const struct lysp_ctx *, const struct lysp_ext_instance *, LY_LOG_LEVEL, LY_ERR, const char *, ...); +void lyplg_ext_compile_log(const struct lysc_ctx *, const struct lysc_ext_instance *, LY_LOG_LEVEL, LY_ERR, const char *, ...); +LY_ERR lyplg_ext_parse_extension_instance(struct lysp_ctx *, struct lysp_ext_instance *); +LY_ERR lyplg_ext_compile_extension_instance(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *, struct lysc_node *); +void lyplg_ext_pfree_instance_substatements(const struct ly_ctx *ctx, struct lysp_ext_substmt *substmts); +void lyplg_ext_cfree_instance_substatements(const struct ly_ctx *ctx, struct lysc_ext_substmt *substmts); +typedef LY_ERR (*lyplg_ext_parse_clb)(struct lysp_ctx *, struct lysp_ext_instance *); +typedef LY_ERR (*lyplg_ext_compile_clb)(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *); +typedef void (*lyplg_ext_parse_free_clb)(const struct ly_ctx *, struct lysp_ext_instance *); +typedef void (*lyplg_ext_compile_free_clb)(const struct ly_ctx *, struct lysc_ext_instance *); +struct lyplg_ext { + const char *id; + lyplg_ext_parse_clb parse; + lyplg_ext_compile_clb compile; + lyplg_ext_parse_free_clb pfree; + lyplg_ext_compile_free_clb cfree; + ...; +}; + +struct lyplg_ext_record { + const char *module; + const char *revision; + const char *name; + struct lyplg_ext plugin; + ...; +}; + +#define LYPLG_EXT_API_VERSION ... +LY_ERR lyplg_add_extension_plugin(struct ly_ctx *, uint32_t, const struct lyplg_ext_record *); +extern "Python" LY_ERR lypy_lyplg_ext_parse_clb(struct lysp_ctx *, struct lysp_ext_instance *); +extern "Python" LY_ERR lypy_lyplg_ext_compile_clb(struct lysc_ctx *, const struct lysp_ext_instance *, struct lysc_ext_instance *); +extern "Python" void lypy_lyplg_ext_parse_free_clb(const struct ly_ctx *, struct lysp_ext_instance *); +extern "Python" void lypy_lyplg_ext_compile_free_clb(const struct ly_ctx *, struct lysc_ext_instance *); /* from libc, needed to free allocated strings */ void free(void *); diff --git a/cffi/source.c b/cffi/source.c index f7fe18a2..3c76e984 100644 --- a/cffi/source.c +++ b/cffi/source.c @@ -6,9 +6,6 @@ #include <libyang/libyang.h> #include <libyang/version.h> -#if (LY_VERSION_MAJOR != 2) -#error "This version of libyang bindings only works with libyang 2.x" -#endif -#if (LY_VERSION_MINOR < 25) -#error "Need at least libyang 2.25" +#if LY_VERSION_MAJOR * 10000 + LY_VERSION_MINOR * 100 + LY_VERSION_MICRO < 40202 +#error "This version of libyang bindings only works with libyang soversion 4.2.2+" #endif diff --git a/check-commits.sh b/check-commits.sh new file mode 100755 index 00000000..99ef06cf --- /dev/null +++ b/check-commits.sh @@ -0,0 +1,129 @@ +#!/bin/sh + +set -e + +revision_range="${1?revision range}" + +valid=0 +revisions=$(git rev-list --reverse "$revision_range") +total=$(echo $revisions | wc -w) +if [ "$total" -eq 0 ]; then + exit 0 +fi +tmp=$(mktemp) +trap "rm -f $tmp" EXIT + +allowed_trailers=" +Closes +Fixes +Link +Suggested-by +Requested-by +Reported-by +Co-authored-by +Signed-off-by +Tested-by +Reviewed-by +Acked-by +" + +n=0 +title= +shortrev= +fail=false +repo=CESNET/libyang-python +repo_url=https://github.com/$repo +api_url=https://api.github.com/repos/$repo + +err() { + + echo "error: commit $shortrev (\"$title\") $*" >&2 + fail=true +} + +check_issue() { + curl -f -X GET -L --no-progress-meter \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "$api_url/issues/${1##*/}" | jq -r .state | grep -Fx open +} + +for rev in $revisions; do + n=$((n + 1)) + title=$(git log --format='%s' -1 "$rev") + fail=false + shortrev=$(printf '%-12.12s' $rev) + + if [ "$(echo "$title" | wc -m)" -gt 72 ]; then + err "title is longer than 72 characters, please make it shorter" + fi + if ! echo "$title" | grep -qE '^[a-z0-9,{}/_-]+: '; then + err "title lacks a lowercase topic prefix (e.g. 'data: ')" + fi + if echo "$title" | grep -qE '^[a-z0-9,{}/_-]+: [A-Z][a-z]'; then + err "title starts with an capital letter, please use lower case" + fi + if ! echo "$title" | grep -qE '[A-Za-z0-9]$'; then + err "title ends with punctuation, please remove it" + fi + + author=$(git log --format='%an <%ae>' -1 "$rev") + if ! git log --format="%(trailers:key=Signed-off-by,only,valueonly,unfold)" -1 "$rev" | + grep -qFx "$author"; then + err "'Signed-off-by: $author' trailer is missing" + fi + + for trailer in $(git log --format="%(trailers:only,keyonly)" -1 "$rev"); do + if ! echo "$allowed_trailers" | grep -qFx "$trailer"; then + err "trailer '$trailer' is misspelled or not in the sanctioned list" + fi + done + + git log --format="%(trailers:key=Closes,only,valueonly,unfold)" -1 "$rev" > $tmp + while read -r value; do + if [ -z "$value" ]; then + continue + fi + case "$value" in + $repo_url/*/[0-9]*) + if ! check_issue "$value"; then + err "'$value' does not reference a valid open issue" + fi + ;; + \#[0-9]*) + err "please use the full issue URL: 'Closes: $repo_url/issues/$value'" + ;; + *) + err "invalid trailer value '$value'. The 'Closes:' trailer must only be used to reference issue URLs" + ;; + esac + done < "$tmp" + + git log --format="%(trailers:key=Fixes,only,valueonly,unfold)" -1 "$rev" > $tmp + while read -r value; do + if [ -z "$value" ]; then + continue + fi + fixes_rev=$(echo "$value" | sed -En 's/([A-Fa-f0-9]{7,})[[:space:]]\(".*"\)/\1/p') + if ! git cat-file commit "$fixes_rev" >/dev/null; then + err "trailer '$value' does not refer to a known commit" + fi + done < "$tmp" + + body=$(git log --format='%b' -1 "$rev") + body=${body%$(git log --format='%(trailers)' -1 "$rev")} + if [ "$(echo "$body" | wc -w)" -lt 3 ]; then + err "body has less than three words, please describe your changes" + fi + + if [ "$fail" = true ]; then + continue + fi + echo "ok commit $shortrev (\"$title\")" + valid=$((valid + 1)) +done + +echo "$valid/$total valid commit messages" +if [ "$valid" -ne "$total" ]; then + exit 1 +fi diff --git a/libyang/__init__.py b/libyang/__init__.py index 1207a6b3..d8f95556 100644 --- a/libyang/__init__.py +++ b/libyang/__init__.py @@ -13,6 +13,7 @@ DLeafList, DList, DNode, + DNodeAttrs, DNotif, DRpc, ) @@ -62,11 +63,16 @@ UnitsRemoved, schema_diff, ) +from .extension import ExtensionPlugin, LibyangExtensionError from .keyed_list import KeyedList -from .log import configure_logging +from .log import configure_logging, temp_log_options from .schema import ( + Enum, Extension, + ExtensionCompiled, + ExtensionParsed, Feature, + Identity, IfAndFeatures, IfFeature, IfFeatureExpr, @@ -74,7 +80,31 @@ IfNotFeature, IfOrFeatures, Module, + Must, + PAction, + PActionInOut, + PAnydata, + Pattern, + PAugment, + PCase, + PChoice, + PContainer, + PEnum, + PGrouping, + PIdentity, + PLeaf, + PLeafList, + PList, + PNode, + PNotif, + PRefine, + PType, + PUses, Revision, + SAnydata, + SAnyxml, + SCase, + SChoice, SContainer, SLeaf, SLeafList, @@ -83,6 +113,7 @@ SRpc, SRpcInOut, Type, + Typedef, ) from .util import DataType, IOType, LibyangError from .xpath import ( @@ -114,12 +145,17 @@ "DefaultRemoved", "DescriptionAdded", "DescriptionRemoved", + "Enum", "EnumAdded", "EnumRemoved", "Extension", "ExtensionAdded", + "ExtensionCompiled", + "ExtensionParsed", + "ExtensionPlugin", "ExtensionRemoved", "Feature", + "Identity", "IfAndFeatures", "IfFeature", "IfFeatureExpr", @@ -137,12 +173,32 @@ "MandatoryAdded", "MandatoryRemoved", "Module", + "Must", "MustAdded", "MustRemoved", "NodeTypeAdded", "NodeTypeRemoved", "OrderedByUserAdded", "OrderedByUserRemoved", + "PAction", + "PActionInOut", + "PAnydata", + "PAugment", + "PCase", + "PChoice", + "PContainer", + "PEnum", + "PGrouping", + "PIdentity", + "PLeaf", + "PLeafList", + "PList", + "PNode", + "PNotif", + "PRefine", + "PType", + "PUses", + "Pattern", "PatternAdded", "PatternRemoved", "PresenceAdded", @@ -150,6 +206,10 @@ "RangeAdded", "RangeRemoved", "Revision", + "SAnydata", + "SAnyxml", + "SCase", + "SChoice", "SContainer", "SLeaf", "SLeafList", diff --git a/libyang/context.py b/libyang/context.py index d5f797d9..d0309d70 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -4,44 +4,236 @@ # SPDX-License-Identifier: MIT import os -from typing import IO, Any, Iterator, Optional, Union +from typing import IO, Any, Callable, Iterator, List, Optional, Sequence, Tuple, Union from _libyang import ffi, lib from .data import ( DNode, data_format, data_type, + newval_flags, parser_flags, - path_flags, validation_flags, ) from .schema import Module, SNode, schema_in_format -from .util import DataType, IOType, LibyangError, c2str, data_load, str2c +from .util import ( + DataType, + IOType, + LibyangError, + LibyangErrorItem, + c2str, + data_load, + str2c, +) # ------------------------------------------------------------------------------------- -class Context: +@ffi.def_extern(name="lypy_module_imp_data_free_clb") +def libyang_c_module_imp_data_free_clb(cdata, user_data): + instance = ffi.from_handle(user_data) + instance.free_module_data(cdata) + + +# ------------------------------------------------------------------------------------- +@ffi.def_extern(name="lypy_module_imp_clb") +def libyang_c_module_imp_clb( + mod_name, + mod_rev, + submod_name, + submod_rev, + user_data, + fmt, + module_data, + free_module_data, +): + """ + Implement the C callback function for loading modules from any location. + + :arg c_str mod_name: + The YANG module name + :arg c_str mod_rev: + The YANG module revision + :arg c_str submod_name: + The YANG submodule name + :arg c_str submod_rev: + The YANG submodule revision + :arg user_data: + The user data provided by user during registration. In this implementation + it is always considered to be handle of Python object + :arg fmt: + The output pointer where to set the format of schema + :arg module_data: + The output pointer where to set the schema data itself + :arg free_module_data: + The output pointer of callback function which will be called when the schema + data are no longer needed + + :returns: + The LY_SUCCESS in case the needed YANG (sub)module schema was found + The LY_ENOT in case the needed YANG (sub)module schema was not found + """ + fmt[0] = lib.LYS_IN_UNKNOWN + module_data[0] = ffi.NULL + free_module_data[0] = lib.lypy_module_imp_data_free_clb + instance = ffi.from_handle(user_data) + ret = instance.get_module_data( + c2str(mod_name), c2str(mod_rev), c2str(submod_name), c2str(submod_rev) + ) + if ret is None: + return lib.LY_ENOT + in_fmt, content = ret + fmt[0] = schema_in_format(in_fmt) + module_data[0] = content + return lib.LY_SUCCESS + + +# ------------------------------------------------------------------------------------- +class ContextExternalModuleLoader: + __slots__ = ( + "_cdata", + "_module_data_clb", + "_cffi_handle", + "_cdata_modules", + ) + + def __init__(self, cdata) -> None: + self._cdata = cdata # C type: "struct ly_ctx *" + self._module_data_clb = None + self._cffi_handle = ffi.new_handle(self) + self._cdata_modules = [] + + def free_module_data(self, cdata) -> None: + """ + Free previously stored data, obtained after a get_module_data. + + :arg cdata: + The pointer to YANG modelu schema (c_str), which shall be released from memory + """ + self._cdata_modules.remove(cdata) - __slots__ = ("cdata",) + def get_module_data( + self, + mod_name: Optional[str], + mod_rev: Optional[str], + submod_name: Optional[str], + submod_rev: Optional[str], + ) -> Optional[Tuple[str, str]]: + """ + Get the YANG module schema data based requirements from libyang_c_module_imp_clb + function and forward that request to user Python based callback function. + + The returned data from callback function are stored within the context to make sure + of no memory access issues. These data a stored until the free_module_data function + is called directly by libyang. + + :arg self + This instance on context + :arg mod_name: + The optional YANG module name + :arg mod_rev: + The optional YANG module revision + :arg submod_name: + The optional YANG submodule name + :arg submod_rev: + The optional YANG submodule revision + + :returns: + Tuple of format string and YANG (sub)module schema + """ + if self._module_data_clb is None: + return None + ret = self._module_data_clb(mod_name, mod_rev, submod_name, submod_rev) + if ret is None: + return None + fmt_str, module_data = ret + module_data_c = str2c(module_data) + self._cdata_modules.append(module_data_c) + return fmt_str, module_data_c + + def set_module_data_clb( + self, + clb: Optional[ + Callable[ + [Optional[str], Optional[str], Optional[str], Optional[str]], + Optional[Tuple[str, str]], + ] + ] = None, + ) -> None: + """ + Set the callback function, which will be called if libyang context would like to + load module or submodule, which is not locally available in context path(s). + + :arg self + This instance on context + :arg clb: + The callback function. The expected arguments are: + mod_name: Module name + mod_rev: Module revision + submod_name: Submodule name + submod_rev: Submodule revision + The expeted return value is either: + tuple of: + format: The string format of the loaded data + data: The YANG (sub)module data as string + or None in case of error + """ + self._module_data_clb = clb + if clb is None: + lib.ly_ctx_set_module_imp_clb(self._cdata, ffi.NULL, ffi.NULL) + else: + lib.ly_ctx_set_module_imp_clb( + self._cdata, lib.lypy_module_imp_clb, self._cffi_handle + ) + + +# ------------------------------------------------------------------------------------- +class Context: + __slots__ = ( + "cdata", + "external_module_loader", + "__dict__", + ) def __init__( self, search_path: Optional[str] = None, + disable_searchdirs: bool = False, disable_searchdir_cwd: bool = True, explicit_compile: Optional[bool] = False, + leafref_extended: bool = False, + leafref_linking: bool = False, + builtin_plugins_only: bool = False, + all_implemented: bool = False, + enable_imp_features: bool = False, + compile_obsolete: bool = False, yanglib_path: Optional[str] = None, yanglib_fmt: str = "json", cdata=None, # C type: "struct ly_ctx *" ): if cdata is not None: self.cdata = ffi.cast("struct ly_ctx *", cdata) + self.external_module_loader = ContextExternalModuleLoader(self.cdata) return # already initialized options = 0 + if disable_searchdirs: + options |= lib.LY_CTX_DISABLE_SEARCHDIRS if disable_searchdir_cwd: options |= lib.LY_CTX_DISABLE_SEARCHDIR_CWD if explicit_compile: options |= lib.LY_CTX_EXPLICIT_COMPILE + if leafref_extended: + options |= lib.LY_CTX_LEAFREF_EXTENDED + if leafref_linking: + options |= lib.LY_CTX_LEAFREF_LINKING + if builtin_plugins_only: + options |= lib.LY_CTX_BUILTIN_PLUGINS_ONLY + if all_implemented: + options |= lib.LY_CTX_ALL_IMPLEMENTED + if enable_imp_features: + options |= lib.LY_CTX_ENABLE_IMP_FEATURES + if compile_obsolete: + options |= lib.LY_CTX_COMPILE_OBSOLETE # force priv parsed options |= lib.LY_CTX_SET_PRIV_PARSED @@ -62,6 +254,7 @@ def __init__( search_path = ":".join(search_paths) if search_paths else None if yanglib_path is None: + options |= lib.LY_CTX_NO_YANGLIBRARY if lib.ly_ctx_new(str2c(search_path), options, ctx) != lib.LY_SUCCESS: raise self.error("cannot create context") else: @@ -81,6 +274,7 @@ def __init__( ) if not self.cdata: raise self.error("cannot create context") + self.external_module_loader = ContextExternalModuleLoader(self.cdata) def compile_schema(self): ret = lib.ly_ctx_compile(self.cdata) @@ -107,19 +301,35 @@ def __exit__(self, *args, **kwargs): self.destroy() def error(self, msg: str, *args) -> LibyangError: - msg %= args + if args: + msg = msg % args + + parts = [msg] + errors = [] if self.cdata: err = lib.ly_err_first(self.cdata) while err: - if err.msg: - msg += ": %s" % c2str(err.msg) - if err.path: - msg += ": %s" % c2str(err.path) + m = c2str(err.msg) if err.msg else None + dp = c2str(err.data_path) if err.data_path else None + sp = c2str(err.schema_path) if err.schema_path else None + ln = int(err.line) if err.line else None + parts.extend( + tmpl.format(val) + for val, tmpl in [ + (m, ": {}"), + (dp, ": Data path: {}"), + (sp, ": Schema path: {}"), + (ln, " (line {})"), + ] + if val is not None + ) + errors.append(LibyangErrorItem(m, dp, sp, ln)) err = err.next lib.ly_err_clean(self.cdata, ffi.NULL) - return LibyangError(msg) + msg = "".join(parts) + return LibyangError(msg, errors=errors) def parse_module( self, @@ -144,7 +354,9 @@ def parse_module( mod = ffi.new("struct lys_module **") fmt = schema_in_format(fmt) - if lib.lys_parse(self.cdata, data[0], fmt, feat, mod) != lib.LY_SUCCESS: + ret = lib.lys_parse(self.cdata, data[0], fmt, feat, mod) + lib.ly_in_free(data[0], 0) + if ret != lib.LY_SUCCESS: raise self.error("failed to parse module") return Module(self, mod[0]) @@ -157,10 +369,19 @@ def parse_module_file( def parse_module_str(self, s: str, fmt: str = "yang", features=None) -> Module: return self.parse_module(s, IOType.MEMORY, fmt, features) - def load_module(self, name: str) -> Module: + def load_module( + self, + name: str, + revision: Optional[str] = None, + enabled_features: Sequence[str] = (), + ) -> Module: if self.cdata is None: raise RuntimeError("context already destroyed") - mod = lib.ly_ctx_load_module(self.cdata, str2c(name), ffi.NULL, ffi.NULL) + if enabled_features: + features = tuple([str2c(f) for f in enabled_features] + [ffi.NULL]) + else: + features = ffi.NULL + mod = lib.ly_ctx_load_module(self.cdata, str2c(name), str2c(revision), features) if mod == ffi.NULL: raise self.error("cannot load module") @@ -175,17 +396,27 @@ def get_module(self, name: str) -> Module: return Module(self, mod) - def find_path(self, path: str, output: bool = False) -> Iterator[SNode]: + def find_path( + self, + path: str, + output: bool = False, + root_node: Optional[SNode] = None, + ) -> Iterator[SNode]: if self.cdata is None: raise RuntimeError("context already destroyed") + if root_node is not None: + ctx_node = root_node.cdata + else: + ctx_node = ffi.NULL + flags = 0 if output: flags |= lib.LYS_FIND_XP_OUTPUT node_set = ffi.new("struct ly_set **") if ( - lib.lys_find_xpath(self.cdata, ffi.NULL, str2c(path), 0, node_set) + lib.lys_find_xpath(self.cdata, ctx_node, str2c(path), 0, node_set) != lib.LY_SUCCESS ): raise self.error("cannot find path") @@ -199,12 +430,44 @@ def find_path(self, path: str, output: bool = False) -> Iterator[SNode]: finally: lib.ly_set_free(node_set, ffi.NULL) - def find_jsonpath( + def find_xpath_atoms( self, path: str, + output: bool = False, root_node: Optional["libyang.SNode"] = None, + ) -> Iterator[SNode]: + if self.cdata is None: + raise RuntimeError("context already destroyed") + + if root_node is not None: + ctx_node = root_node.cdata + else: + ctx_node = ffi.NULL + + flags = lib.LYS_FIND_XP_OUTPUT if output else 0 + + node_set = ffi.new("struct ly_set **") + if ( + lib.lys_find_xpath_atoms(self.cdata, ctx_node, str2c(path), flags, node_set) + != lib.LY_SUCCESS + ): + raise self.error("cannot find path") + + node_set = node_set[0] + if node_set.count == 0: + raise self.error("cannot find path") + try: + for i in range(node_set.count): + yield SNode.new(self, node_set.snodes[i]) + finally: + lib.ly_set_free(node_set, ffi.NULL) + + def find_jsonpath( + self, + path: str, + root_node: Optional[SNode] = None, output: bool = False, - ) -> Optional["libyang.SNode"]: + ) -> Optional[SNode]: if root_node is not None: ctx_node = root_node.cdata else: @@ -221,7 +484,7 @@ def create_data_path( parent: Optional[DNode] = None, value: Any = None, update: bool = True, - no_parent_ret: bool = True, + store_only: bool = False, rpc_output: bool = False, force_return_value: bool = True, ) -> Optional[DNode]: @@ -232,8 +495,8 @@ def create_data_path( 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 + flags = newval_flags( + update=update, store_only=store_only, rpc_output=rpc_output ) dnode = ffi.new("struct lyd_node **") ret = lib.lyd_new_path( @@ -246,7 +509,8 @@ def create_data_path( ) dnode = dnode[0] if ret != lib.LY_SUCCESS: - if lib.ly_vecode(self.cdata) != lib.LYVE_SUCCESS: + err = lib.ly_err_last(self.cdata) + if err != ffi.NULL and err.vecode != lib.LYVE_SUCCESS: raise self.error("cannot create data path: %s", path) lib.ly_err_clean(self.cdata, ffi.NULL) if not dnode and not force_return_value: @@ -281,6 +545,8 @@ def parse_op( in_data: Union[IO, str], dtype: DataType, parent: DNode = None, + opaq: bool = False, + strict: bool = False, ) -> DNode: fmt = data_format(fmt) data = ffi.new("struct ly_in **") @@ -290,13 +556,15 @@ def parse_op( if ret != lib.LY_SUCCESS: raise self.error("failed to read input data") + flags = parser_flags(opaq=opaq, strict=strict) tree = ffi.new("struct lyd_node **", ffi.NULL) op = ffi.new("struct lyd_node **", ffi.NULL) par = ffi.new("struct lyd_node **", ffi.NULL) if parent is not None: par[0] = parent.cdata - ret = lib.lyd_parse_op(self.cdata, par[0], data[0], fmt, dtype, tree, op) + ret = lib.lyd_parse_op(self.cdata, par[0], data[0], fmt, dtype, flags, tree, op) + lib.ly_in_free(data[0], 0) if ret != lib.LY_SUCCESS: raise self.error("failed to parse input data") @@ -308,9 +576,17 @@ def parse_op_mem( data: str, dtype: DataType = DataType.DATA_YANG, parent: DNode = None, + opaq: bool = False, + strict: bool = False, ): return self.parse_op( - fmt, in_type=IOType.MEMORY, in_data=data, dtype=dtype, parent=parent + fmt, + in_type=IOType.MEMORY, + in_data=data, + dtype=dtype, + parent=parent, + opaq=opaq, + strict=strict, ) def parse_data( @@ -319,26 +595,33 @@ def parse_data( in_type: IOType, in_data: Union[str, bytes, IO], parent: DNode = None, - lyb_mod_update: bool = False, no_state: bool = False, parse_only: bool = False, opaq: bool = False, ordered: bool = False, strict: bool = False, validate_present: bool = False, + validate_multi_error: bool = False, + store_only: bool = False, + json_null: bool = False, + json_string_datatypes: bool = False, ) -> Optional[DNode]: if self.cdata is None: raise RuntimeError("context already destroyed") parser_flgs = parser_flags( - lyb_mod_update=lyb_mod_update, no_state=no_state, parse_only=parse_only, opaq=opaq, ordered=ordered, strict=strict, + store_only=store_only, + json_null=json_null, + json_string_datatypes=json_string_datatypes, ) validation_flgs = validation_flags( - no_state=no_state, validate_present=validate_present + no_state=no_state, + validate_present=validate_present, + validate_multi_error=validate_multi_error, ) fmt = data_format(fmt) encode = True @@ -350,24 +633,10 @@ def parse_data( if ret != lib.LY_SUCCESS: raise self.error("failed to read input data") - if parent is not None: - ret = lib.lyd_parse_data( - self.cdata, - parent.cdata, - data[0], - fmt, - parser_flgs, - validation_flgs, - ffi.NULL, - ) - lib.ly_in_free(data[0], 0) - if ret != lib.LY_SUCCESS: - raise self.error("failed to parse data tree") - return None - + parent_cdata = parent.cdata if parent is not None else ffi.NULL dnode = ffi.new("struct lyd_node **") ret = lib.lyd_parse_data( - self.cdata, ffi.NULL, data[0], fmt, parser_flgs, validation_flgs, dnode + self.cdata, parent_cdata, data[0], fmt, parser_flgs, validation_flgs, dnode ) lib.ly_in_free(data[0], 0) if ret != lib.LY_SUCCESS: @@ -383,26 +652,32 @@ def parse_data_mem( data: Union[str, bytes], fmt: str, parent: DNode = None, - lyb_mod_update: bool = False, no_state: bool = False, parse_only: bool = False, opaq: bool = False, ordered: bool = False, strict: bool = False, validate_present: bool = False, + validate_multi_error: bool = False, + store_only: bool = False, + json_null: bool = False, + json_string_datatypes: bool = False, ) -> Optional[DNode]: return self.parse_data( fmt, in_type=IOType.MEMORY, in_data=data, parent=parent, - lyb_mod_update=lyb_mod_update, no_state=no_state, parse_only=parse_only, opaq=opaq, ordered=ordered, strict=strict, validate_present=validate_present, + validate_multi_error=validate_multi_error, + store_only=store_only, + json_null=json_null, + json_string_datatypes=json_string_datatypes, ) def parse_data_file( @@ -410,28 +685,142 @@ def parse_data_file( fileobj: IO, fmt: str, parent: DNode = None, - lyb_mod_update: bool = False, no_state: bool = False, parse_only: bool = False, opaq: bool = False, ordered: bool = False, strict: bool = False, validate_present: bool = False, + validate_multi_error: bool = False, + store_only: bool = False, + json_null: bool = False, + json_string_datatypes: bool = False, ) -> Optional[DNode]: return self.parse_data( fmt, in_type=IOType.FD, in_data=fileobj, parent=parent, - lyb_mod_update=lyb_mod_update, no_state=no_state, parse_only=parse_only, opaq=opaq, ordered=ordered, strict=strict, validate_present=validate_present, + validate_multi_error=validate_multi_error, + store_only=store_only, + json_null=json_null, + json_string_datatypes=json_string_datatypes, ) + def find_leafref_path_target_paths(self, leafref_path: str) -> List[str]: + """ + Fetch all leafref targets of the specified path + + This is an enhanced version of lysc_node_lref_target() which will return + a set of leafref target paths retrieved from the specified schema path. + While lysc_node_lref_target() will only work on nodetype of LYS_LEAF and + LYS_LEAFLIST this function will also evaluate other datatypes that may + contain leafrefs such as LYS_UNION. This does not, however, search for + children with leafref targets. + + :arg self + This instance on context + :arg leafref_path: + Path to node to search for leafref targets + :returns List of target paths that the leafrefs of the specified node + point to. + """ + if self.cdata is None: + raise RuntimeError("context already destroyed") + if leafref_path is None: + raise RuntimeError("leafref_path must be defined") + + out = [] + + node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(leafref_path), 0) + if node == ffi.NULL: + raise self.error("leafref_path not found") + + node_set = ffi.new("struct ly_set **") + if ( + lib.lysc_node_lref_targets(node, node_set) != lib.LY_SUCCESS + or node_set[0] == ffi.NULL + or node_set[0].count == 0 + ): + raise self.error("leafref_path does not contain any leafref targets") + + node_set = node_set[0] + for i in range(node_set.count): + path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0) + out.append(c2str(path)) + lib.free(path) + + lib.ly_set_free(node_set, ffi.NULL) + + return out + + def find_backlinks_paths( + self, match_path: str = None, match_ancestors: bool = False + ) -> List[str]: + """ + Search entire schema for nodes that contain leafrefs and return as a + list of schema node paths. + + Perform a complete scan of the schema tree looking for nodes that + contain leafref entries. When a node contains a leafref entry, and + match_path is specified, determine if reference points to match_path, + if so add the node's path to returned list. If no match_path is + specified, the node containing the leafref is always added to the + returned set. When match_ancestors is true, will evaluate if match_path + is self or an ansestor of self. + + This does not return the leafref targets, but the actual node that + contains a leafref. + + :arg self + This instance on context + :arg match_path: + Target path to use for matching + :arg match_ancestors: + Whether match_path is a base ancestor or an exact node + :returns List of paths. Exception of match_path is not found or if no + backlinks are found. + """ + if self.cdata is None: + raise RuntimeError("context already destroyed") + out = [] + + match_node = ffi.NULL + if match_path is not None and match_path == "/" or match_path == "": + match_path = None + + if match_path: + match_node = lib.lys_find_path(self.cdata, ffi.NULL, str2c(match_path), 0) + if match_node == ffi.NULL: + raise self.error("match_path not found") + + node_set = ffi.new("struct ly_set **") + if ( + lib.lysc_node_lref_backlinks( + self.cdata, match_node, match_ancestors, node_set + ) + != lib.LY_SUCCESS + or node_set[0] == ffi.NULL + or node_set[0].count == 0 + ): + raise self.error("backlinks not found") + + node_set = node_set[0] + for i in range(node_set.count): + path = lib.lysc_path(node_set.snodes[i], lib.LYSC_PATH_DATA, ffi.NULL, 0) + out.append(c2str(path)) + lib.free(path) + + lib.ly_set_free(node_set, ffi.NULL) + + return out + def __iter__(self) -> Iterator[Module]: """ Return an iterator that yields all implemented modules from the context @@ -443,3 +832,13 @@ def __iter__(self) -> Iterator[Module]: while mod: yield Module(self, mod) mod = lib.ly_ctx_get_module_iter(self.cdata, idx) + + def add_to_dict(self, orig_str: str) -> Any: + cstr = ffi.new("char **") + ret = lib.lydict_insert(self.cdata, str2c(orig_str), 0, cstr) + if ret != lib.LY_SUCCESS: + raise LibyangError("Unable to insert string into context dictionary") + return cstr[0] + + def remove_from_dict(self, orig_str: str) -> None: + lib.lydict_remove(self.cdata, str2c(orig_str)) diff --git a/libyang/data.py b/libyang/data.py index 357339ee..230c51af 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT import logging -from typing import IO, Any, Dict, Iterator, Optional, Union +from typing import IO, Any, Dict, Iterator, Optional, Tuple, Union from _libyang import ffi, lib from .keyed_list import KeyedList @@ -18,7 +18,7 @@ SRpc, Type, ) -from .util import DataType, IOType, LibyangError, c2str, str2c +from .util import DataType, IOType, LibyangError, c2str, ly_array_iter, str2c LOG = logging.getLogger(__name__) @@ -53,11 +53,11 @@ def printer_flags( ) -> int: flags = 0 if with_siblings: - flags |= lib.LYD_PRINT_WITHSIBLINGS + flags |= lib.LYD_PRINT_SIBLINGS if not pretty: flags |= lib.LYD_PRINT_SHRINK if keep_empty_containers: - flags |= lib.LYD_PRINT_KEEPEMPTYCONT + flags |= lib.LYD_PRINT_EMPTY_CONT if trim_default_values: flags |= lib.LYD_PRINT_WD_TRIM if include_implicit_defaults: @@ -77,29 +77,48 @@ def data_format(fmt_string: str) -> int: # ------------------------------------------------------------------------------------- -def path_flags( - update: bool = False, rpc_output: bool = False, no_parent_ret: bool = False +def newval_flags( + rpc_output: bool = False, + store_only: bool = False, + bin_value: bool = False, + canon_value: bool = False, + meta_clear_default: bool = False, + update: bool = False, + opaq: bool = False, ) -> int: + """ + Translate from booleans to newvaloptions flags. + """ flags = 0 + if rpc_output: + flags |= lib.LYD_NEW_VAL_OUTPUT + if store_only: + flags |= lib.LYD_NEW_VAL_STORE_ONLY + if bin_value: + flags |= lib.LYD_NEW_VAL_BIN + if canon_value: + flags |= lib.LYD_NEW_VAL_CANON + if meta_clear_default: + flags |= lib.LYD_NEW_META_CLEAR_DFLT if update: flags |= lib.LYD_NEW_PATH_UPDATE - if rpc_output: - flags |= lib.LYD_NEW_PATH_OUTPUT + if opaq: + flags |= lib.LYD_NEW_PATH_OPAQ return flags # ------------------------------------------------------------------------------------- def parser_flags( - lyb_mod_update: bool = False, no_state: bool = False, parse_only: bool = False, opaq: bool = False, ordered: bool = False, strict: bool = False, + store_only: bool = False, + json_null: bool = False, + json_string_datatypes: bool = False, ) -> int: flags = 0 - if lyb_mod_update: - flags |= lib.LYD_PARSE_LYB_MOD_UPDATE if no_state: flags |= lib.LYD_PARSE_NO_STATE if parse_only: @@ -110,6 +129,12 @@ def parser_flags( flags |= lib.LYD_PARSE_ORDERED if strict: flags |= lib.LYD_PARSE_STRICT + if store_only: + flags |= lib.LYD_PARSE_STORE_ONLY + if json_null: + flags |= lib.LYD_PARSE_JSON_NULL + if json_string_datatypes: + flags |= lib.LYD_PARSE_JSON_STRING_DATATYPES return flags @@ -149,34 +174,25 @@ def dup_flags( # ------------------------------------------------------------------------------------- -def data_type(dtype): - if dtype == DataType.DATA_YANG: - return lib.LYD_TYPE_DATA_YANG - if dtype == DataType.RPC_YANG: - return lib.LYD_TYPE_RPC_YANG - if dtype == DataType.NOTIF_YANG: - return lib.LYD_TYPE_NOTIF_YANG - if dtype == DataType.REPLY_YANG: - return lib.LYD_TYPE_REPLY_YANG - if dtype == DataType.RPC_NETCONF: - return lib.LYD_TYPE_RPC_NETCONF - if dtype == DataType.NOTIF_NETCONF: - return lib.LYD_TYPE_NOTIF_NETCONF - if dtype == DataType.REPLY_NETCONF: - return lib.LYD_TYPE_REPLY_NETCONF - raise ValueError("Unknown data type") +def data_type(dtype: DataType) -> int: + if not isinstance(dtype, DataType): + dtype = DataType(dtype) + return dtype.value # ------------------------------------------------------------------------------------- def validation_flags( no_state: bool = False, validate_present: bool = False, + validate_multi_error: bool = False, ) -> int: flags = 0 if no_state: flags |= lib.LYD_VALIDATE_NO_STATE if validate_present: flags |= lib.LYD_VALIDATE_PRESENT + if validate_multi_error: + flags |= lib.LYD_VALIDATE_MULTI_ERROR return flags @@ -187,13 +203,69 @@ def diff_flags(with_defaults: bool = False) -> int: return flags +# ------------------------------------------------------------------------------------- +class DNodeAttrs: + __slots__ = ("context", "parent", "cdata", "__dict__") + + def __init__(self, context: "libyang.Context", parent: "libyang.DNode"): + self.context = context + self.parent = parent + self.cdata = [] # C type: "struct lyd_attr *" + + def get(self, name: str) -> Optional[str]: + for attr_name, attr_value in self: + if attr_name == name: + return attr_value + return None + + def set(self, name: str, value: str): + attrs = ffi.new("struct lyd_attr **") + ret = lib.lyd_new_attr( + self.parent.cdata, + ffi.NULL, + str2c(name), + str2c(value), + attrs, + ) + if ret != lib.LY_SUCCESS: + raise self.context.error("cannot create attr") + self.cdata.append(attrs[0]) + + def remove(self, name: str): + for attr in self.cdata: + if self._get_attr_name(attr) == name: + lib.lyd_free_attr_single(self.context.cdata, attr) + self.cdata.remove(attr) + break + + def __contains__(self, name: str) -> bool: + for attr_name, _ in self: + if attr_name == name: + return True + return False + + def __iter__(self) -> Iterator[Tuple[str, str]]: + for attr in self.cdata: + name = self._get_attr_name(attr) + yield (name, c2str(attr.value)) + + def __len__(self) -> int: + return len(self.cdata) + + @staticmethod + def _get_attr_name(cdata) -> str: + if cdata.name.prefix != ffi.NULL: + return f"{c2str(cdata.name.prefix)}:{c2str(cdata.name.name)}" + return c2str(cdata.name.name) + + # ------------------------------------------------------------------------------------- class DNode: """ Data tree node. """ - __slots__ = ("context", "cdata", "free_func") + __slots__ = ("context", "cdata", "attributes", "free_func", "__dict__") def __init__(self, context: "libyang.Context", cdata): """ @@ -204,9 +276,10 @@ def __init__(self, context: "libyang.Context", cdata): """ self.context = context self.cdata = cdata # C type: "struct lyd_node *" + self.attributes = None self.free_func = None # type: Callable[DNode] - def meta(self): + def meta(self) -> Dict[str, str]: ret = {} item = self.cdata.meta while item != ffi.NULL: @@ -218,7 +291,7 @@ def meta(self): item = item.next return ret - def get_meta(self, name): + def get_meta(self, name: str) -> Optional[str]: item = self.cdata.meta while item != ffi.NULL: if c2str(item.name) == name: @@ -230,7 +303,7 @@ def get_meta(self, name): item = item.next return None - def meta_free(self, name): + def meta_free(self, name: str): item = self.cdata.meta while item != ffi.NULL: if c2str(item.name) == name: @@ -238,25 +311,35 @@ def meta_free(self, name): break item = item.next - def new_meta(self, name: str, value: str, clear_dflt: bool = False): + def new_meta( + self, name: str, value: str, clear_dflt: bool = False, store_only: bool = False + ): + flags = newval_flags(meta_clear_default=clear_dflt, store_only=store_only) ret = lib.lyd_new_meta( ffi.NULL, self.cdata, ffi.NULL, str2c(name), str2c(value), - clear_dflt, + flags, ffi.NULL, ) if ret != lib.LY_SUCCESS: raise self.context.error("cannot create meta") + def attrs(self) -> DNodeAttrs: + if not self.attributes: + self.attributes = DNodeAttrs(self.context, self) + return self.attributes + def add_defaults( self, no_config: bool = False, no_defaults: bool = False, no_state: bool = False, output: bool = False, + only_node: bool = False, + only_module: Optional[Module] = None, ): flags = implicit_flags( no_config=no_config, @@ -264,9 +347,21 @@ def add_defaults( no_state=no_state, output=output, ) - node_p = ffi.new("struct lyd_node **") - node_p[0] = self.cdata - ret = lib.lyd_new_implicit_all(node_p, self.context.cdata, flags, ffi.NULL) + if only_node: + node_p = ffi.cast("struct lyd_node *", self.cdata) + ret = lib.lyd_new_implicit_tree(node_p, flags, ffi.NULL) + else: + node_p = ffi.new("struct lyd_node **") + node_p[0] = self.cdata + if only_module is not None: + ret = lib.lyd_new_implicit_module( + node_p, only_module.cdata, flags, ffi.NULL + ) + else: + ret = lib.lyd_new_implicit_all( + node_p, self.context.cdata, flags, ffi.NULL + ) + if ret != lib.LY_SUCCESS: raise self.context.error("cannot get module") @@ -280,6 +375,9 @@ def flags(self): ret["new"] = True return ret + def is_default(self) -> bool: + return lib.lyd_is_default(self.cdata) + def set_when(self, value: bool): if value: self.cdata.flags |= lib.LYD_WHEN_TRUE @@ -295,22 +393,18 @@ def new_path( opt_opaq: bool = False, opt_bin_value: bool = False, opt_canon_value: bool = False, + opt_store_only: bool = False, ): - - opt = 0 - if opt_update: - opt |= lib.LYD_NEW_PATH_UPDATE - if opt_output: - opt |= lib.LYD_NEW_PATH_OUTPUT - if opt_opaq: - opt |= lib.LYD_NEW_PATH_OPAQ - if opt_bin_value: - opt |= lib.LYD_NEW_PATH_BIN_VALUE - if opt_canon_value: - opt |= lib.LYD_NEW_PATH_CANON_VALUE - + flags = newval_flags( + update=opt_update, + rpc_output=opt_output, + opaq=opt_opaq, + bin_value=opt_bin_value, + canon_value=opt_canon_value, + store_only=opt_store_only, + ) ret = lib.lyd_new_path( - self.cdata, ffi.NULL, str2c(path), str2c(value), opt, ffi.NULL + self.cdata, ffi.NULL, str2c(path), str2c(value), flags, ffi.NULL ) if ret != lib.LY_SUCCESS: raise self.context.error("cannot get module") @@ -320,6 +414,21 @@ def insert_child(self, node): if ret != lib.LY_SUCCESS: raise self.context.error("cannot insert node") + def insert_sibling(self, node): + ret = lib.lyd_insert_sibling(self.cdata, node.cdata, ffi.NULL) + if ret != lib.LY_SUCCESS: + raise self.context.error("cannot insert sibling") + + def insert_after(self, node): + ret = lib.lyd_insert_after(self.cdata, node.cdata) + if ret != lib.LY_SUCCESS: + raise self.context.error("cannot insert sibling after") + + def insert_before(self, node): + ret = lib.lyd_insert_before(self.cdata, node.cdata) + if ret != lib.LY_SUCCESS: + raise self.context.error("cannot insert sibling before") + def name(self) -> str: return c2str(self.cdata.schema.name) @@ -405,11 +514,7 @@ def eval_xpath(self, xpath: str): return False def path(self) -> str: - path = lib.lyd_path(self.cdata, lib.LYD_PATH_STD, ffi.NULL, 0) - try: - return c2str(path) - finally: - lib.free(path) + return self._get_path(self.cdata) def validate( self, @@ -418,6 +523,7 @@ def validate( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + dep_tree: Optional["DNode"] = None, ) -> None: dtype = None if rpc: @@ -430,7 +536,7 @@ def validate( if dtype is None: self.validate_all(no_state, validate_present) else: - self.validate_op(dtype) + self.validate_op(dtype, dep_tree) def validate_all( self, @@ -452,11 +558,15 @@ def validate_all( def validate_op( self, dtype: DataType, + dep_tree: Optional["DNode"] = None, ) -> None: dtype = data_type(dtype) - node_p = ffi.new("struct lyd_node **") - node_p[0] = self.cdata - ret = lib.lyd_validate_op(node_p[0], ffi.NULL, dtype, ffi.NULL) + ret = lib.lyd_validate_op( + self.cdata, + ffi.NULL if dep_tree is None else dep_tree.cdata, + dtype, + ffi.NULL, + ) if ret != lib.LY_SUCCESS: raise self.context.error("validation failed") @@ -553,6 +663,8 @@ def merge( if ret != lib.LY_SUCCESS: raise self.context.error("merge failed") + self.cdata = node_p[0] + def iter_tree(self) -> Iterator["DNode"]: n = next_n = self.cdata while n != ffi.NULL: @@ -711,6 +823,17 @@ def print_dict( """ Convert a DNode object to a python dictionary. + The format is inspired by the YANG JSON format described in :rfc:`7951` but has + some differences: + + * ``int64`` and ``uint64`` values are represented by python ``int`` values + instead of string values. + * ``decimal64`` values are represented by python ``float`` values instead of + string values. + * ``empty`` values are represented by python ``None`` values instead of + ``[None]`` list instances. To check if an ``empty`` leaf is set in a + container, you should use the idiomatic ``if "foo" in container:`` construct. + :arg bool strip_prefixes: If True (the default), module prefixes are stripped from dictionary keys. If False, dictionary keys are in the form ``<module>:<name>``. @@ -856,6 +979,12 @@ def merge_data_dict( rpcreply=rpcreply, ) + def unlink(self, with_siblings: bool = False) -> None: + if with_siblings: + lib.lyd_unlink_siblings(self.cdata) + else: + lib.lyd_unlink_tree(self.cdata) + def free_internal(self, with_siblings: bool = True) -> None: if with_siblings: lib.lyd_free_all(self.cdata) @@ -869,7 +998,42 @@ def free(self, with_siblings: bool = True) -> None: else: self.free_internal(with_siblings) finally: - self.cdata = None + self.cdata = ffi.NULL + + def leafref_link_node_tree(self) -> None: + """ + Traverse through data tree including root node siblings and adds + leafrefs links to the given nodes. + + Requires leafref_linking to be set on the libyang context. + """ + lib.lyd_leafref_link_node_tree(self.cdata) + + def leafref_nodes(self) -> Iterator["DNode"]: + """ + Gets the nodes that are referring to this node. + + Requires leafref_linking to be set on the libyang context. + """ + term_node = ffi.cast("struct lyd_node_term *", self.cdata) + out = ffi.new("const struct lyd_leafref_links_rec **") + if lib.lyd_leafref_get_links(term_node, out) != lib.LY_SUCCESS: + return + for n in ly_array_iter(out[0].leafref_nodes): + yield DNode.new(self.context, n) + + def target_nodes(self) -> Iterator["DNode"]: + """ + Gets the target nodes that are referred by this node. + + Requires leafref_linking to be set on the libyang context. + """ + term_node = ffi.cast("struct lyd_node_term *", self.cdata) + out = ffi.new("const struct lyd_leafref_links_rec **") + if lib.lyd_leafref_get_links(term_node, out) != lib.LY_SUCCESS: + return + for n in ly_array_iter(out[0].target_nodes): + yield DNode.new(self.context, n) def __repr__(self): cls = self.__class__ @@ -892,20 +1056,38 @@ def _decorator(nodeclass): @classmethod def new(cls, context: "libyang.Context", cdata) -> "DNode": cdata = ffi.cast("struct lyd_node *", cdata) - nodecls = cls.NODETYPE_CLASS.get(cdata.schema.nodetype, None) + if not cdata.schema: + schemas = list(context.find_path(cls._get_path(cdata))) + if len(schemas) != 1: + raise LibyangError("Unable to determine schema") + nodecls = cls.NODETYPE_CLASS.get(schemas[0].nodetype(), None) + else: + nodecls = cls.NODETYPE_CLASS.get(cdata.schema.nodetype, None) if nodecls is None: raise TypeError("node type %s not implemented" % cdata.schema.nodetype) return nodecls(context, cdata) + @staticmethod + def _get_path(cdata) -> str: + path = lib.lyd_path(cdata, lib.LYD_PATH_STD, ffi.NULL, 0) + try: + return c2str(path) + finally: + lib.free(path) + # ------------------------------------------------------------------------------------- @DNode.register(SNode.CONTAINER) class DContainer(DNode): def create_path( - self, path: str, value: Any = None, rpc_output: bool = False + self, + path: str, + value: Any = None, + rpc_output: bool = False, + store_only: bool = False, ) -> Optional[DNode]: return self.context.create_data_path( - path, parent=self, value=value, rpc_output=rpc_output + path, parent=self, value=value, rpc_output=rpc_output, store_only=store_only ) def children(self, no_keys=False) -> Iterator[DNode]: @@ -944,44 +1126,35 @@ def value(self) -> Any: @staticmethod def cdata_leaf_value(cdata, context: "libyang.Context" = None) -> Any: - val = lib.lyd_get_value(cdata) if val == ffi.NULL: return None val = c2str(val) - term_node = ffi.cast("struct lyd_node_term *", cdata) - val_type = ffi.new("const struct lysc_type **", ffi.NULL) - - # get real value type - ctx = context.cdata if context else ffi.NULL - ret = lib.lyd_value_validate( - ctx, - term_node.schema, - str2c(val), - len(val), - ffi.NULL, - val_type, - ffi.NULL, - ) - - if ret in (lib.LY_SUCCESS, lib.LY_EINCOMPLETE): - val_type = val_type[0].basetype - if val_type in Type.STR_TYPES: - return val - if val_type in Type.NUM_TYPES: - return int(val) - if val_type == Type.BOOL: - return val == "true" - if val_type == Type.DEC64: - return float(val) - if val_type == Type.LEAFREF: - return DLeaf.cdata_leaf_value(cdata.value.leafref, context) - if val_type == Type.EMPTY: - return None + if cdata.schema == ffi.NULL: + # opaq node return val - raise TypeError("value type validation error") + node_term = ffi.cast("struct lyd_node_term *", cdata) + + # inspired from libyang lyd_value_validate + val_type = Type(context, node_term.value.realtype, None).base() + if val_type == Type.UNION: + val_type = Type( + context, node_term.value.subvalue.value.realtype, None + ).base() + + if val_type in Type.STR_TYPES: + return val + if val_type in Type.NUM_TYPES: + return int(val) + if val_type == Type.BOOL: + return val == "true" + if val_type == Type.DEC64: + return float(val) + if val_type == Type.EMPTY: + return None + return val # ------------------------------------------------------------------------------------- @@ -1030,6 +1203,7 @@ def dict_to_dnode( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + store_only: bool = False, ) -> Optional[DNode]: """ Convert a python dictionary to a DNode object given a YANG module object. The return @@ -1056,6 +1230,8 @@ def dict_to_dnode( Data represents RPC or action output parameters. :arg notification: Data represents notification parameters. + :arg store_only: + Data are being stored regardless of type validation (length, range, pattern, etc.) """ if not dic: return None @@ -1077,8 +1253,14 @@ def _create_leaf(_parent, module, name, value, in_rpc_output=False): value = str(value) n = ffi.new("struct lyd_node **") + flags = newval_flags(rpc_output=in_rpc_output, store_only=store_only) ret = lib.lyd_new_term( - _parent, module.cdata, str2c(name), str2c(value), in_rpc_output, n + _parent, + module.cdata, + str2c(name), + str2c(value), + flags, + n, ) if ret != lib.LY_SUCCESS: @@ -1109,11 +1291,12 @@ def _create_container(_parent, module, name, in_rpc_output=False): def _create_list(_parent, module, name, key_values, in_rpc_output=False): n = ffi.new("struct lyd_node **") + flags = newval_flags(rpc_output=in_rpc_output, store_only=store_only) ret = lib.lyd_new_list( _parent, module.cdata, str2c(name), - in_rpc_output, + flags, n, *[str2c(str(i)) for i in key_values], ) diff --git a/libyang/diff.py b/libyang/diff.py index 57099518..37441f14 100644 --- a/libyang/diff.py +++ b/libyang/diff.py @@ -12,6 +12,7 @@ def schema_diff( ctx_old: Context, ctx_new: Context, exclude_node_cb: Optional[Callable[[SNode], bool]] = None, + use_data_path: bool = False, ) -> Iterator["SNodeDiff"]: """ Compare two libyang Context objects, for a given set of paths and return all @@ -22,9 +23,12 @@ def schema_diff( :arg ctx_new: The second context. :arg exclude_node_cb: - Optionnal user callback that will be called with each node that is found in each + Optional 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). + :arg use_data_path: + Use data path instead of schema path to compare the nodes. Using data path + ignores choices and cases. :return: An iterator that yield `SNodeDiff` objects. @@ -40,7 +44,11 @@ def flatten(node, dic): """ if exclude_node_cb(node): return - dic[node.schema_path()] = node + if use_data_path: + path = node.data_path() + else: + path = node.schema_path() + dic[path] = node if isinstance(node, (SContainer, SList, SNotif, SRpc, SRpcInOut)): for child in node: flatten(child, dic) @@ -73,12 +81,11 @@ def flatten(node, dic): # ------------------------------------------------------------------------------------- class SNodeDiff: - pass + __slots__ = ("__dict__",) # ------------------------------------------------------------------------------------- class SNodeRemoved(SNodeDiff): - __slots__ = ("node",) def __init__(self, node: SNode): @@ -94,7 +101,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class SNodeAdded(SNodeDiff): - __slots__ = ("node",) def __init__(self, node: SNode): @@ -106,7 +112,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class SNodeAttributeChanged(SNodeDiff): - __slots__ = ("old", "new", "value") def __init__(self, old: SNode, new: SNode, value: Any = None): @@ -289,7 +294,6 @@ class UnitsAdded(SNodeAttributeChanged): # ------------------------------------------------------------------------------------- def snode_changes(old: SNode, new: SNode) -> Iterator[SNodeDiff]: - if old.nodetype() != new.nodetype(): yield NodeTypeRemoved(old, new, old.keyword()) yield NodeTypeAdded(old, new, new.keyword()) diff --git a/libyang/extension.py b/libyang/extension.py new file mode 100644 index 00000000..f7e4ba93 --- /dev/null +++ b/libyang/extension.py @@ -0,0 +1,226 @@ +# Copyright (c) 2018-2019 Robin Jarry +# Copyright (c) 2020 6WIND S.A. +# Copyright (c) 2021 RACOM s.r.o. +# SPDX-License-Identifier: MIT + +from typing import Callable, Optional + +from _libyang import ffi, lib +from .context import Context +from .log import get_libyang_level +from .schema import ExtensionCompiled, ExtensionParsed, Module, SNode +from .util import LibyangError, c2str, str2c + + +# ------------------------------------------------------------------------------------- +extensions_plugins = {} + + +class LibyangExtensionError(LibyangError): + def __init__(self, message: str, ret: int, log_level: int) -> None: + super().__init__(message) + self.ret = ret + self.log_level = log_level + + +@ffi.def_extern(name="lypy_lyplg_ext_parse_clb") +def libyang_c_lyplg_ext_parse_clb(pctx, pext): + plugin = extensions_plugins[lib.lysc_get_ext_plugin(pext.plugin_ref)] + module_cdata = lib.lyplg_ext_parse_get_cur_pmod(pctx).mod + context = Context(cdata=module_cdata.ctx) + module = Module(context, module_cdata) + parsed_ext = ExtensionParsed(context, pext, module) + plugin.set_parse_ctx(pctx) + try: + plugin.parse_clb(module, parsed_ext) + return lib.LY_SUCCESS + except LibyangExtensionError as e: + ly_level = get_libyang_level(e.log_level) + if ly_level is None: + ly_level = lib.LY_EPLUGIN + e_str = str(e) + plugin.add_error_message(e_str) + lib.lyplg_ext_parse_log(pctx, pext, ly_level, e.ret, str2c(e_str)) + return e.ret + + +@ffi.def_extern(name="lypy_lyplg_ext_compile_clb") +def libyang_c_lyplg_ext_compile_clb(cctx, pext, cext): + plugin = extensions_plugins[lib.lysc_get_ext_plugin(pext.plugin_ref)] + context = Context(cdata=lib.lyplg_ext_compile_get_ctx(cctx)) + module = Module(context, cext.module) + parsed_ext = ExtensionParsed(context, pext, module) + compiled_ext = ExtensionCompiled(context, cext) + plugin.set_compile_ctx(cctx) + try: + plugin.compile_clb(parsed_ext, compiled_ext) + return lib.LY_SUCCESS + except LibyangExtensionError as e: + ly_level = get_libyang_level(e.log_level) + if ly_level is None: + ly_level = lib.LY_EPLUGIN + e_str = str(e) + plugin.add_error_message(e_str) + lib.lyplg_ext_compile_log(cctx, cext, ly_level, e.ret, str2c(e_str)) + return e.ret + + +@ffi.def_extern(name="lypy_lyplg_ext_parse_free_clb") +def libyang_c_lyplg_ext_parse_free_clb(ctx, pext): + plugin = extensions_plugins[lib.lysc_get_ext_plugin(pext.plugin_ref)] + context = Context(cdata=ctx) + parsed_ext = ExtensionParsed(context, pext, None) + plugin.parse_free_clb(parsed_ext) + + +@ffi.def_extern(name="lypy_lyplg_ext_compile_free_clb") +def libyang_c_lyplg_ext_compile_free_clb(ctx, cext): + plugin = extensions_plugins[ + lib.lysc_get_ext_plugin(getattr(cext, "def").plugin_ref) + ] + context = Context(cdata=ctx) + compiled_ext = ExtensionCompiled(context, cext) + plugin.compile_free_clb(compiled_ext) + + +class ExtensionPlugin: + ERROR_SUCCESS = lib.LY_SUCCESS + ERROR_MEM = lib.LY_EMEM + ERROR_INVALID_INPUT = lib.LY_EINVAL + ERROR_NOT_VALID = lib.LY_EVALID + ERROR_DENIED = lib.LY_EDENIED + ERROR_NOT = lib.LY_ENOT + + def __init__( + self, + module_name: str, + name: str, + id_str: str, + context: Optional[Context] = None, + parse_clb: Optional[Callable[[Module, ExtensionParsed], None]] = None, + compile_clb: Optional[ + Callable[[ExtensionParsed, ExtensionCompiled], None] + ] = None, + parse_free_clb: Optional[Callable[[ExtensionParsed], None]] = None, + compile_free_clb: Optional[Callable[[ExtensionCompiled], None]] = None, + ) -> None: + """ + Set the callback functions, which will be called if libyang will be processing + given extension defined by name from module defined by module_name. + + :arg self: + This instance of extension plugin + :arg module_name: + The name of module in which the extension is defined + :arg name: + The name of extension itself + :arg id_str: + The unique ID of extension plugin within the libyang context + :arg context: + The context in which the extension plugin will be used. If set to None, + the plugin will be used for all existing and even future contexts + :arg parse_clb: + The optional callback function of which will be called during extension parsing + Expected arguments are: + module: The module which is being parsed + extension: The exact extension instance + Expected raises: + LibyangExtensionError in case of processing error + :arg compile_clb: + The optional callback function of which will be called during extension compiling + Expected arguments are: + extension_parsed: The parsed extension instance + extension_compiled: The compiled extension instance + Expected raises: + LibyangExtensionError in case of processing error + :arg parse_free_clb + The optional callback function of which will be called during freeing of parsed extension + Expected arguments are: + extension: The parsed extension instance to be freed + :arg compile_free_clb + The optional callback function of which will be called during freeing of compiled extension + Expected arguments are: + extension: The compiled extension instance to be freed + """ + self.context = context + self.module_name = module_name + self.module_name_cstr = str2c(self.module_name) + self.name = name + self.name_cstr = str2c(self.name) + self.id_str = id_str + self.id_cstr = str2c(self.id_str) + self.parse_clb = parse_clb + self.compile_clb = compile_clb + self.parse_free_clb = parse_free_clb + self.compile_free_clb = compile_free_clb + self._error_messages = [] + self._pctx = ffi.NULL + self._cctx = ffi.NULL + + self.cdata = ffi.new("struct lyplg_ext_record[2]") + self.cdata[0].module = self.module_name_cstr + self.cdata[0].name = self.name_cstr + self.cdata[0].plugin.id = self.id_cstr + if self.parse_clb is not None: + self.cdata[0].plugin.parse = lib.lypy_lyplg_ext_parse_clb + if self.compile_clb is not None: + self.cdata[0].plugin.compile = lib.lypy_lyplg_ext_compile_clb + if self.parse_free_clb is not None: + self.cdata[0].plugin.pfree = lib.lypy_lyplg_ext_parse_free_clb + if self.compile_free_clb is not None: + self.cdata[0].plugin.cfree = lib.lypy_lyplg_ext_compile_free_clb + ret = lib.lyplg_add_extension_plugin( + context.cdata if context is not None else ffi.NULL, + lib.LYPLG_EXT_API_VERSION, + ffi.cast("const void *", self.cdata), + ) + if ret != lib.LY_SUCCESS: + raise LibyangError("Unable to add extension plugin") + if self.cdata[0].plugin not in extensions_plugins: + extensions_plugins[self.cdata[0].plugin] = self + + def __del__(self) -> None: + if self.cdata[0].plugin in extensions_plugins: + del extensions_plugins[self.cdata[0].plugin] + + @staticmethod + def stmt2str(stmt: int) -> str: + return c2str(lib.lyplg_ext_stmt2str(stmt)) + + def add_error_message(self, err_msg: str) -> None: + self._error_messages.append(err_msg) + + def clear_error_messages(self) -> None: + self._error_messages.clear() + + def set_parse_ctx(self, pctx) -> None: + self._pctx = pctx + + def set_compile_ctx(self, cctx) -> None: + self._cctx = cctx + + def parse_substmts(self, ext: ExtensionParsed) -> int: + return lib.lyplg_ext_parse_extension_instance(self._pctx, ext.cdata) + + def compile_substmts( + self, + pext: ExtensionParsed, + cext: ExtensionCompiled, + parent: Optional[SNode] = None, + ) -> int: + return lib.lyplg_ext_compile_extension_instance( + self._cctx, + pext.cdata, + cext.cdata, + ffi.NULL if parent is None else parent.cdata, + ) + + def free_parse_substmts(self, ext: ExtensionParsed) -> None: + lib.lyplg_ext_pfree_instance_substatements( + self.context.cdata, ext.cdata.substmts + ) + + def free_compile_substmts(self, ext: ExtensionCompiled) -> None: + lib.lyplg_ext_cfree_instance_substatements( + self.context.cdata, ext.cdata.substmts + ) diff --git a/libyang/keyed_list.py b/libyang/keyed_list.py index 02b030fa..1a98af32 100644 --- a/libyang/keyed_list.py +++ b/libyang/keyed_list.py @@ -1,8 +1,9 @@ # Copyright (c) 2020 6WIND S.A. # SPDX-License-Identifier: MIT +from collections.abc import Hashable import copy -from typing import Any, Hashable, Iterable, Optional, Tuple, Union +from typing import Any, Iterable, Optional, Tuple, Union # ------------------------------------------------------------------------------------- diff --git a/libyang/log.py b/libyang/log.py index 2b241157..564cf604 100644 --- a/libyang/log.py +++ b/libyang/log.py @@ -2,6 +2,7 @@ # Copyright (c) 2020 6WIND S.A. # SPDX-License-Identifier: MIT +from contextlib import contextmanager import logging from _libyang import ffi, lib @@ -19,14 +20,35 @@ } +def get_libyang_level(py_level): + for ly_lvl, py_lvl in LOG_LEVELS.items(): + if py_lvl == py_level: + return ly_lvl + return None + + +@contextmanager +def temp_log_options(opt: int = 0): + opts = ffi.new("uint32_t *", opt) + + lib.ly_temp_log_options(opts) + yield + lib.ly_temp_log_options(ffi.NULL) + + @ffi.def_extern(name="lypy_log_cb") -def libyang_c_logging_callback(level, msg, path): +def libyang_c_logging_callback(level, msg, data_path, schema_path, line): args = [c2str(msg)] - if path: - fmt = "%s: %s" - args.append(c2str(path)) - else: - fmt = "%s" + fmt = "%s" + if data_path: + fmt += ": %s" + args.append(c2str(data_path)) + if schema_path: + fmt += ": %s" + args.append(c2str(schema_path)) + if line != 0: + fmt += " line %u" + args.append(line) LOG.log(LOG_LEVELS.get(level, logging.NOTSET), fmt, *args) @@ -45,16 +67,15 @@ def configure_logging(enable_py_logger: bool, level: int = logging.ERROR) -> Non :arg 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_log_level(ly_lvl) - break + ly_level = get_libyang_level(level) + if ly_level is not None: + lib.ly_log_level(ly_level) if enable_py_logger: lib.ly_log_options(lib.LY_LOLOG | lib.LY_LOSTORE) - lib.ly_set_log_clb(lib.lypy_log_cb, True) + lib.ly_set_log_clb(lib.lypy_log_cb) else: lib.ly_log_options(lib.LY_LOSTORE) - lib.ly_set_log_clb(ffi.NULL, False) + lib.ly_set_log_clb(ffi.NULL) configure_logging(False, logging.ERROR) diff --git a/libyang/schema.py b/libyang/schema.py index bf9c1647..d3f54654 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -3,10 +3,18 @@ # SPDX-License-Identifier: MIT from contextlib import suppress -from typing import IO, Any, Dict, Iterator, Optional, Tuple, Union +from typing import IO, Any, Dict, Iterator, List, Optional, Tuple, Union from _libyang import ffi, lib -from .util import IOType, c2str, init_output, ly_array_iter, str2c +from .util import ( + IOType, + LibyangError, + c2str, + init_output, + ly_array_iter, + ly_list_iter, + str2c, +) # ------------------------------------------------------------------------------------- @@ -44,8 +52,7 @@ def printer_flags( # ------------------------------------------------------------------------------------- class Module: - - __slots__ = ("context", "cdata") + __slots__ = ("context", "cdata", "__dict__") def __init__(self, context: "libyang.Context", cdata): self.context = context @@ -103,6 +110,11 @@ def features(self) -> Iterator["Feature"]: for i in features_list: yield Feature(self.context, i) + def compiled_enabled_features(self) -> Iterator[str]: + if self.cdata.compiled: + for f in ly_array_iter(self.cdata.compiled.features): + yield c2str(f) + def get_feature(self, name: str) -> "Feature": for f in self.features(): if f.name() == name: @@ -113,11 +125,88 @@ def revisions(self) -> Iterator["Revision"]: for revision in ly_array_iter(self.cdata.parsed.revs): yield Revision(self.context, revision, self) + def typedefs(self) -> Iterator["Typedef"]: + for typedef in ly_array_iter(self.cdata.parsed.typedefs): + yield Typedef(self.context, typedef) + + def get_typedef(self, name: str) -> Optional["Typedef"]: + for typedef in self.typedefs(): + if typedef.name() != name: + continue + return typedef + return None + + def imports(self) -> Iterator["Import"]: + for i in ly_array_iter(self.cdata.parsed.imports): + yield Import(self.context, i, self) + + def get_module_from_prefix(self, prefix: str) -> Optional["Module"]: + for i in self.imports(): + if i.prefix() != prefix: + continue + return self.context.get_module(i.name()) + return None + def __iter__(self) -> Iterator["SNode"]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator["SNode"]: - return iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator["SNode"]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) + + def parsed_children(self) -> Iterator["PNode"]: + for c in ly_list_iter(self.cdata.parsed.data): + yield PNode.new(self.context, c, self) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata.parsed.groupings): + yield PGrouping(self.context, g, self) + + def augments(self) -> Iterator["PAugment"]: + for a in ly_array_iter(self.cdata.parsed.augments): + yield PAugment(self.context, a, self) + + def actions(self) -> Iterator["PAction"]: + for a in ly_list_iter(self.cdata.parsed.rpcs): + yield PAction(self.context, a, self) + + def notifications(self) -> Iterator["PNotif"]: + for n in ly_list_iter(self.cdata.parsed.notifs): + yield PNotif(self.context, n, self) + + def identities(self) -> Iterator["Identity"]: + for i in ly_array_iter(self.cdata.identities): + yield Identity(self.context, i) + + def parsed_identities(self) -> Iterator["PIdentity"]: + for i in ly_array_iter(self.cdata.parsed.identities): + yield PIdentity(self.context, i, self) + + def extensions(self) -> Iterator["ExtensionCompiled"]: + compiled = ffi.cast("struct lysc_module *", self.cdata.compiled) + if compiled == ffi.NULL: + return + exts = ffi.cast("struct lysc_ext_instance *", self.cdata.compiled.exts) + if exts == ffi.NULL: + return + for extension in ly_array_iter(exts): + yield ExtensionCompiled(self.context, extension) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional["ExtensionCompiled"]: + 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 __str__(self) -> str: return self.name() @@ -188,6 +277,7 @@ def parse_data_dict( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + store_only: bool = False, ) -> "libyang.data.DNode": """ Convert a python dictionary to a DNode object following the schema of this @@ -223,13 +313,13 @@ def parse_data_dict( rpc=rpc, rpcreply=rpcreply, notification=notification, + store_only=store_only, ) # ------------------------------------------------------------------------------------- class Revision: - - __slots__ = ("context", "cdata", "module") + __slots__ = ("context", "cdata", "module", "__dict__") def __init__(self, context: "libyang.Context", cdata, module): self.context = context @@ -271,11 +361,56 @@ def __str__(self): # ------------------------------------------------------------------------------------- -class Extension: +class Import: + __slots__ = ("context", "cdata", "module", "__dict__") - __slots__ = ("context", "cdata") + def __init__(self, context: "libyang.Context", cdata, module): + self.context = context + self.cdata = cdata # C type: "struct lysp_import *" + self.module = module - def __init__(self, context: "libyang.Context", cdata, module_parent: Module = None): + def name(self) -> str: + return c2str(self.cdata.name) + + def prefix(self) -> Optional[str]: + return c2str(self.cdata.prefix) + + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) + + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) + + def extensions(self) -> Iterator["ExtensionParsed"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional["ExtensionParsed"]: + 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 Extension: + __slots__ = ("context", "cdata", "__dict__") + + def __init__(self, context: "libyang.Context", cdata): self.context = context self.cdata = cdata @@ -295,7 +430,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class ExtensionParsed(Extension): - __slots__ = ("module_parent",) def __init__(self, context: "libyang.Context", cdata, module_parent: Module = None): @@ -304,6 +438,8 @@ def __init__(self, context: "libyang.Context", cdata, module_parent: Module = No def _module_from_parsed(self) -> Module: prefix = c2str(self.cdata.name).split(":")[0] + if self.module_parent is None: + raise self.context.error("cannot get module") for cdata_imp_mod in ly_array_iter(self.module_parent.cdata.parsed.imports): if ffi.string(cdata_imp_mod.prefix).decode() == prefix: return Module(self.context, cdata_imp_mod.module) @@ -315,10 +451,35 @@ def name(self) -> str: def module(self) -> Module: return self._module_from_parsed() + def parent_node( + self, + ) -> Optional[Union["PNode", "PIdentity", "PRefine", "PType", "PEnum"]]: + if self.cdata.parent_stmt == lib.LY_STMT_IDENTITY: + cdata = ffi.cast("struct lysp_ident *", self.cdata.parent) + return PIdentity(self.context, cdata, self.module_parent) + if self.cdata.parent_stmt == lib.LY_STMT_REFINE: + cdata = ffi.cast("struct lysp_refine *", self.cdata.parent) + return PRefine(self.context, cdata, self.module_parent) + if self.cdata.parent_stmt == lib.LY_STMT_TYPE: + cdata = ffi.cast("struct lysp_type *", self.cdata.parent) + return PType(self.context, cdata, self.module_parent) + if self.cdata.parent_stmt == lib.LY_STMT_ENUM: + cdata = ffi.cast("struct lysp_type_enum *", self.cdata.parent) + return PEnum(self.context, cdata, self.module_parent) + if bool(self.cdata.parent_stmt & lib.LY_STMT_NODE_MASK): + try: + return PNode.new(self.context, self.cdata.parent, self.module_parent) + except LibyangError: + return None + return None + + def extensions(self) -> Iterator["ExtensionParsed"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module_parent) + # ------------------------------------------------------------------------------------- class ExtensionCompiled(Extension): - __slots__ = ("cdata_def",) def __init__(self, context: "libyang.Context", cdata): @@ -333,11 +494,25 @@ def module(self) -> Module: raise self.context.error("cannot get module") return Module(self.context, self.cdata_def.module) + def parent_node(self) -> Optional[Union["SNode", "Identity"]]: + if self.cdata.parent_stmt == lib.LY_STMT_IDENTITY: + cdata = ffi.cast("struct lysc_ident *", self.cdata.parent) + return Identity(self.context, cdata) + if bool(self.cdata.parent_stmt & lib.LY_STMT_NODE_MASK): + try: + return SNode.new(self.context, self.cdata.parent) + except LibyangError: + return None + return None + + def extensions(self) -> Iterator["ExtensionCompiled"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionCompiled(self.context, ext) + # ------------------------------------------------------------------------------------- class _EnumBit: - - __slots__ = ("context", "cdata") + __slots__ = ("context", "cdata", "__dict__") def __init__(self, context: "libyang.Context", cdata): self.context = context @@ -355,6 +530,23 @@ def name(self) -> str: def description(self) -> str: return c2str(self.cdata.dsc) + def extensions(self) -> Iterator[ExtensionCompiled]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionCompiled(self.context, ext) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional[ExtensionCompiled]: + 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 deprecated(self) -> bool: return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) @@ -387,10 +579,34 @@ class Bit(_EnumBit): # ------------------------------------------------------------------------------------- -class Type: - +class Pattern: __slots__ = ("context", "cdata", "cdata_parsed") + def __init__(self, context: "libyang.Context", cdata, cdata_parsed=None): + self.context = context + self.cdata = cdata # C type: "struct lysc_pattern *" + self.cdata_parsed = cdata_parsed # C type: "struct lysp_restr *" + + def expression(self) -> str: + if self.cdata is None and self.cdata_parsed: + return c2str(self.cdata_parsed.arg.str + 1) + return c2str(self.cdata.expr) + + def inverted(self) -> bool: + if self.cdata is None and self.cdata_parsed: + return self.cdata_parsed.arg.str[0] == b"\x15" + return self.cdata.inverted + + def error_message(self) -> Optional[str]: + if self.cdata is None and self.cdata_parsed: + return c2str(self.cdata_parsed.emsg) + return c2str(self.cdata.emsg) if self.cdata.emsg != ffi.NULL else None + + +# ------------------------------------------------------------------------------------- +class Type: + __slots__ = ("context", "cdata", "cdata_parsed", "__dict__") + UNKNOWN = lib.LY_TYPE_UNKNOWN BINARY = lib.LY_TYPE_BINARY UINT8 = lib.LY_TYPE_UINT8 @@ -455,6 +671,9 @@ def name(self) -> str: return self.basename() def description(self) -> Optional[str]: + typedef = self.typedef() + if typedef: + return typedef.description() return None def base(self) -> int: @@ -483,12 +702,45 @@ def leafref_path(self) -> Optional["str"]: lr = ffi.cast("struct lysc_type_leafref *", self.cdata) return c2str(lib.lyxp_get_expr(lr.path)) - def union_types(self) -> Iterator["Type"]: + def identity_bases(self) -> Iterator["Identity"]: + if self.cdata.basetype != lib.LY_TYPE_IDENT: + return + ident = ffi.cast("struct lysc_type_identityref *", self.cdata) + for b in ly_array_iter(ident.bases): + yield Identity(self.context, b) + + def typedef(self) -> "Typedef": + if ":" in self.name(): + module_prefix, type_name = self.name().split(":") + import_module = self.module().get_module_from_prefix(module_prefix) + if import_module: + return import_module.get_typedef(type_name) + return None + + def union_types(self, with_typedefs: bool = False) -> Iterator["Type"]: if self.cdata.basetype != self.UNION: return + + typedef = self.typedef() t = ffi.cast("struct lysc_type_union *", self.cdata) - for union_type in ly_array_iter(t.types): - yield Type(self.context, union_type, None) + if self.cdata_parsed and self.cdata_parsed.types != ffi.NULL: + for union_type, union_type_parsed in zip( + ly_array_iter(t.types), ly_array_iter(self.cdata_parsed.types) + ): + yield Type(self.context, union_type, union_type_parsed) + elif ( + with_typedefs + and typedef + and typedef.cdata + and typedef.cdata.type.types != ffi.NULL + ): + for union_type, union_type_parsed in zip( + ly_array_iter(t.types), ly_array_iter(typedef.cdata.type.types) + ): + yield Type(self.context, union_type, union_type_parsed) + else: + for union_type in ly_array_iter(t.types): + yield Type(self.context, union_type, None) def enums(self) -> Iterator[Enum]: if self.cdata.basetype != self.ENUM: @@ -532,6 +784,22 @@ def all_ranges(self) -> Iterator[str]: if rng is not None: yield rng + def fraction_digits(self) -> Optional[int]: + if not self.cdata_parsed: + return None + if self.cdata.basetype != self.DEC64: + return None + return self.cdata_parsed.fraction_digits + + def all_fraction_digits(self) -> Iterator[int]: + if self.cdata.basetype == lib.LY_TYPE_UNION: + for t in self.union_types(): + yield from t.all_fraction_digits() + else: + fd = self.fraction_digits() + if fd is not None: + yield fd + STR_TYPES = frozenset((STRING, BINARY, ENUM, IDENT, BITS)) def length(self) -> Optional[str]: @@ -575,12 +843,34 @@ def all_patterns(self) -> Iterator[Tuple[str, bool]]: else: yield from self.patterns() + def pattern_details(self) -> Iterator[Pattern]: + if self.cdata.basetype != self.STRING: + return + t = ffi.cast("struct lysc_type_str *", self.cdata) + if t.patterns == ffi.NULL: + return + for p in ly_array_iter(t.patterns): + if not p: + continue + yield Pattern(self.context, p) + + def all_pattern_details(self) -> Iterator[Pattern]: + if self.cdata.basetype == lib.LY_TYPE_UNION: + for t in self.union_types(): + yield from t.all_pattern_details() + else: + yield from self.pattern_details() + + def require_instance(self) -> Optional[bool]: + if self.cdata.basetype != self.LEAFREF: + return None + t = ffi.cast("struct lysc_type_leafref *", self.cdata) + return bool(t.require_instance) + def module(self) -> Module: - # TODO: pointer to the parsed module wehere is the type defined is in self.cdata_parsed.pmod - # however there is no way how to get name of the module from lysp_module - if not self.cdata.der.module: + if not self.cdata_parsed: return None - return Module(self.context, self.cdata.der.module) + return Module(self.context, self.cdata_parsed.pmod.mod) def extensions(self) -> Iterator[ExtensionCompiled]: for extension in ly_array_iter(self.cdata.exts): @@ -606,15 +896,19 @@ def __repr__(self): def __str__(self): return self.name() + def parsed(self) -> Optional["PType"]: + if self.cdata_parsed is None or self.cdata_parsed == ffi.NULL: + return None + return PType(self.context, self.cdata_parsed, self.module()) -# ------------------------------------------------------------------------------------- -class Feature: - __slots__ = ("context", "cdata") +# ------------------------------------------------------------------------------------- +class Typedef: + __slots__ = ("context", "cdata", "__dict__") def __init__(self, context: "libyang.Context", cdata): self.context = context - self.cdata = cdata # C type: "struct lysp_feature *" + self.cdata = cdata # C type: "struct lysp_tpdf *" def name(self) -> str: return c2str(self.cdata.name) @@ -622,11 +916,31 @@ def name(self) -> str: def description(self) -> Optional[str]: return c2str(self.cdata.dsc) + def units(self) -> Optional[str]: + return c2str(self.cdata.units) + def reference(self) -> Optional[str]: return c2str(self.cdata.ref) - def state(self) -> bool: - return bool(self.cdata.flags & lib.LYS_FENABLED) + def extensions(self) -> Iterator[ExtensionCompiled]: + ext = ffi.cast("struct lysc_ext_instance *", self.cdata.exts) + if ext == ffi.NULL: + return + for extension in ly_array_iter(ext): + yield ExtensionCompiled(self.context, extension) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional[ExtensionCompiled]: + 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 deprecated(self) -> bool: return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) @@ -634,16 +948,6 @@ def deprecated(self) -> bool: def obsolete(self) -> bool: return bool(self.cdata.flags & lib.LYS_STATUS_OBSLT) - def if_features(self) -> Iterator["IfFeatureExpr"]: - arr_length = ffi.cast("uint64_t *", self.cdata.iffeatures)[-1] - for i in range(arr_length): - yield IfFeatureExpr(self.context, self.cdata.iffeatures[i]) - - def test_all_if_features(self) -> Iterator["IfFeatureExpr"]: - for cdata_lysc_iffeature in ly_array_iter(self.cdata.iffeatures_c): - for cdata_feature in ly_array_iter(cdata_lysc_iffeature.features): - yield Feature(self.context, cdata_feature) - def module(self) -> Module: return Module(self.context, self.cdata.module) @@ -652,70 +956,174 @@ def __str__(self): # ------------------------------------------------------------------------------------- -class IfFeatureExpr: - - __slots__ = ("context", "cdata", "module_features", "compiled") +class Identity: + __slots__ = ("context", "cdata") - def __init__(self, context: "libyang.Context", cdata, module_features=None): - """ - if module_features is not None, it means we are using a parsed IfFeatureExpr - """ + def __init__(self, context: "libyang.Context", cdata): self.context = context - # Can be "struct lysc_iffeature *" if comes from module feature - # Can be "struct lysp_qname *" if comes from lysp_node - self.cdata = cdata - self.module_features = module_features - self.compiled = not module_features + self.cdata = cdata # C type: "struct lysc_ident *" - def _get_operator(self, position: int) -> int: - # 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.cdata.expr[position // 4] - result = item & (mask << shift) - return result >> shift + def name(self) -> str: + return c2str(self.cdata.name) - def _get_operands_parsed(self): - qname = ffi.string(self.cdata.str).decode() - tokens = qname.split() - operators = [] - features = [] - operators_map = { - "or": lib.LYS_IFF_OR, - "and": lib.LYS_IFF_AND, - "not": lib.LYS_IFF_NOT, - "f": lib.LYS_IFF_F, - } + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) - def get_feature(name): - for feature in self.module_features: - if feature.name() == name: - return feature.cdata - raise Exception("No feature %s in module" % name) + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) - def parse_iffeature(tokens): - def oper2(op): - op_index = tokens.index(op) - operators.append(operators_map[op]) - left, right = tokens[:op_index], tokens[op_index + 1 :] - parse_iffeature(left) - parse_iffeature(right) + def module(self) -> Module: + return Module(self.context, self.cdata.module) - def oper1(op): - op_index = tokens.index(op) - feature_name = tokens[op_index + 1] - operators.append(operators_map[op]) - operators.append(operators_map["f"]) - features.append(get_feature(feature_name)) + def derived(self) -> Iterator["Identity"]: + for i in ly_array_iter(self.cdata.derived): + yield Identity(self.context, i) - oper_map = {"or": oper2, "and": oper2, "not": oper1} - for op, fun in oper_map.items(): - with suppress(ValueError): - fun(op) - return + def extensions(self) -> Iterator[ExtensionCompiled]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionCompiled(self.context, ext) - # Token is a feature + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional[ExtensionCompiled]: + 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 deprecated(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) + + def obsolete(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_OBSLT) + + def status(self) -> str: + if self.cdata.flags & lib.LYS_STATUS_OBSLT: + return "obsolete" + if self.cdata.flags & lib.LYS_STATUS_DEPRC: + return "deprecated" + return "current" + + def __repr__(self): + cls = self.__class__ + return "<%s.%s: %s>" % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + +# ------------------------------------------------------------------------------------- +class Feature: + __slots__ = ("context", "cdata", "__dict__") + + def __init__(self, context: "libyang.Context", cdata): + self.context = context + self.cdata = cdata # C type: "struct lysp_feature *" + + def name(self) -> str: + return c2str(self.cdata.name) + + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) + + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) + + def state(self) -> bool: + return bool(self.cdata.flags & lib.LYS_FENABLED) + + def deprecated(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) + + def obsolete(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_OBSLT) + + def if_features(self) -> Iterator["IfFeatureExpr"]: + arr_length = ffi.cast("uint64_t *", self.cdata.iffeatures)[-1] + for i in range(arr_length): + yield IfFeatureExpr(self.context, self.cdata.iffeatures[i]) + + def test_all_if_features(self) -> Iterator["IfFeatureExpr"]: + for cdata_lysc_iffeature in ly_array_iter(self.cdata.iffeatures_c): + for cdata_feature in ly_array_iter(cdata_lysc_iffeature.features): + yield Feature(self.context, cdata_feature) + + def module(self) -> Module: + return Module(self.context, self.cdata.module) + + def __str__(self): + return self.name() + + +# ------------------------------------------------------------------------------------- +class IfFeatureExpr: + __slots__ = ("context", "cdata", "module_features", "compiled", "__dict__") + + def __init__(self, context: "libyang.Context", cdata, module_features=None): + """ + if module_features is not None, it means we are using a parsed IfFeatureExpr + """ + self.context = context + # Can be "struct lysc_iffeature *" if comes from module feature + # Can be "struct lysp_qname *" if comes from lysp_node + self.cdata = cdata + self.module_features = module_features + self.compiled = not module_features + + def _get_operator(self, position: int) -> int: + # 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.cdata.expr[position // 4] + result = item & (mask << shift) + return result >> shift + + def _get_operands_parsed(self): + qname = ffi.string(self.cdata.str).decode() + tokens = qname.split() + operators = [] + features = [] + operators_map = { + "or": lib.LYS_IFF_OR, + "and": lib.LYS_IFF_AND, + "not": lib.LYS_IFF_NOT, + "f": lib.LYS_IFF_F, + } + + def get_feature(name): + for feature in self.module_features: + if feature.name() == name: + return feature.cdata + raise LibyangError("No feature %s in module" % name) + + def parse_iffeature(tokens): + def oper2(op): + op_index = tokens.index(op) + operators.append(operators_map[op]) + left, right = tokens[:op_index], tokens[op_index + 1 :] + parse_iffeature(left) + parse_iffeature(right) + + def oper1(op): + op_index = tokens.index(op) + feature_name = tokens[op_index + 1] + operators.append(operators_map[op]) + operators.append(operators_map["f"]) + features.append(get_feature(feature_name)) + + oper_map = {"or": oper2, "and": oper2, "not": oper1} + for op, fun in oper_map.items(): + with suppress(ValueError): + fun(op) + return + + # Token is a feature operators.append(operators_map["f"]) features.append(get_feature(tokens[0])) @@ -789,7 +1197,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class IfFeature(IfFeatureExprTree): - __slots__ = ("context", "cdata") def __init__(self, context: "libyang.Context", cdata): @@ -812,7 +1219,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class IfNotFeature(IfFeatureExprTree): - __slots__ = ("context", "child") def __init__(self, context: "libyang.Context", child: IfFeatureExprTree): @@ -831,7 +1237,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class IfAndFeatures(IfFeatureExprTree): - __slots__ = ("context", "a", "b") def __init__( @@ -856,7 +1261,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- class IfOrFeatures(IfFeatureExprTree): - __slots__ = ("context", "a", "b") def __init__( @@ -880,11 +1284,32 @@ def __str__(self): # ------------------------------------------------------------------------------------- -class SNode: - +class Must: __slots__ = ("context", "cdata", "cdata_parsed") + def __init__(self, context: "libyang.Context", cdata, cdata_parsed=None): + self.context = context + self.cdata = cdata # C type: "struct lysc_must *" + self.cdata_parsed = cdata_parsed # C type: "struct lysp_must *" + + def condition(self) -> str: + if self.cdata is None and self.cdata_parsed: + return c2str(self.cdata_parsed.arg.str + 1) + return c2str(lib.lyxp_get_expr(self.cdata.cond)) + + def error_message(self) -> Optional[str]: + if self.cdata is None and self.cdata_parsed: + return c2str(self.cdata_parsed.emsg) + return c2str(self.cdata.emsg) if self.cdata.emsg != ffi.NULL else None + + +# ------------------------------------------------------------------------------------- +class SNode: + __slots__ = ("context", "cdata", "cdata_parsed", "__dict__") + CONTAINER = lib.LYS_CONTAINER + CHOICE = lib.LYS_CHOICE + CASE = lib.LYS_CASE LEAF = lib.LYS_LEAF LEAFLIST = lib.LYS_LEAFLIST LIST = lib.LYS_LIST @@ -909,6 +1334,10 @@ class SNode: ANYDATA: "anydata", } + PATH_LOG = lib.LYSC_PATH_LOG + PATH_DATA = lib.LYSC_PATH_DATA + PATH_DATA_PATTERN = lib.LYSC_PATH_DATA_PATTERN + def __init__(self, context: "libyang.Context", cdata): self.context = context self.cdata = cdata # C type: "struct lysc_node *" @@ -954,22 +1383,19 @@ def status(self) -> str: def module(self) -> Module: return Module(self.context, self.cdata.module) - def schema_path(self) -> str: + def schema_path(self, path_type: int = PATH_LOG) -> str: try: - s = lib.lysc_path(self.cdata, lib.LYSC_PATH_LOG, ffi.NULL, 0) + s = lib.lysc_path(self.cdata, path_type, ffi.NULL, 0) return c2str(s) finally: lib.free(s) def data_path(self, key_placeholder: str = "'%s'") -> str: - try: - s = lib.lysc_path(self.cdata, lib.LYSC_PATH_DATA_PATTERN, ffi.NULL, 0) - val = c2str(s) - if key_placeholder != "'%s'": - val = val.replace("'%s'", key_placeholder) - return val - finally: - lib.free(s) + val = self.schema_path(self.PATH_DATA_PATTERN) + + if key_placeholder != "'%s'": + val = val.replace("'%s'", key_placeholder) + return val def extensions(self) -> Iterator[ExtensionCompiled]: ext = ffi.cast("struct lysc_ext_instance *", self.cdata.exts) @@ -979,7 +1405,17 @@ def extensions(self) -> Iterator[ExtensionCompiled]: yield ExtensionCompiled(self.context, extension) def must_conditions(self) -> Iterator[str]: - return iter(()) + for must in self.musts(): + yield must.condition() + + def musts(self) -> Iterator[Must]: + mc = lib.lysc_node_musts(self.cdata) + if mc == ffi.NULL: + return + for m in ly_array_iter(mc): + if not m: + continue + yield Must(self.context, m) def get_extension( self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None @@ -1009,13 +1445,28 @@ def parent(self) -> Optional["SNode"]: return None def when_conditions(self): - wh = ffi.new("struct lysc_when **") wh = lib.lysc_node_when(self.cdata) if wh == ffi.NULL: return for cond in ly_array_iter(wh): yield c2str(lib.lyxp_get_expr(cond.cond)) + def when_conditions_nodes(self) -> Iterator[Optional["SNode"]]: + wh = lib.lysc_node_when(self.cdata) + if wh == ffi.NULL: + return + for cond in ly_array_iter(wh): + yield ( + None + if cond.context == ffi.NULL + else SNode.new(self.context, cond.context) + ) + + def parsed(self) -> Optional["PNode"]: + if self.cdata_parsed is None or self.cdata_parsed == ffi.NULL: + return None + return PNode.new(self.context, self.cdata_parsed, self.module()) + def iter_tree(self, full: bool = False) -> Iterator["SNode"]: """ Do a DFS walk of the schema node. @@ -1076,46 +1527,58 @@ def new(context: "libyang.Context", cdata) -> "SNode": # ------------------------------------------------------------------------------------- @SNode.register(SNode.LEAF) class SLeaf(SNode): - - __slots__ = ("cdata_leaf", "cdata_leaf_parsed") + __slots__ = ("cdata_leaf", "cdata_leaf_parsed", "cdata_default_realtype") def __init__(self, context: "libyang.Context", cdata): super().__init__(context, cdata) self.cdata_leaf = ffi.cast("struct lysc_node_leaf *", cdata) self.cdata_leaf_parsed = ffi.cast("struct lysp_node_leaf *", self.cdata_parsed) + self.cdata_default_realtype = None - def default(self) -> Union[None, bool, int, str]: - if not self.cdata_leaf.dflt: + def default(self) -> Union[None, bool, int, str, float]: + if not self.cdata_leaf.dflt.str: return None - val = lib.lyd_value_get_canonical(self.context.cdata, self.cdata_leaf.dflt) - if not val: - return None - val = c2str(val) - val_type = Type(self.context, self.cdata_leaf.dflt.realtype, None) + + if self.cdata_default_realtype is None: + # calculate real type of default value just once + val_type_cdata = ffi.new("struct lysc_type **", ffi.NULL) + ret = lib.lyd_value_validate_dflt( + self.cdata, + self.cdata_leaf.dflt.str, + self.cdata_leaf.dflt.prefixes, + ffi.NULL, + val_type_cdata, + ffi.NULL, + ) + if ret not in (lib.LY_SUCCESS, lib.LY_EINCOMPLETE): + raise self.context.error("Unable to get real type of default value") + self.cdata_default_realtype = Type(self.context, val_type_cdata[0], None) + + val = c2str(self.cdata_leaf.dflt.str) + val_type = self.cdata_default_realtype if val_type.base() == Type.BOOL: return val == "true" if val_type.base() in Type.NUM_TYPES: return int(val) + if val_type.base() == Type.DEC64: + return float(val) return val def units(self) -> Optional[str]: return c2str(self.cdata_leaf.units) def type(self) -> Type: - return Type(self.context, self.cdata_leaf.type, self.cdata_leaf_parsed.type) + return Type( + self.context, + self.cdata_leaf.type, + self.cdata_leaf_parsed.type if self.cdata_leaf_parsed else None, + ) def is_key(self) -> bool: if self.cdata_leaf.flags & lib.LYS_KEY: return True return False - def must_conditions(self) -> Iterator[str]: - pdata = self.cdata_leaf_parsed - if pdata.musts == ffi.NULL: - return - for must in ly_array_iter(pdata.musts): - yield c2str(must.arg.str) - def __str__(self): return "%s %s" % (self.name(), self.type().name()) @@ -1123,8 +1586,7 @@ def __str__(self): # ------------------------------------------------------------------------------------- @SNode.register(SNode.LEAFLIST) class SLeafList(SNode): - - __slots__ = ("cdata_leaflist", "cdata_leaflist_parsed") + __slots__ = ("cdata_leaflist", "cdata_leaflist_parsed", "cdata_default_realtypes") def __init__(self, context: "libyang.Context", cdata): super().__init__(context, cdata) @@ -1132,6 +1594,7 @@ def __init__(self, context: "libyang.Context", cdata): self.cdata_leaflist_parsed = ffi.cast( "struct lysp_node_leaflist *", self.cdata_parsed ) + self.cdata_default_realtypes = None def ordered(self) -> bool: return bool(self.cdata_parsed.flags & lib.LYS_ORDBY_USER) @@ -1141,33 +1604,61 @@ def units(self) -> Optional[str]: def type(self) -> Type: return Type( - self.context, self.cdata_leaflist.type, self.cdata_leaflist_parsed.type + self.context, + self.cdata_leaflist.type, + self.cdata_leaflist_parsed.type if self.cdata_leaflist_parsed else None, ) - def defaults(self) -> Iterator[str]: + def defaults(self) -> Iterator[Union[None, bool, int, str, float]]: if self.cdata_leaflist.dflts == ffi.NULL: return - arr_length = ffi.cast("uint64_t *", self.cdata_leaflist.dflts)[-1] - for i in range(arr_length): - val = lib.lyd_value_get_canonical( - self.context.cdata, self.cdata_leaflist.dflts[i] - ) - if not val: + + if self.cdata_default_realtypes is None: + # calculate real types of default values just once + val_type_cdata = ffi.new("struct lysc_type **", ffi.NULL) + self.cdata_default_realtypes = [] + for dflt in ly_array_iter(self.cdata_leaflist.dflts): + if not dflt.str: + self.cdata_default_realtypes.append(None) + continue + val_type_cdata[0] = ffi.NULL + ret = lib.lyd_value_validate_dflt( + self.cdata, + dflt.str, + dflt.prefixes, + ffi.NULL, + val_type_cdata, + ffi.NULL, + ) + if ret not in (lib.LY_SUCCESS, lib.LY_EINCOMPLETE): + raise self.context.error("Unable to get real type of default value") + self.cdata_default_realtypes.append( + Type(self.context, val_type_cdata[0], None) + ) + + for dflt, val_type in zip( + ly_array_iter(self.cdata_leaflist.dflts), self.cdata_default_realtypes + ): + if not dflt.str: yield None - ret = c2str(val) - val_type = self.cdata_leaflist.dflts[i].realtype - if val_type == Type.BOOL: - ret = val == "true" - elif val_type in Type.NUM_TYPES: - ret = int(val) - yield ret + continue + val = c2str(dflt.str) + if val_type.base() == Type.BOOL: + yield val == "true" + elif val_type.base() in Type.NUM_TYPES: + yield int(val) + elif val_type.base() == Type.DEC64: + yield float(val) + else: + yield val + + def max_elements(self) -> Optional[int]: + return ( + self.cdata_leaflist.max if self.cdata_leaflist.max != (2**32 - 1) else None + ) - def must_conditions(self) -> Iterator[str]: - pdata = self.cdata_leaflist_parsed - if pdata.musts == ffi.NULL: - return - for must in ly_array_iter(pdata.musts): - yield c2str(must.arg.str) + def min_elements(self) -> int: + return self.cdata_leaflist.min def __str__(self): return "%s %s" % (self.name(), self.type().name()) @@ -1176,7 +1667,6 @@ def __str__(self): # ------------------------------------------------------------------------------------- @SNode.register(SNode.CONTAINER) class SContainer(SNode): - __slots__ = ("cdata_container", "cdata_container_parsed") def __init__(self, context: "libyang.Context", cdata): @@ -1192,24 +1682,57 @@ def presence(self) -> Optional[str]: return c2str(self.cdata_container_parsed.presence) - def must_conditions(self) -> Iterator[str]: - pdata = self.cdata_container_parsed - if pdata.musts == ffi.NULL: - return - for must in ly_array_iter(pdata.musts): - yield c2str(must.arg.str) + def __iter__(self) -> Iterator[SNode]: + return self.children() + + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) + + +# ------------------------------------------------------------------------------------- +@SNode.register(SNode.CHOICE) +class SChoice(SNode): + __slots__ = ("cdata_choice",) + + def __init__(self, context: "libyang.Context", cdata): + super().__init__(context, cdata) + self.cdata_choice = ffi.cast("struct lysc_node_choice *", cdata) + + def __iter__(self) -> Iterator[SNode]: + return self.children() + + def children( + self, types: Optional[Tuple[int, ...]] = None, with_case: bool = False + ) -> Iterator[SNode]: + return iter_children(self.context, self.cdata, types=types, with_case=with_case) + + def default(self) -> Optional[SNode]: + if self.cdata_choice.dflt == ffi.NULL: + return None + return SNode.new(self.context, self.cdata_choice.dflt) + +# ------------------------------------------------------------------------------------- +@SNode.register(SNode.CASE) +class SCase(SNode): def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - return iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- @SNode.register(SNode.LIST) class SList(SNode): - __slots__ = ("cdata_list", "cdata_list_parsed") def __init__(self, context: "libyang.Context", cdata): @@ -1224,9 +1747,18 @@ def __iter__(self) -> Iterator[SNode]: return self.children() def children( - self, skip_keys: bool = False, types: Optional[Tuple[int, ...]] = None + self, + skip_keys: bool = False, + types: Optional[Tuple[int, ...]] = None, + with_choice: bool = False, ) -> Iterator[SNode]: - return iter_children(self.context, self.cdata, skip_keys=skip_keys, types=types) + return iter_children( + self.context, + self.cdata, + skip_keys=skip_keys, + types=types, + with_choice=with_choice, + ) def keys(self) -> Iterator[SNode]: node = lib.lysc_node_child(self.cdata) @@ -1235,12 +1767,18 @@ def keys(self) -> Iterator[SNode]: yield SLeaf(self.context, node) node = node.next - def must_conditions(self) -> Iterator[str]: - pdata = self.cdata_list_parsed - if pdata.musts == ffi.NULL: - return - for must in ly_array_iter(pdata.musts): - yield c2str(must.arg.str) + def uniques(self) -> Iterator[List[SNode]]: + for unique in ly_array_iter(self.cdata_list.uniques): + nodes = [] + for node in ly_array_iter(unique): + nodes.append(SNode.new(self.context, node)) + yield nodes + + def max_elements(self) -> Optional[int]: + return self.cdata_list.max if self.cdata_list.max != (2**32 - 1) else None + + def min_elements(self) -> int: + return self.cdata_list.min def __str__(self): return "%s [%s]" % (self.name(), ", ".join(k.name() for k in self.keys())) @@ -1253,8 +1791,12 @@ class SRpcInOut(SNode): def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - return iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- @@ -1284,12 +1826,16 @@ def output(self) -> Optional[SRpcInOut]: def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - yield from iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + yield from iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # With libyang2, you can get only input or output # To keep behavior, we iter 2 times witt output options yield from iter_children( - self.context, self.cdata, types=types, options=lib.LYS_GETNEXT_OUTPUT + self.context, self.cdata, types=types, output=True, with_choice=with_choice ) @@ -1299,8 +1845,12 @@ class SNotif(SNode): def __iter__(self) -> Iterator[SNode]: return self.children() - def children(self, types: Optional[Tuple[int, ...]] = None) -> Iterator[SNode]: - return iter_children(self.context, self.cdata, types=types) + def children( + self, types: Optional[Tuple[int, ...]] = None, with_choice: bool = False + ) -> Iterator[SNode]: + return iter_children( + self.context, self.cdata, types=types, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- @@ -1315,13 +1865,43 @@ class SAnydata(SNode): pass +# ------------------------------------------------------------------------------------- +def iter_children_options( + with_choice: bool = False, + no_choice: bool = False, + with_case: bool = False, + into_non_presence_container: bool = False, + output: bool = False, + with_schema_mount: bool = False, +) -> int: + options = 0 + if with_choice: + options |= lib.LYS_GETNEXT_WITHCHOICE + if no_choice: + options |= lib.LYS_GETNEXT_NOCHOICE + if with_case: + options |= lib.LYS_GETNEXT_WITHCASE + if into_non_presence_container: + options |= lib.LYS_GETNEXT_INTONPCONT + if output: + options |= lib.LYS_GETNEXT_OUTPUT + if with_schema_mount: + options |= lib.LYS_GETNEXT_WITHSCHEMAMOUNT + return options + + # ------------------------------------------------------------------------------------- def iter_children( context: "libyang.Context", parent, # C type: Union["struct lys_module *", "struct lys_node *"] skip_keys: bool = False, types: Optional[Tuple[int, ...]] = None, - options: int = 0, + with_choice: bool = False, + no_choice: bool = False, + with_case: bool = False, + into_non_presence_container: bool = False, + output: bool = False, + with_schema_mount: bool = False, ) -> Iterator[SNode]: if types is None: types = ( @@ -1332,6 +1912,8 @@ def iter_children( lib.LYS_LEAF, lib.LYS_LEAFLIST, lib.LYS_NOTIF, + lib.LYS_CHOICE, + lib.LYS_CASE, ) def _skip(node) -> bool: @@ -1347,11 +1929,21 @@ def _skip(node) -> bool: return False if ffi.typeof(parent) == ffi.typeof("struct lys_module *"): + if parent.compiled == ffi.NULL: + return module = parent.compiled parent = ffi.NULL else: module = ffi.NULL + options = iter_children_options( + with_choice=with_choice, + no_choice=no_choice, + with_case=with_case, + into_non_presence_container=into_non_presence_container, + output=output, + with_schema_mount=with_schema_mount, + ) child = lib.lys_getnext(ffi.NULL, parent, module, options) while child: if not _skip(child): @@ -1369,3 +1961,739 @@ def _skip(node) -> bool: Rpc = SRpc RpcInOut = SRpcInOut Anyxml = SAnyxml + + +# ------------------------------------------------------------------------------------- +class PEnum: + __slots__ = ("context", "cdata", "module") + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + self.context = context + self.cdata = cdata # C type of "struct lysp_type_enum *" + self.module = module + + def name(self) -> str: + return c2str(self.cdata.name) + + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) + + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) + + def value(self) -> int: + return self.cdata.value + + def if_features(self) -> Iterator[IfFeatureExpr]: + for f in ly_array_iter(self.cdata.iffeatures): + yield IfFeatureExpr(self.context, f, list(self.module.features())) + + def extensions(self) -> Iterator["ExtensionParsed"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module) + + +# ------------------------------------------------------------------------------------- +class PType: + __slots__ = ("context", "cdata", "module") + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + self.context = context + self.cdata = cdata # C type of "struct lysp_type *" + self.module = module + + def name(self) -> str: + return c2str(self.cdata.name) + + def range(self) -> Optional[str]: + if self.cdata.range == ffi.NULL: + return None + return c2str(self.cdata.range.arg.str) + + def length(self) -> Optional[str]: + if self.cdata.length == ffi.NULL: + return None + return c2str(self.cdata.length.arg.str) + + def patterns(self) -> Iterator[Pattern]: + for p in ly_array_iter(self.cdata.patterns): + yield Pattern(self.context, None, p) + + def enums(self) -> Iterator[PEnum]: + for e in ly_array_iter(self.cdata.enums): + yield PEnum(self.context, e, self.module) + + def bits(self) -> Iterator[PEnum]: + for b in ly_array_iter(self.cdata.bits): + yield PEnum(self.context, b, self.module) + + def path(self) -> Optional[str]: + if self.cdata.path == ffi.NULL: + return None + return c2str(lib.lyxp_get_expr(self.cdata.path)) + + def bases(self) -> Iterator[str]: + for b in ly_array_iter(self.cdata.bases): + yield c2str(b) + + def types(self) -> Iterator["PType"]: + for t in ly_array_iter(self.cdata.types): + yield PType(self.context, t, self.module) + + def extensions(self) -> Iterator["ExtensionParsed"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module) + + def pmod(self) -> Optional[Module]: + if self.cdata.pmod == ffi.NULL: + return None + return Module(self.context, self.cdata.pmod.mod) + + def compiled(self) -> Optional[Type]: + if self.cdata.compiled == ffi.NULL: + return None + return Type(self.context, self.cdata.compiled, self.cdata) + + def fraction_digits(self) -> int: + return self.cdata.fraction_digits + + def require_instance(self) -> bool: + return self.cdata.require_instance + + +# ------------------------------------------------------------------------------------- +class PRefine: + __slots__ = ("context", "cdata", "module") + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + self.context = context + self.cdata = cdata # C type of "struct lysp_refine *" + self.module = module + + def nodeid(self) -> str: + return c2str(self.cdata.nodeid) + + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) + + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) + + def if_features(self) -> Iterator[IfFeatureExpr]: + for f in ly_array_iter(self.cdata.iffeatures): + yield IfFeatureExpr(self.context, f, list(self.module.features())) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata.musts): + yield Must(self.context, None, m) + + def presence(self) -> Optional[str]: + return c2str(self.cdata.presence) + + def defaults(self) -> Iterator[str]: + for d in ly_array_iter(self.cdata.dflts): + yield c2str(d.str) + + def min_elements(self) -> int: + return self.cdata.min + + def max_elements(self) -> Optional[int]: + return self.cdata.max if self.cdata.max != 0 else None + + def extensions(self) -> Iterator["ExtensionParsed"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module) + + +# ------------------------------------------------------------------------------------- +class PIdentity: + __slots__ = ("context", "cdata", "module") + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + self.context = context + self.cdata = cdata # C type: "struct lysp_ident *" + self.module = module + + def name(self) -> str: + return c2str(self.cdata.name) + + def if_features(self) -> Iterator[IfFeatureExpr]: + for f in ly_array_iter(self.cdata.iffeatures): + yield IfFeatureExpr(self.context, f, list(self.module.features())) + + def bases(self) -> Iterator[str]: + for b in ly_array_iter(self.cdata.bases): + yield c2str(b) + + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) + + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) + + def extensions(self) -> Iterator[ExtensionParsed]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module) + + def deprecated(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) + + def obsolete(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_OBSLT) + + def status(self) -> str: + if self.cdata.flags & lib.LYS_STATUS_OBSLT: + return "obsolete" + if self.cdata.flags & lib.LYS_STATUS_DEPRC: + return "deprecated" + return "current" + + def __repr__(self): + cls = self.__class__ + return "<%s.%s: %s>" % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + +# ------------------------------------------------------------------------------------- +class PNode: + CONTAINER = lib.LYS_CONTAINER + CHOICE = lib.LYS_CHOICE + CASE = lib.LYS_CASE + LEAF = lib.LYS_LEAF + LEAFLIST = lib.LYS_LEAFLIST + LIST = lib.LYS_LIST + RPC = lib.LYS_RPC + ACTION = lib.LYS_ACTION + INPUT = lib.LYS_INPUT + OUTPUT = lib.LYS_OUTPUT + NOTIF = lib.LYS_NOTIF + ANYXML = lib.LYS_ANYXML + ANYDATA = lib.LYS_ANYDATA + AUGMENT = lib.LYS_AUGMENT + USES = lib.LYS_USES + GROUPING = lib.LYS_GROUPING + KEYWORDS = { + CONTAINER: "container", + LEAF: "leaf", + LEAFLIST: "leaf-list", + LIST: "list", + RPC: "rpc", + ACTION: "action", + INPUT: "input", + OUTPUT: "output", + NOTIF: "notification", + ANYXML: "anyxml", + ANYDATA: "anydata", + AUGMENT: "augment", + USES: "uses", + GROUPING: "grouping", + } + + __slots__ = ("context", "cdata", "module", "__dict__") + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + self.context = context + self.cdata = ffi.cast("struct lysp_node *", cdata) + self.module = module + + def parent(self) -> Optional["PNode"]: + if self.cdata.parent == ffi.NULL: + return None + return PNode.new(self.context, self.cdata.parent, self.module) + + def nodetype(self) -> int: + return self.cdata.nodetype + + def siblings(self) -> Iterator["PNode"]: + for s in ly_list_iter(self.cdata.next): + yield PNode.new(self.context, s, self.module) + + def name(self) -> str: + return c2str(self.cdata.name) + + def description(self) -> Optional[str]: + return c2str(self.cdata.dsc) + + def reference(self) -> Optional[str]: + return c2str(self.cdata.ref) + + def if_features(self) -> Iterator[IfFeatureExpr]: + for f in ly_array_iter(self.cdata.iffeatures): + yield IfFeatureExpr(self.context, f, list(self.module.features())) + + def extensions(self) -> Iterator["ExtensionParsed"]: + for ext in ly_array_iter(self.cdata.exts): + yield ExtensionParsed(self.context, ext, self.module) + + def get_extension( + self, name: str, prefix: Optional[str] = None, arg_value: Optional[str] = None + ) -> Optional["ExtensionParsed"]: + 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 config_set(self) -> bool: + return bool(self.cdata.flags & lib.LYS_SET_CONFIG) + + def config_false(self) -> bool: + return bool(self.cdata.flags & lib.LYS_CONFIG_R) + + def mandatory(self) -> bool: + return bool(self.cdata.flags & lib.LYS_MAND_TRUE) + + def deprecated(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_DEPRC) + + def obsolete(self) -> bool: + return bool(self.cdata.flags & lib.LYS_STATUS_OBSLT) + + def status(self) -> str: + if self.cdata.flags & lib.LYS_STATUS_OBSLT: + return "obsolete" + if self.cdata.flags & lib.LYS_STATUS_DEPRC: + return "deprecated" + return "current" + + def __repr__(self): + cls = self.__class__ + return "<%s.%s: %s>" % (cls.__module__, cls.__name__, str(self)) + + def __str__(self): + return self.name() + + NODETYPE_CLASS = {} + + @staticmethod + def register(nodetype): + def _decorator(nodeclass): + PNode.NODETYPE_CLASS[nodetype] = nodeclass + return nodeclass + + return _decorator + + @staticmethod + def new(context: "libyang.Context", cdata, module: Module) -> "PNode": + cdata = ffi.cast("struct lysp_node *", cdata) + nodecls = PNode.NODETYPE_CLASS.get(cdata.nodetype, None) + if nodecls is None: + raise TypeError("node type %s not implemented" % cdata.nodetype) + return nodecls(context, cdata, module) + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.CONTAINER) +class PContainer(PNode): + __slots__ = ("cdata_container",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_container = ffi.cast("struct lysp_node_container *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_container.musts): + yield Must(self.context, None, m) + + def when_condition(self) -> Optional[str]: + if self.cdata_container.when == ffi.NULL: + return None + return c2str(self.cdata_container.when.cond) + + def presence(self) -> Optional[str]: + return c2str(self.cdata_container.presence) + + def typedefs(self) -> Iterator[Typedef]: + for t in ly_array_iter(self.cdata_container.typedefs): + yield Typedef(self.context, t) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata_container.groupings): + yield PGrouping(self.context, g, self.module) + + def children(self) -> Iterator[PNode]: + for c in ly_list_iter(self.cdata_container.child): + yield PNode.new(self.context, c, self.module) + + def actions(self) -> Iterator["PAction"]: + for a in ly_list_iter(self.cdata_container.actions): + yield PAction(self.context, a, self.module) + + def notifications(self) -> Iterator["PNotif"]: + for n in ly_list_iter(self.cdata_container.notifs): + yield PNotif(self.context, n, self.module) + + def __iter__(self) -> Iterator[PNode]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.LEAF) +class PLeaf(PNode): + __slots__ = ("cdata_leaf",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_leaf = ffi.cast("struct lysp_node_leaf *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_leaf.musts): + yield Must(self.context, None, m) + + def when_condition(self) -> Optional[str]: + if self.cdata_leaf.when == ffi.NULL: + return None + return c2str(self.cdata_leaf.when.cond) + + def type(self) -> PType: + return PType(self.context, self.cdata_leaf.type, self.module) + + def units(self) -> Optional[str]: + return c2str(self.cdata_leaf.units) + + def default(self) -> Optional[str]: + return c2str(self.cdata_leaf.dflt.str) + + def is_key(self) -> bool: + if self.cdata.flags & lib.LYS_KEY: + return True + return False + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.LEAFLIST) +class PLeafList(PNode): + __slots__ = ("cdata_leaflist",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_leaflist = ffi.cast("struct lysp_node_leaflist *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_leaflist.musts): + yield Must(self.context, None, m) + + def when_condition(self) -> Optional[str]: + if self.cdata_leaflist.when == ffi.NULL: + return None + return c2str(self.cdata_leaflist.when.cond) + + def type(self) -> PType: + return PType(self.context, self.cdata_leaflist.type, self.module) + + def units(self) -> Optional[str]: + return c2str(self.cdata_leaflist.units) + + def defaults(self) -> Iterator[str]: + for d in ly_array_iter(self.cdata_leaflist.dflts): + yield c2str(d.str) + + def min_elements(self) -> int: + return self.cdata_leaflist.min + + def max_elements(self) -> Optional[int]: + return self.cdata_leaflist.max if self.cdata_leaflist.max != 0 else None + + def ordered(self) -> bool: + return bool(self.cdata.flags & lib.LYS_ORDBY_USER) + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.LIST) +class PList(PNode): + __slots__ = ("cdata_list",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_list = ffi.cast("struct lysp_node_list *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_list.musts): + yield Must(self.context, None, m) + + def when_condition(self) -> Optional[str]: + if self.cdata_list.when == ffi.NULL: + return None + return c2str(self.cdata_list.when.cond) + + def key(self) -> Optional[str]: + return c2str(self.cdata_list.key) + + def typedefs(self) -> Iterator[Typedef]: + for t in ly_array_iter(self.cdata_list.typedefs): + yield Typedef(self.context, t) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata_list.groupings): + yield PGrouping(self.context, g, self.module) + + def children(self) -> Iterator[PNode]: + for c in ly_list_iter(self.cdata_list.child): + yield PNode.new(self.context, c, self.module) + + def actions(self) -> Iterator["PAction"]: + for a in ly_list_iter(self.cdata_list.actions): + yield PAction(self.context, a, self.module) + + def notifications(self) -> Iterator["PNotif"]: + for n in ly_list_iter(self.cdata_list.notifs): + yield PNotif(self.context, n, self.module) + + def uniques(self) -> Iterator[str]: + for u in ly_array_iter(self.cdata_list.uniques): + yield c2str(u.str) + + def min_elements(self) -> int: + return self.cdata_list.min + + def max_elements(self) -> Optional[int]: + return self.cdata_list.max if self.cdata_list.max != 0 else None + + def ordered(self) -> bool: + return bool(self.cdata.flags & lib.LYS_ORDBY_USER) + + def __iter__(self) -> Iterator[PNode]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.CASE) +class PCase(PNode): + __slots__ = ("cdata_case",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_case = ffi.cast("struct lysp_node_case *", cdata) + + def children(self) -> Iterator[PNode]: + for c in ly_list_iter(self.cdata_case.child): + yield PNode.new(self.context, c, self.module) + + def when_condition(self) -> Optional[str]: + if self.cdata_case.when == ffi.NULL: + return None + return c2str(self.cdata_case.when.cond) + + def __iter__(self) -> Iterator[PNode]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.CHOICE) +class PChoice(PNode): + __slots__ = ("cdata_choice",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_choice = ffi.cast("struct lysp_node_choice *", cdata) + + def children(self) -> Iterator[PCase]: + for c in ly_list_iter(self.cdata_choice.child): + yield PCase(self.context, c, self.module) + + def when_condition(self) -> Optional[str]: + if self.cdata_choice.when == ffi.NULL: + return None + return c2str(self.cdata_choice.when.cond) + + def default(self) -> Optional[str]: + return c2str(self.cdata_choice.dflt.str) + + def __iter__(self) -> Iterator[PCase]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.ANYXML) +@PNode.register(PNode.ANYDATA) +class PAnydata(PNode): + __slots__ = ("cdata_anydata",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_anydata = ffi.cast("struct lysp_node_anydata *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_anydata.musts): + yield Must(self.context, None, m) + + def when_condition(self) -> Optional[str]: + if self.cdata_anydata.when == ffi.NULL: + return None + return c2str(self.cdata_anydata.when.cond) + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.AUGMENT) +class PAugment(PNode): + __slots__ = ("cdata_augment",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_augment = ffi.cast("struct lysp_node_augment *", cdata) + + def children(self) -> Iterator["PNode"]: + for c in ly_list_iter(self.cdata_augment.child): + yield PNode.new(self.context, c, self.module) + + def when_condition(self) -> Optional[str]: + if self.cdata_augment.when == ffi.NULL: + return None + return c2str(self.cdata_augment.when.cond) + + def actions(self) -> Iterator["PAction"]: + for a in ly_list_iter(self.cdata_augment.actions): + yield PAction(self.context, a, self.module) + + def notifications(self) -> Iterator["PNotif"]: + for n in ly_list_iter(self.cdata_augment.notifs): + yield PNotif(self.context, n, self.module) + + def __iter__(self) -> Iterator[PNode]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.USES) +class PUses(PNode): + __slots__ = ("cdata_uses",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_uses = ffi.cast("struct lysp_node_uses *", cdata) + + def refines(self) -> Iterator[PRefine]: + for r in ly_array_iter(self.cdata_uses.refines): + yield PRefine(self.context, r, self.module) + + def augments(self) -> Iterator[PAugment]: + for a in ly_list_iter(self.cdata_uses.augments): + yield PAugment(self.context, a, self.module) + + def when_condition(self) -> Optional[str]: + if self.cdata_uses.when == ffi.NULL: + return None + return c2str(self.cdata_uses.when.cond) + + +# ------------------------------------------------------------------------------------- +class PActionInOut(PNode): + __slots__ = ("cdata_action_inout",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_action_inout = ffi.cast("struct lysp_node_action_inout *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_action_inout.musts): + yield Must(self.context, None, m) + + def typedefs(self) -> Iterator[Typedef]: + for t in ly_array_iter(self.cdata_action_inout.typedefs): + yield Typedef(self.context, t) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata_action_inout.groupings): + yield PGrouping(self.context, g, self.module) + + def children(self) -> Iterator[PNode]: + for c in ly_list_iter(self.cdata_action_inout.child): + yield PNode.new(self.context, c, self.module) + + def __iter__(self) -> Iterator[PNode]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.RPC) +@PNode.register(PNode.ACTION) +class PAction(PNode): + __slots__ = ("cdata_action",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_action = ffi.cast("struct lysp_node_action *", cdata) + + def typedefs(self) -> Iterator[Typedef]: + for t in ly_array_iter(self.cdata_action.typedefs): + yield Typedef(self.context, t) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata_action.groupings): + yield PGrouping(self.context, g, self.module) + + def input(self) -> PActionInOut: + ptr = ffi.addressof(self.cdata_action.input) + return PActionInOut(self.context, ptr, self.module) + + def output(self) -> PActionInOut: + ptr = ffi.addressof(self.cdata_action.output) + return PActionInOut(self.context, ptr, self.module) + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.NOTIF) +class PNotif(PNode): + __slots__ = ("cdata_notif",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_notif = ffi.cast("struct lysp_node_notif *", cdata) + + def musts(self) -> Iterator[Must]: + for m in ly_array_iter(self.cdata_notif.musts): + yield Must(self.context, None, m) + + def typedefs(self) -> Iterator[Typedef]: + for t in ly_array_iter(self.cdata_notif.typedefs): + yield Typedef(self.context, t) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata_notif.groupings): + yield PGrouping(self.context, g, self.module) + + def children(self) -> Iterator[PNode]: + for c in ly_list_iter(self.cdata_notif.child): + yield PNode.new(self.context, c, self.module) + + def __iter__(self) -> Iterator[PNode]: + return self.children() + + +# ------------------------------------------------------------------------------------- +@PNode.register(PNode.GROUPING) +class PGrouping(PNode): + __slots__ = ("cdata_grouping",) + + def __init__(self, context: "libyang.Context", cdata, module: Module) -> None: + super().__init__(context, cdata, module) + self.cdata_grouping = ffi.cast("struct lysp_node_grp *", cdata) + + def typedefs(self) -> Iterator[Typedef]: + for t in ly_array_iter(self.cdata_grouping.typedefs): + yield Typedef(self.context, t) + + def groupings(self) -> Iterator["PGrouping"]: + for g in ly_list_iter(self.cdata_grouping.groupings): + yield PGrouping(self.context, g, self.module) + + def children(self) -> Iterator[PNode]: + for c in ly_list_iter(self.cdata_grouping.child): + yield PNode.new(self.context, c, self.module) + + def actions(self) -> Iterator[PAction]: + for a in ly_list_iter(self.cdata_grouping.actions): + yield PAction(self.context, a, self.module) + + def notifications(self) -> Iterator[PNotif]: + for n in ly_list_iter(self.cdata_grouping.notifs): + yield PNotif(self.context, n, self.module) + + def __iter__(self) -> Iterator[PNode]: + return self.children() diff --git a/libyang/util.py b/libyang/util.py index 9554356e..7b272692 100644 --- a/libyang/util.py +++ b/libyang/util.py @@ -2,16 +2,34 @@ # Copyright (c) 2021 RACOM s.r.o. # SPDX-License-Identifier: MIT +from dataclasses import dataclass import enum -from typing import Optional +from typing import Iterable, Optional import warnings from _libyang import ffi, lib +# ------------------------------------------------------------------------------------- +@dataclass(frozen=True) +class LibyangErrorItem: + msg: Optional[str] + data_path: Optional[str] + schema_path: Optional[str] + line: Optional[int] + + # ------------------------------------------------------------------------------------- class LibyangError(Exception): - pass + def __init__( + self, message: str, *args, errors: Optional[Iterable[LibyangErrorItem]] = None + ): + super().__init__(message, *args) + self.message = message + self.errors = tuple(errors or ()) + + def __str__(self): + return self.message # ------------------------------------------------------------------------------------- @@ -59,6 +77,14 @@ def ly_array_iter(cdata): yield cdata[i] +# ------------------------------------------------------------------------------------- +def ly_list_iter(cdata): + item = cdata + while item != ffi.NULL: + yield item + item = item.next + + # ------------------------------------------------------------------------------------- class IOType(enum.Enum): FD = enum.auto() @@ -69,13 +95,16 @@ class IOType(enum.Enum): # ------------------------------------------------------------------------------------- class DataType(enum.Enum): - DATA_YANG = enum.auto() - RPC_YANG = enum.auto() - NOTIF_YANG = enum.auto() - REPLY_YANG = enum.auto() - RPC_NETCONF = enum.auto() - NOTIF_NETCONF = enum.auto() - REPLY_NETCONF = enum.auto() + DATA_YANG = lib.LYD_TYPE_DATA_YANG + RPC_YANG = lib.LYD_TYPE_RPC_YANG + NOTIF_YANG = lib.LYD_TYPE_NOTIF_YANG + REPLY_YANG = lib.LYD_TYPE_REPLY_YANG + RPC_NETCONF = lib.LYD_TYPE_RPC_NETCONF + NOTIF_NETCONF = lib.LYD_TYPE_NOTIF_NETCONF + REPLY_NETCONF = lib.LYD_TYPE_REPLY_NETCONF + RPC_RESTCONF = lib.LYD_TYPE_RPC_RESTCONF + NOTIF_RESTCONF = lib.LYD_TYPE_NOTIF_RESTCONF + REPLY_RESTCONF = lib.LYD_TYPE_REPLY_RESTCONF # ------------------------------------------------------------------------------------- @@ -113,4 +142,6 @@ def data_load(in_type, in_data, data, data_keepalive, encode=True): c_str = str2c(in_data, encode=encode) data_keepalive.append(c_str) ret = lib.ly_in_new_memory(c_str, data) + else: + raise ValueError("invalid input") return ret diff --git a/libyang/xpath.py b/libyang/xpath.py index facaa8b7..e2a33196 100644 --- a/libyang/xpath.py +++ b/libyang/xpath.py @@ -1,6 +1,7 @@ # Copyright (c) 2020 6WIND S.A. # SPDX-License-Identifier: MIT +import contextlib import fnmatch import re from typing import Any, Dict, Iterator, List, Optional, Tuple, Union @@ -56,24 +57,35 @@ def xpath_split(xpath: str) -> Iterator[Tuple[str, str, List[Tuple[str, str]]]]: while i < len(xpath) and xpath[i] == "[": i += 1 # skip opening '[' j = xpath.find("=", i) # find key name end - key_name = xpath[i:j] - quote = xpath[j + 1] # record opening quote character - j = i = j + 2 # skip '=' and opening quote - while True: - if xpath[j] == quote and xpath[j - 1] != "\\": - break - j += 1 - # replace escaped chars by their non-escape version - key_value = xpath[i:j].replace(f"\\{quote}", f"{quote}") - keys.append((key_name, key_value)) - i = j + 2 # skip closing quote and ']' + + if j != -1: # keyed specifier + key_name = xpath[i:j] + quote = xpath[j + 1] # record opening quote character + j = i = j + 2 # skip '=' and opening quote + while True: + if xpath[j] == quote and xpath[j - 1] != "\\": + break + j += 1 + # replace escaped chars by their non-escape version + key_value = xpath[i:j].replace(f"\\{quote}", f"{quote}") + keys.append((key_name, key_value)) + i = j + 2 # skip closing quote and ']' + else: # index specifier + j = i + while True: + if xpath[j] == "]": + break + j += 1 + key_value = xpath[i:j] + keys.append(("", key_value)) + i = j + 2 yield prefix, name, keys # ------------------------------------------------------------------------------------- def _xpath_keys_to_key_name( - keys: List[Tuple[str, str]] + keys: List[Tuple[str, str]], ) -> Optional[Union[str, Tuple[str, ...]]]: """ Extract key name from parsed xpath keys returned by xpath_split. The return value @@ -134,6 +146,12 @@ def _list_find_key_index(keys: List[Tuple[str, str]], lst: List) -> int: if py_to_yang(elem) == keys[0][1]: return i + elif keys[0][0] == "": + # keys[0][1] is directly the index + index = int(keys[0][1]) - 1 + if len(lst) > index: + return index + else: for i, elem in enumerate(lst): if not isinstance(elem, dict): @@ -410,32 +428,47 @@ def xpath_set( lst.append(value) return lst[key_val] - if isinstance(lst, list): - # regular python list, need to iterate over it - try: - i = _list_find_key_index(keys, lst) - # found - if force: - lst[i] = value - return lst[i] - except ValueError: - # not found - if after is None: - lst.append(value) - elif after == "": - lst.insert(0, value) - else: - if after[0] != "[": - after = "[.=%r]" % str(after) - _, _, after_keys = next(xpath_split("/*" + after)) - insert_index = _list_find_key_index(after_keys, lst) + 1 - if insert_index == len(lst): - lst.append(value) - else: - lst.insert(insert_index, value) - return value + # regular python list from now + if not isinstance(lst, list): + raise TypeError("expected a list") + + with contextlib.suppress(ValueError): + i = _list_find_key_index(keys, lst) + # found + if force: + lst[i] = value + return lst[i] + + # value not found; handle insertion based on 'after' + if after is None: + lst.append(value) + return value + + if after == "": + lst.insert(0, value) + return value + + # first try to find the value in the leaf list + try: + _, _, after_keys = next( + xpath_split(f"/*{after}" if after[0] == "[" else f"/*[.={after!r}]") + ) + insert_index = _list_find_key_index(after_keys, lst) + 1 + except ValueError: + # handle 'after' as numeric index + if not after.isnumeric(): + raise + + insert_index = int(after) + if insert_index > len(lst): + raise + + if insert_index == len(lst): + lst.append(value) + else: + lst.insert(insert_index, value) - raise TypeError("expected a list") + return value # ------------------------------------------------------------------------------------- diff --git a/pylintrc b/pylintrc index 658c45ef..cd4638b7 100644 --- a/pylintrc +++ b/pylintrc @@ -74,9 +74,12 @@ disable= too-many-branches, too-many-lines, too-many-locals, + too-many-positional-arguments, too-many-return-statements, too-many-statements, unused-argument, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, wrong-import-order, [REPORTS] @@ -491,7 +494,7 @@ valid-metaclass-classmethod-first-arg=mcs [DESIGN] # Maximum number of arguments for function / method. -max-args=15 +max-args=20 # Maximum number of attributes for a class (see R0902). max-attributes=20 @@ -525,4 +528,4 @@ min-public-methods=0 # Exceptions that will emit a warning when being caught. Defaults to # "Exception". -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/tests/test_context.py b/tests/test_context.py index 6e88a261..8ffd0454 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -4,7 +4,8 @@ import os import unittest -from libyang import Context, LibyangError, Module, SRpc +from libyang import Context, LibyangError, Module, SContainer, SLeaf, SLeafList +from libyang.util import c2str YANG_DIR = os.path.join(os.path.dirname(__file__), "yang") @@ -19,7 +20,11 @@ def test_ctx_no_dir(self): self.assertIsNot(ctx, None) def test_ctx_yanglib(self): - ctx = Context(YANG_DIR, yanglib_path=YANG_DIR + "/yang-library.json") + ctx = Context( + YANG_DIR, + yanglib_path=YANG_DIR + "/yang-library.json", + compile_obsolete=True, + ) ctx.load_module("yolo-system") dnode = ctx.get_yanglib_data() j = dnode.print_mem("json", with_siblings=True) @@ -62,6 +67,13 @@ def test_ctx_load_module(self): mod = ctx.load_module("yolo-system") self.assertIsInstance(mod, Module) + def test_ctx_load_module_with_features(self): + with Context(YANG_DIR) as ctx: + mod = ctx.load_module("yolo-system", None, ["*"]) + self.assertIsInstance(mod, Module) + for f in list(mod.features()): + self.assertTrue(f.state()) + def test_ctx_get_module(self): with Context(YANG_DIR) as ctx: ctx.load_module("yolo-system") @@ -82,8 +94,26 @@ def test_ctx_load_invalid_module(self): 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) + node = next(ctx.find_path("/yolo-system:conf/offline")) + self.assertIsInstance(node, SLeaf) + node2 = next(ctx.find_path("../number", root_node=node)) + self.assertIsInstance(node2, SLeafList) + + def test_ctx_find_xpath_atoms(self): + with Context(YANG_DIR) as ctx: + ctx.load_module("yolo-system") + node_iter = ctx.find_xpath_atoms("/yolo-system:conf/offline") + node = next(node_iter) + self.assertIsInstance(node, SContainer) + node = next(node_iter) + self.assertIsInstance(node, SLeaf) + node_iter = ctx.find_xpath_atoms("../number", root_node=node) + node = next(node_iter) + self.assertIsInstance(node, SLeaf) + node = next(node_iter) + self.assertIsInstance(node, SContainer) + node = next(node_iter) + self.assertIsInstance(node, SLeafList) def test_ctx_iter_modules(self): with Context(YANG_DIR) as ctx: @@ -104,3 +134,43 @@ def test_ctx_parse_module(self): with Context(YANG_DIR) as ctx: mod = ctx.parse_module_file(f, features=["turbo-boost", "networking"]) self.assertIsInstance(mod, Module) + + def test_ctx_leafref_extended(self): + with Context(YANG_DIR, leafref_extended=True) as ctx: + mod = ctx.load_module("yolo-leafref-extended") + self.assertIsInstance(mod, Module) + + def test_context_dict(self): + with Context(YANG_DIR) as ctx: + orig_str = "teststring" + handle = ctx.add_to_dict(orig_str) + self.assertEqual(orig_str, c2str(handle)) + ctx.remove_from_dict(orig_str) + + def test_ctx_disable_searchdirs(self): + with Context(YANG_DIR, disable_searchdirs=True) as ctx: + with self.assertRaises(LibyangError): + ctx.load_module("yolo-nodetypes") + + def test_ctx_using_clb(self): + def get_module_valid_clb(mod_name, *_): + YOLO_NODETYPES_MOD_PATH = os.path.join(YANG_DIR, "yolo/yolo-nodetypes.yang") + self.assertEqual(mod_name, "yolo-nodetypes") + with open(YOLO_NODETYPES_MOD_PATH, encoding="utf-8") as f: + mod_str = f.read() + return "yang", mod_str + + def get_module_invalid_clb(mod_name, *_): + return None + + with Context(YANG_DIR, disable_searchdirs=True) as ctx: + with self.assertRaises(LibyangError): + ctx.load_module("yolo-nodetypes") + + ctx.external_module_loader.set_module_data_clb(get_module_invalid_clb) + with self.assertRaises(LibyangError): + mod = ctx.load_module("yolo-nodetypes") + + ctx.external_module_loader.set_module_data_clb(get_module_valid_clb) + mod = ctx.load_module("yolo-nodetypes") + self.assertIsInstance(mod, Module) diff --git a/tests/test_data.py b/tests/test_data.py index b217b97a..8f56dc19 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,6 +1,7 @@ # Copyright (c) 2020 6WIND S.A. # SPDX-License-Identifier: MIT +import gc import json import os import unittest @@ -14,12 +15,16 @@ DataType, DContainer, DLeaf, + DList, DNode, + DNodeAttrs, DNotif, DRpc, IOType, LibyangError, + Module, ) +from libyang.data import dict_to_dnode YANG_DIR = os.path.join(os.path.dirname(__file__), "yang") @@ -28,10 +33,11 @@ # ------------------------------------------------------------------------------------- class DataTest(unittest.TestCase): def setUp(self): - self.ctx = Context(YANG_DIR) + self.ctx = Context(YANG_DIR, compile_obsolete=True) modules = [ self.ctx.load_module("ietf-netconf"), self.ctx.load_module("yolo-system"), + self.ctx.load_module("yolo-nodetypes"), ] for mod in modules: @@ -45,18 +51,18 @@ def tearDown(self): "yolo-system:conf": { "hostname": "foo", "url": [ - { - "proto": "https", - "host": "github.com", - "path": "/CESNET/libyang-python", - "enabled": false - }, { "proto": "http", "host": "foobar.com", "port": 8080, "path": "/index.html", "enabled": true + }, + { + "proto": "https", + "host": "github.com", + "path": "/CESNET/libyang-python", + "enabled": false } ], "number": [ @@ -70,7 +76,9 @@ def tearDown(self): """ def test_data_parse_config_json(self): - dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, "json", no_state=True) + dnode = self.ctx.parse_data_mem( + self.JSON_CONFIG, "json", no_state=True, ordered=True + ) self.assertIsInstance(dnode, DContainer) try: j = dnode.print_mem("json", with_siblings=True) @@ -78,15 +86,54 @@ def test_data_parse_config_json(self): finally: dnode.free() - JSON_CONFIG_ADD_LIST_ITEM = """{ + JSON_CONFIG_WITH_STATE = """{ + "yolo-system:state": { + "speed": 4321 + }, "yolo-system:conf": { "hostname": "foo", "url": [ + { + "proto": "http", + "host": "foobar.com", + "port": 8080, + "path": "/index.html", + "enabled": true + }, { "proto": "https", "host": "github.com", "path": "/CESNET/libyang-python", "enabled": false + } + ], + "number": [ + 1000, + 2000, + 3000 + ], + "speed": 1234 + } +} +""" + + def test_data_parse_config_json_without_yang_lib(self): + dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, "json") + self.assertIsInstance(dnode, DContainer) + try: + j = dnode.print_mem("json", with_siblings=True) + self.assertEqual(j, self.JSON_CONFIG_WITH_STATE) + finally: + dnode.free() + + JSON_CONFIG_ADD_LIST_ITEM = """{ + "yolo-system:conf": { + "hostname": "foo", + "url": [ + { + "proto": "http", + "host": "barfoo.com", + "path": "/barfoo/index.html" }, { "proto": "http", @@ -96,9 +143,10 @@ def test_data_parse_config_json(self): "enabled": true }, { - "proto": "http", - "host": "barfoo.com", - "path": "/barfoo/index.html" + "proto": "https", + "host": "github.com", + "path": "/CESNET/libyang-python", + "enabled": false } ], "number": [ @@ -208,7 +256,9 @@ def test_data_parse_state_json(self): """ def test_data_parse_config_xml(self): - dnode = self.ctx.parse_data_mem(self.XML_CONFIG, "xml", validate_present=True) + dnode = self.ctx.parse_data_mem( + self.XML_CONFIG, "xml", validate_present=True, ordered=True + ) self.assertIsInstance(dnode, DContainer) try: xml = dnode.print_mem("xml", with_siblings=True, trim_default_values=True) @@ -216,6 +266,45 @@ def test_data_parse_config_xml(self): finally: dnode.free() + XML_CONFIG_MULTI_ERROR = """<conf xmlns="urn:yang:yolo:system"> + <hostname>foo</hostname> + <url> + <proto>https</proto> + <path>/CESNET/libyang-python</path> + <enabled>abcd</enabled> + </url> + <number>2000</number> +</conf> +""" + + def test_data_parse_config_xml_multi_error(self): + with self.assertRaises(Exception) as cm: + self.ctx.parse_data_mem( + self.XML_CONFIG_MULTI_ERROR, + "xml", + validate_present=True, + validate_multi_error=True, + ) + self.assertEqual( + str(cm.exception), + 'failed to parse data tree: Invalid boolean value "abcd".: ' + "Data path: /yolo-system:conf/url[proto='https']/enabled (line 6): " + 'List instance is missing its key "host".: ' + "Data path: /yolo-system:conf/url[proto='https'] (line 7)", + ) + + first = cm.exception.errors[0] + self.assertEqual(first.msg, 'Invalid boolean value "abcd".') + self.assertEqual( + first.data_path, "/yolo-system:conf/url[proto='https']/enabled" + ) + self.assertEqual(first.line, 6) + + second = cm.exception.errors[1] + self.assertEqual(second.msg, 'List instance is missing its key "host".') + self.assertEqual(second.data_path, "/yolo-system:conf/url[proto='https']") + self.assertEqual(second.line, 7) + XML_STATE = """<state xmlns="urn:yang:yolo:system"> <hostname>foo</hostname> <url> @@ -424,6 +513,7 @@ def test_data_from_dict_module_free_func(self): } def test_data_from_dict_module_with_prefix(self): + self.maxDiff = None module = self.ctx.get_module("yolo-system") dnode = module.parse_data_dict( self.DICT_CONFIG_WITH_PREFIX, strict=True, validate_present=True @@ -664,6 +754,22 @@ def test_notification_from_dict_module(self): dnotif.free() self.assertEqual(json.loads(j), json.loads(self.JSON_NOTIF)) + DICT_NOTIF_KEYLESS_LIST = { + "config-change": {"edit": [{"target": "a"}, {"target": "b"}]}, + } + + def test_data_to_dict_keyless_list(self): + module = self.ctx.get_module("yolo-system") + dnotif = module.parse_data_dict( + self.DICT_NOTIF_KEYLESS_LIST, strict=True, notification=True + ) + self.assertIsInstance(dnotif, DNotif) + try: + dic = dnotif.print_dict() + finally: + dnotif.free() + self.assertEqual(dic, self.DICT_NOTIF_KEYLESS_LIST) + XML_DIFF_STATE1 = """<state xmlns="urn:yang:yolo:system"> <hostname>foo</hostname> <speed>1234</speed> @@ -723,7 +829,7 @@ def test_notification_from_dict_module(self): <host>foobar.com</host> <enabled yang:operation="replace" yang:orig-default="false" yang:orig-value="true">false</enabled> </url> - <url yang:operation="create"> + <url yang:operation="create" yang:key="[proto='http'][host='foobar.com']"> <proto>ftp</proto> <host>github.com</host> <path>/CESNET/libyang-python</path> @@ -752,17 +858,17 @@ def test_data_diff(self): TREE = [ "/yolo-system:conf", "/yolo-system:conf/hostname", - "/yolo-system:conf/url[proto='https'][host='github.com']", - "/yolo-system:conf/url[proto='https'][host='github.com']/proto", - "/yolo-system:conf/url[proto='https'][host='github.com']/host", - "/yolo-system:conf/url[proto='https'][host='github.com']/path", - "/yolo-system:conf/url[proto='https'][host='github.com']/enabled", "/yolo-system:conf/url[proto='http'][host='foobar.com']", "/yolo-system:conf/url[proto='http'][host='foobar.com']/proto", "/yolo-system:conf/url[proto='http'][host='foobar.com']/host", "/yolo-system:conf/url[proto='http'][host='foobar.com']/port", "/yolo-system:conf/url[proto='http'][host='foobar.com']/path", "/yolo-system:conf/url[proto='http'][host='foobar.com']/enabled", + "/yolo-system:conf/url[proto='https'][host='github.com']", + "/yolo-system:conf/url[proto='https'][host='github.com']/proto", + "/yolo-system:conf/url[proto='https'][host='github.com']/host", + "/yolo-system:conf/url[proto='https'][host='github.com']/path", + "/yolo-system:conf/url[proto='https'][host='github.com']/enabled", "/yolo-system:conf/number[.='1000']", "/yolo-system:conf/number[.='2000']", "/yolo-system:conf/number[.='3000']", @@ -805,18 +911,251 @@ def test_find_all(self): } ] } - self.assertEqual(urls[0].print_dict(absolute=False), expected_url) + self.assertEqual(urls[1].print_dict(absolute=False), expected_url) finally: dnode.free() def test_add_defaults(self): - dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, "json", validate_present=True) - node = dnode.find_path("/yolo-system:conf/speed") + JSON = '{"yolo-nodetypes:records": [{"id": "rec1"}], "yolo-nodetypes:conf": {}}' + dnode = self.ctx.parse_data_mem( + JSON, "json", validate_present=True, parse_only=True + ) + self.assertIsInstance(dnode, DList) + node = dnode.find_one("id") self.assertIsInstance(node, DLeaf) - node.free(with_siblings=False) - node = dnode.find_path("/yolo-system:conf/speed") + node = dnode.find_one("name") self.assertIsNone(node) - dnode.add_defaults() + node = dnode.find_one("/yolo-system:conf/speed") + self.assertIsNone(node) + + dnode.add_defaults(only_node=True) + node = dnode.find_one("name") + self.assertIsInstance(node, DLeaf) + self.assertEqual(node.value(), "ASD") + node = dnode.find_one("/yolo-nodetypes:conf/percentage") + self.assertIsNone(node) + node = dnode.find_one("/yolo-system:conf/speed") + self.assertIsNone(node) + + dnode.add_defaults(only_module=dnode.module()) + node = dnode.find_one("/yolo-nodetypes:conf/percentage") + self.assertIsInstance(node, DLeaf) + self.assertEqual(node.value(), 10.2) + node = dnode.find_one("/yolo-system:conf/speed") + self.assertIsNone(node) + + dnode.add_defaults(only_node=False) node = dnode.find_path("/yolo-system:conf/speed") self.assertIsInstance(node, DLeaf) self.assertEqual(node.value(), 4321) + + def test_dnode_double_free(self): + dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, "json", validate_present=True) + dnode.free() + dnode.free() + + def test_dnode_unlink(self): + dnode = self.ctx.parse_data_mem(self.JSON_CONFIG, "json", validate_present=True) + self.assertIsInstance(dnode, DContainer) + try: + child = dnode.find_one("hostname") + self.assertIsInstance(child, DNode) + child.unlink(with_siblings=False) + self.assertIsNone(dnode.find_one("hostname")) + child = next(dnode.children(), None) + self.assertIsNot(child, None) + child.unlink(with_siblings=True) + child = next(dnode.children(), None) + self.assertIsNone(child, None) + finally: + dnode.free() + + def test_dnode_insert_sibling(self): + MAIN = {"yolo-nodetypes:conf": {"percentage": "20.2"}} + SIBLING = {"yolo-nodetypes:test1": 10} + module = self.ctx.get_module("yolo-nodetypes") + dnode1 = dict_to_dnode(MAIN, module, None, validate=False) + dnode2 = dict_to_dnode(SIBLING, module, None, validate=False) + self.assertEqual(len(list(dnode1.siblings(include_self=False))), 0) + self.assertEqual(len(list(dnode2.siblings(include_self=False))), 0) + dnode2.insert_sibling(dnode1) + self.assertEqual(len(list(dnode1.siblings(include_self=False))), 1) + self.assertEqual(len(list(dnode2.siblings(include_self=False))), 1) + sibling = next(dnode1.siblings(include_self=False), None) + self.assertIsInstance(sibling, DLeaf) + self.assertEqual(sibling.cdata, dnode2.cdata) + + def test_dnode_insert_sibling_before_after(self): + R1 = {"yolo-nodetypes:records": [{"id": "id1", "name": "name1"}]} + R2 = {"yolo-nodetypes:records": [{"id": "id2", "name": "name2"}]} + R3 = {"yolo-nodetypes:records": [{"id": "id3", "name": "name3"}]} + module = self.ctx.get_module("yolo-nodetypes") + dnode1 = dict_to_dnode(R1, module, None, validate=False) + dnode2 = dict_to_dnode(R2, module, None, validate=False) + dnode3 = dict_to_dnode(R3, module, None, validate=False) + self.assertEqual(dnode1.first_sibling().cdata, dnode1.cdata) + dnode1.insert_before(dnode2) + dnode1.insert_after(dnode3) + self.assertEqual( + [dnode2.cdata, dnode1.cdata, dnode3.cdata], + [s.cdata for s in dnode1.first_sibling().siblings()], + ) + self.assertEqual(dnode1.first_sibling().cdata, dnode2.cdata) + + def _create_opaq_hostname(self): + root = self.ctx.create_data_path(path="/yolo-system:conf") + root.new_path( + "hostname", + None, + opt_opaq=True, + ) + return root.find_one("/yolo-system:conf/hostname") + + def test_dnode_new_opaq_find_one(self): + dnode = self._create_opaq_hostname() + + self.assertIsInstance(dnode, DLeaf) + + def test_dnode_attrs(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + self.assertIsInstance(attrs, DNodeAttrs) + + def test_dnode_attrs_set(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + self.assertEqual(len(attrs.cdata), 0) + attrs.set("ietf-netconf:operation", "remove") + + self.assertEqual(len(attrs.cdata), 1) + + def test_dnode_attrs_get(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + attrs.set("ietf-netconf:operation", "remove") + + value = attrs.get("ietf-netconf:operation") + self.assertEqual(value, "remove") + + def test_dnode_attrs__len(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + self.assertEqual(len(attrs), 0) + attrs.set("ietf-netconf:operation", "remove") + + self.assertEqual(len(attrs), 1) + + def test_dnode_attrs__contains(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + attrs.set("ietf-netconf:operation", "remove") + + self.assertTrue("ietf-netconf:operation" in attrs) + + def test_dnode_attrs_remove(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + attrs.set("ietf-netconf:operation", "remove") + attrs.remove("ietf-netconf:operation") + + self.assertEqual(len(attrs), 0) + + def test_dnode_attrs_set_and_remove_multiple(self): + dnode = self._create_opaq_hostname() + attrs = dnode.attrs() + + attrs.set("ietf-netconf:operation", "remove") + attrs.set("something:else", "test") + attrs.set("no_prefix", "test") + self.assertEqual(len(attrs), 3) + + attrs.remove("something:else") + self.assertEqual(len(attrs), 2) + self.assertIn("no_prefix", attrs) + self.assertIn("ietf-netconf:operation", attrs) + + attrs.remove("no_prefix") + self.assertEqual(len(attrs), 1) + + attrs.remove("ietf-netconf:operation") + self.assertEqual(len(attrs), 0) + + def test_dnode_leafref_linking(self): + MAIN = """{ + "yolo-leafref-extended:list1": [{ + "leaf1": "val1", + "leaflist2": ["val2", "val3"] + }], + "yolo-leafref-extended:ref1": "val1" + }""" + self.ctx.destroy() + self.ctx = Context( + YANG_DIR, leafref_extended=True, leafref_linking=True, compile_obsolete=True + ) + mod = self.ctx.load_module("yolo-leafref-extended") + self.assertIsInstance(mod, Module) + dnode1 = self.ctx.parse_data_mem(MAIN, "json", parse_only=True) + self.assertIsInstance(dnode1, DList) + dnode2 = next(dnode1.siblings(include_self=False)) + self.assertIsInstance(dnode2, DLeaf) + dnode3 = next(dnode1.children()) + self.assertIsInstance(dnode3, DLeaf) + self.assertIsNone(next(dnode3.leafref_nodes(), None)) + dnode2.leafref_link_node_tree() + dnode4 = next(dnode3.leafref_nodes()) + self.assertIsInstance(dnode4, DLeaf) + self.assertEqual(dnode4.cdata, dnode2.cdata) + dnode5 = next(dnode4.target_nodes()) + self.assertIsInstance(dnode5, DLeaf) + self.assertEqual(dnode5.cdata, dnode3.cdata) + dnode1.free() + + def test_dnode_store_only(self): + MAIN = {"yolo-nodetypes:test1": 50} + module = self.ctx.load_module("yolo-nodetypes") + dnode = dict_to_dnode(MAIN, module, None, validate=False, store_only=True) + self.assertIsInstance(dnode, DLeaf) + self.assertEqual(dnode.value(), 50) + dnode.free() + + def test_dnode_builtin_plugins_only(self): + MAIN = {"yolo-nodetypes:ip-address": "test"} + self.tearDown() + gc.collect() + self.ctx = Context(YANG_DIR, builtin_plugins_only=True, compile_obsolete=True) + module = self.ctx.load_module("yolo-nodetypes") + dnode = dict_to_dnode(MAIN, module, None, validate=False, store_only=True) + self.assertIsInstance(dnode, DLeaf) + self.assertEqual(dnode.value(), "test") + dnode.free() + + def test_merge_store_only(self): + MAIN = {"yolo-nodetypes:test1": 50} + module = self.ctx.load_module("yolo-nodetypes") + dnode = module.parse_data_dict(MAIN, validate=False, store_only=True) + self.assertIsInstance(dnode, DLeaf) + self.assertEqual(dnode.value(), 50) + dnode.free() + + def test_merge_builtin_plugins_only(self): + MAIN = {"yolo-nodetypes:ip-address": "test"} + self.tearDown() + gc.collect() + self.ctx = Context(YANG_DIR, builtin_plugins_only=True, compile_obsolete=True) + module = self.ctx.load_module("yolo-nodetypes") + dnode = module.parse_data_dict(MAIN, validate=False, store_only=True) + self.assertIsInstance(dnode, DLeaf) + self.assertEqual(dnode.value(), "test") + dnode.free() + + def test_dnode_parse_json_null(self): + JSON = """{"yolo-nodetypes:ip-address": null}""" + dnode = self.ctx.parse_data_mem(JSON, "json", json_null=True) + dnode_names = [d.name() for d in dnode.siblings()] + self.assertFalse("ip-address" in dnode_names) diff --git a/tests/test_diff.py b/tests/test_diff.py index 7155b3e0..2263e344 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -12,6 +12,7 @@ EnumRemoved, EnumStatusAdded, EnumStatusRemoved, + ExtensionAdded, NodeTypeAdded, NodeTypeRemoved, SNodeAdded, @@ -28,7 +29,6 @@ # ------------------------------------------------------------------------------------- class DiffTest(unittest.TestCase): - expected_diffs = frozenset( ( (BaseTypeAdded, "/yolo-system:conf/speed"), @@ -76,17 +76,30 @@ class DiffTest(unittest.TestCase): (SNodeAdded, "/yolo-system:alarm-triggered"), (SNodeAdded, "/yolo-system:alarm-triggered/severity"), (SNodeAdded, "/yolo-system:alarm-triggered/description"), + (SNodeAdded, "/yolo-system:config-change"), + (SNodeAdded, "/yolo-system:config-change/edit"), + (SNodeAdded, "/yolo-system:config-change/edit/target"), (EnumRemoved, "/yolo-system:conf/url/proto"), (EnumRemoved, "/yolo-system:state/url/proto"), (EnumStatusAdded, "/yolo-system:conf/url/proto"), (EnumStatusAdded, "/yolo-system:state/url/proto"), + (ExtensionAdded, "/yolo-system:conf/url/proto"), + (ExtensionAdded, "/yolo-system:state/url/proto"), (EnumStatusRemoved, "/yolo-system:conf/url/proto"), (EnumStatusRemoved, "/yolo-system:state/url/proto"), + (SNodeAdded, "/yolo-system:conf/pill/red/out"), + (SNodeAdded, "/yolo-system:state/pill/red/out"), + (SNodeAdded, "/yolo-system:conf/pill/blue/in"), + (SNodeAdded, "/yolo-system:state/pill/blue/in"), + (SNodeAdded, "/yolo-system:alarm-triggered/severity"), + (SNodeAdded, "/yolo-system:alarm-triggered/description"), ) ) def test_diff(self): - with Context(OLD_YANG_DIR) as ctx_old, Context(NEW_YANG_DIR) as ctx_new: + with Context(OLD_YANG_DIR, compile_obsolete=True) as ctx_old, Context( + NEW_YANG_DIR, compile_obsolete=True + ) as ctx_new: mod = ctx_old.load_module("yolo-system") mod.feature_enable_all() mod = ctx_new.load_module("yolo-system") diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 00000000..b932788c --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,193 @@ +# Copyright (c) 2018-2019 Robin Jarry +# SPDX-License-Identifier: MIT + +import logging +import os +from typing import Any, Optional +import unittest + +from libyang import ( + Context, + ExtensionCompiled, + ExtensionParsed, + ExtensionPlugin, + LibyangError, + LibyangExtensionError, + Module, + PLeaf, + SLeaf, +) + + +YANG_DIR = os.path.join(os.path.dirname(__file__), "yang") + + +# ------------------------------------------------------------------------------------- +class TestExtensionPlugin(ExtensionPlugin): + def __init__(self, context: Context) -> None: + super().__init__( + "omg-extensions", + "type-desc", + "omg-extensions-type-desc-plugin-v1", + context, + parse_clb=self._parse_clb, + compile_clb=self._compile_clb, + parse_free_clb=self._parse_free_clb, + compile_free_clb=self._compile_free_clb, + ) + self.parse_clb_called = 0 + self.compile_clb_called = 0 + self.parse_free_clb_called = 0 + self.compile_free_clb_called = 0 + self.parse_clb_exception: Optional[LibyangExtensionError] = None + self.compile_clb_exception: Optional[LibyangExtensionError] = None + self.parse_parent_stmt = None + + def reset(self) -> None: + self.parse_clb_called = 0 + self.compile_clb_called = 0 + self.parse_free_clb_called = 0 + self.compile_free_clb_called = 0 + self.parse_clb_exception = None + self.compile_clb_exception = None + + def _parse_clb(self, module: Module, ext: ExtensionParsed) -> None: + self.parse_clb_called += 1 + if self.parse_clb_exception is not None: + raise self.parse_clb_exception + self.parse_substmts(ext) + self.parse_parent_stmt = self.stmt2str(ext.cdata.parent_stmt) + + def _compile_clb(self, pext: ExtensionParsed, cext: ExtensionCompiled) -> None: + self.compile_clb_called += 1 + if self.compile_clb_exception is not None: + raise self.compile_clb_exception + self.compile_substmts(pext, cext) + + def _parse_free_clb(self, ext: ExtensionParsed) -> None: + self.parse_free_clb_called += 1 + self.free_parse_substmts(ext) + + def _compile_free_clb(self, ext: ExtensionCompiled) -> None: + self.compile_free_clb_called += 1 + self.free_compile_substmts(ext) + + +# ------------------------------------------------------------------------------------- +class ExtensionTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.plugin = TestExtensionPlugin(self.ctx) + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_extension_basic(self): + self.ctx.load_module("yolo-system") + self.assertEqual(5, self.plugin.parse_clb_called) + self.assertEqual(6, self.plugin.compile_clb_called) + self.assertEqual(0, self.plugin.parse_free_clb_called) + self.assertEqual(0, self.plugin.compile_free_clb_called) + self.assertEqual("type", self.plugin.parse_parent_stmt) + self.ctx.destroy() + self.assertEqual(5, self.plugin.parse_clb_called) + self.assertEqual(6, self.plugin.compile_clb_called) + self.assertEqual(5, self.plugin.parse_free_clb_called) + self.assertEqual(6, self.plugin.compile_free_clb_called) + + def test_extension_invalid_parse(self): + self.plugin.parse_clb_exception = LibyangExtensionError( + "this extension cannot be parsed", + self.plugin.ERROR_NOT_VALID, + logging.ERROR, + ) + with self.assertRaises(LibyangError): + self.ctx.load_module("yolo-system") + + def test_extension_invalid_compile(self): + self.plugin.compile_clb_exception = LibyangExtensionError( + "this extension cannot be compiled", + self.plugin.ERROR_NOT_VALID, + logging.ERROR, + ) + with self.assertRaises(LibyangError): + self.ctx.load_module("yolo-system") + + +# ------------------------------------------------------------------------------------- +class ExampleParseExtensionPlugin(ExtensionPlugin): + def __init__(self, context: Context) -> None: + super().__init__( + "omg-extensions", + "parse-validation", + "omg-extensions-parse-validation-plugin-v1", + context, + parse_clb=self._parse_clb, + ) + + def _verify_single(self, parent: Any) -> None: + count = 0 + for e in parent.extensions(): + if e.name() == self.name and e.module().name() == self.module_name: + count += 1 + if count > 1: + raise LibyangExtensionError( + f"Extension {self.name} is allowed to be defined just once per given " + "parent node context.", + self.ERROR_NOT_VALID, + logging.ERROR, + ) + + def _parse_clb(self, _, ext: ExtensionParsed) -> None: + parent = ext.parent_node() + if not isinstance(parent, PLeaf): + raise LibyangExtensionError( + f"Extension {ext.name()} is allowed only in leaf nodes", + self.ERROR_NOT_VALID, + logging.ERROR, + ) + self._verify_single(parent) + # here you put code to perform something reasonable actions you need for your extension + + +class ExampleCompileExtensionPlugin(ExtensionPlugin): + def __init__(self, context: Context) -> None: + super().__init__( + "omg-extensions", + "compile-validation", + "omg-extensions-compile-validation-plugin-v1", + context, + compile_clb=self._compile_clb, + ) + + def _compile_clb(self, pext: ExtensionParsed, cext: ExtensionCompiled) -> None: + parent = cext.parent_node() + if not isinstance(parent, SLeaf): + raise LibyangExtensionError( + f"Extension {cext.name()} is allowed only in leaf nodes", + self.ERROR_NOT_VALID, + logging.ERROR, + ) + # here you put code to perform something reasonable actions you need for your extension + + +class ExtensionExampleTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.plugins = [] + + def tearDown(self): + self.plugins.clear() + self.ctx.destroy() + self.ctx = None + + def test_parse_validation_example(self): + self.plugins.append(ExampleParseExtensionPlugin(self.ctx)) + self.ctx.load_module("yolo-system") + + def test_compile_validation_example(self): + self.plugins.append(ExampleParseExtensionPlugin(self.ctx)) + self.plugins.append(ExampleCompileExtensionPlugin(self.ctx)) + with self.assertRaises(LibyangError): + self.ctx.load_module("yolo-system") diff --git a/tests/test_log.py b/tests/test_log.py new file mode 100644 index 00000000..2834414a --- /dev/null +++ b/tests/test_log.py @@ -0,0 +1,55 @@ +# Copyright (c) 2025, LabN Consulting, L.L.C. +# SPDX-License-Identifier: MIT + +import logging +import os +import sys +import unittest + +from libyang import Context, LibyangError, configure_logging, temp_log_options + + +YANG_DIR = os.path.join(os.path.dirname(__file__), "yang") + + +class LogTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + configure_logging(False, logging.INFO) + + def tearDown(self): + if self.ctx is not None: + self.ctx.destroy() + self.ctx = None + + def _cause_log(self): + try: + assert self.ctx is not None + _ = self.ctx.parse_data_mem("bad", fmt="xml") + except LibyangError: + pass + + @unittest.skipIf(sys.version_info < (3, 10), "Test requires Python 3.10+") + def test_configure_logging(self): + """Test configure_logging API.""" + with self.assertNoLogs("libyang", level="ERROR"): + self._cause_log() + + configure_logging(True, logging.INFO) + with self.assertLogs("libyang", level="ERROR"): + self._cause_log() + + @unittest.skipIf(sys.version_info < (3, 10), "Test requires Python 3.10+") + def test_with_temp_log(self): + """Test configure_logging API.""" + configure_logging(True, logging.INFO) + + with self.assertLogs("libyang", level="ERROR"): + self._cause_log() + + with self.assertNoLogs("libyang", level="ERROR"): + with temp_log_options(0): + self._cause_log() + + with self.assertLogs("libyang", level="ERROR"): + self._cause_log() diff --git a/tests/test_schema.py b/tests/test_schema.py index 3904fbef..2c6ae1bc 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -7,12 +7,37 @@ from libyang import ( Context, Extension, + ExtensionCompiled, + ExtensionParsed, + Identity, IfFeature, IfOrFeatures, IOType, LibyangError, Module, + Must, + PAction, + PActionInOut, + PAnydata, + Pattern, + PAugment, + PCase, + PChoice, + PContainer, + PGrouping, + PIdentity, + PLeaf, + PLeafList, + PList, + PNode, + PNotif, + PRefine, + PType, + PUses, Revision, + SAnydata, + SCase, + SChoice, SContainer, SLeaf, SLeafList, @@ -56,7 +81,7 @@ def test_mod_filepath(self): def test_mod_iter(self): children = list(iter(self.module)) - self.assertEqual(len(children), 5) + self.assertEqual(len(children), 6) def test_mod_children_rpcs(self): rpcs = list(self.module.children(types=(SNode.RPC,))) @@ -73,10 +98,21 @@ def test_mod_enable_features(self): self.assertTrue(self.module.feature_state("turbo-boost")) self.module.feature_disable_all() + def test_mod_imports(self): + imports = list(self.module.imports()) + self.assertEqual(imports[0].name(), "omg-extensions") + self.assertEqual(imports[1].name(), "wtf-types") + self.assertEqual(len(imports), 2) + def test_mod_features(self): features = list(self.module.features()) self.assertEqual(len(features), 2) + def test_mod_compiled_enabled_features(self): + self.module.feature_enable("*") + features = list(self.module.compiled_enabled_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") @@ -98,6 +134,19 @@ def test_mod_revisions(self): self.assertEqual(revisions[0].date(), "1999-04-01") self.assertEqual(revisions[1].date(), "1990-04-01") + def test_mod_extensions(self): + assert self.module is not None # pyright doesn't understand assertIsNotNone() + exts = list(self.module.extensions()) + self.assertEqual(len(exts), 1) + ext = self.module.get_extension("compile-validation", prefix="omg-extensions") + self.assertEqual(ext.argument(), "module-level") + sub_exts = list(ext.extensions()) + self.assertEqual(len(sub_exts), 1) + ext = sub_exts[0] + self.assertEqual(ext.name(), "compile-validation") + self.assertEqual(ext.module().name(), "omg-extensions") + self.assertEqual(ext.argument(), "module-sub-level") + # ------------------------------------------------------------------------------------- class RevisionTest(unittest.TestCase): @@ -166,18 +215,8 @@ def feature_disable_only(feature): continue self.mod.feature_enable(f.name()) - leaf_simple = next(self.ctx.find_path("/yolo-system:conf/yolo-system:speed")) - - self.mod.feature_disable_all() - leaf_not = next(self.ctx.find_path("/yolo-system:conf/yolo-system:offline")) - self.mod.feature_enable_all() - - leaf_and = next(self.ctx.find_path("/yolo-system:conf/yolo-system:full")) - leaf_or = next( - self.ctx.find_path("/yolo-system:conf/yolo-system:isolation-level") - ) - # if-feature is just a feature + leaf_simple = next(self.ctx.find_path("/yolo-system:conf/yolo-system:speed")) tree = next(leaf_simple.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), True) @@ -185,6 +224,8 @@ def feature_disable_only(feature): self.assertEqual(tree.state(), False) # if-feature is "NOT networking" + self.mod.feature_disable_all() + leaf_not = next(self.ctx.find_path("/yolo-system:conf/yolo-system:offline")) tree = next(leaf_not.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), False) @@ -192,6 +233,8 @@ def feature_disable_only(feature): self.assertEqual(tree.state(), True) # if-feature is "turbo-boost AND networking" + self.mod.feature_enable_all() + leaf_and = next(self.ctx.find_path("/yolo-system:conf/yolo-system:full")) tree = next(leaf_and.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), True) @@ -203,6 +246,9 @@ def feature_disable_only(feature): self.assertEqual(tree.state(), False) # if-feature is "turbo-boost OR networking" + leaf_or = next( + self.ctx.find_path("/yolo-system:conf/yolo-system:isolation-level") + ) tree = next(leaf_or.if_features()).tree() self.mod.feature_enable_all() self.assertEqual(tree.state(), True) @@ -231,7 +277,7 @@ def test_iffeature_dump(self): # ------------------------------------------------------------------------------------- class ContainerTest(unittest.TestCase): def setUp(self): - self.ctx = Context(YANG_DIR) + self.ctx = Context(YANG_DIR, compile_obsolete=True) mod = self.ctx.load_module("yolo-system") mod.feature_enable_all() self.container = next(self.ctx.find_path("/yolo-system:conf")) @@ -258,32 +304,112 @@ def test_cont_attrs(self): def test_cont_iter(self): children = list(iter(self.container)) - self.assertEqual(len(children), 9) + self.assertEqual(len(children), 11) def test_cont_children_leafs(self): leafs = list(self.container.children(types=(SNode.LEAF,))) - self.assertEqual(len(leafs), 7) + self.assertEqual(len(leafs), 9) + without_choice = [c.name() for c in self.container.children(with_choice=False)] + with_choice = [c.name() for c in self.container.children(with_choice=True)] + self.assertTrue("pill" not in without_choice) + self.assertTrue("pill" in with_choice) def test_cont_parent(self): self.assertIsNone(self.container.parent()) def test_iter_tree(self): tree = list(self.container.iter_tree()) - self.assertEqual(len(tree), 15) - tree = list(self.container.iter_tree(full=True)) self.assertEqual(len(tree), 20) + tree = list(self.container.iter_tree(full=True)) + self.assertEqual(len(tree), 25) + + def test_container_parsed(self): + pnode = self.container.parsed() + self.assertIsInstance(pnode, PContainer) + self.assertIsNone(next(pnode.musts(), None)) + self.assertIsNone(pnode.when_condition()) + self.assertIsNone(pnode.presence()) + self.assertIsNone(next(pnode.typedefs(), None)) + self.assertIsNone(next(pnode.groupings(), None)) + self.assertIsNotNone(next(iter(pnode))) + self.assertIsNone(next(pnode.actions(), None)) + self.assertIsNone(next(pnode.notifications(), None)) # ------------------------------------------------------------------------------------- -class ListTest(unittest.TestCase): +class UsesTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + mod = self.ctx.load_module("yolo-nodetypes") + mod.feature_enable_all() + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_uses_parsed(self): + snode = next(self.ctx.find_path("/yolo-nodetypes:cont2")) + self.assertIsInstance(snode, SContainer) + pnode = snode.parsed() + self.assertIsInstance(pnode, PContainer) + pnode = next(iter(pnode)) + self.assertIsInstance(pnode, PUses) + + ref_pnode = next(pnode.refines()) + self.assertIsInstance(ref_pnode, PRefine) + self.assertEqual("cont3/leaf1", ref_pnode.nodeid()) + self.assertIsNone(ref_pnode.description()) + self.assertIsNone(ref_pnode.reference()) + self.assertIsNone(next(ref_pnode.if_features(), None)) + self.assertIsNone(next(ref_pnode.musts(), None)) + self.assertIsNone(ref_pnode.presence()) + self.assertIsNone(next(ref_pnode.defaults(), None)) + self.assertEqual(0, ref_pnode.min_elements()) + self.assertIsNone(ref_pnode.max_elements()) + self.assertIsNone(next(ref_pnode.extensions(), None)) + + aug_pnode = next(pnode.augments()) + self.assertIsInstance(aug_pnode, PAugment) + self.assertIsNotNone(next(iter(aug_pnode))) + self.assertIsNone(aug_pnode.when_condition()) + self.assertIsNone(next(aug_pnode.actions(), None)) + self.assertIsNone(next(aug_pnode.notifications(), None)) - SCHEMA_PATH = "/yolo-system:conf/url" - DATA_PATH = "/yolo-system:conf/url[host='%s'][proto='%s']" + +# ------------------------------------------------------------------------------------- +class GroupingTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_grouping_parsed(self): + mod = self.ctx.load_module("yolo-nodetypes") + pnode = next(mod.groupings()) + self.assertIsInstance(pnode, PGrouping) + self.assertIsNone(next(pnode.typedefs(), None)) + self.assertIsNone(next(pnode.groupings(), None)) + child = next(iter(pnode)) + self.assertIsNotNone(child) + self.assertIsNone(next(pnode.actions(), None)) + self.assertIsNone(next(pnode.notifications(), None)) + + +# ------------------------------------------------------------------------------------- +class ListTest(unittest.TestCase): + PATH = { + "LOG": "/yolo-system:conf/url", + "DATA": "/yolo-system:conf/url", + "DATA_PATTERN": "/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)) + self.ctx.load_module("yolo-nodetypes") + self.list = next(self.ctx.find_path(self.PATH["LOG"])) def tearDown(self): self.list = None @@ -295,9 +421,11 @@ def test_list_attrs(self): 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.schema_path(), self.PATH["LOG"]) - self.assertEqual(self.list.data_path(), self.DATA_PATH) + self.assertEqual(self.list.schema_path(SNode.PATH_DATA), self.PATH["DATA"]) + + self.assertEqual(self.list.data_path(), self.PATH["DATA_PATTERN"]) self.assertFalse(self.list.ordered()) def test_list_keys(self): @@ -318,6 +446,51 @@ def test_list_parent(self): self.assertIsInstance(parent, SContainer) self.assertEqual(parent.name(), "conf") + def test_list_uniques(self): + list1 = next(self.ctx.find_path("/yolo-nodetypes:conf/list1")) + self.assertIsInstance(list1, SList) + uniques = list(list1.uniques()) + self.assertEqual(len(uniques), 1) + elements = [u.name() for u in uniques[0]] + self.assertEqual(len(elements), 2) + self.assertTrue("leaf2" in elements) + self.assertTrue("leaf3" in elements) + + list2 = next(self.ctx.find_path("/yolo-nodetypes:conf/list2")) + self.assertIsInstance(list2, SList) + uniques = list(list2.uniques()) + self.assertEqual(len(uniques), 0) + + def test_list_min_max(self): + list1 = next(self.ctx.find_path("/yolo-nodetypes:conf/list1")) + self.assertIsInstance(list1, SList) + self.assertEqual(list1.min_elements(), 2) + self.assertEqual(list1.max_elements(), 10) + + list2 = next(self.ctx.find_path("/yolo-nodetypes:conf/list2")) + self.assertIsInstance(list2, SList) + self.assertEqual(list2.min_elements(), 0) + self.assertEqual(list2.max_elements(), None) + + def test_list_parsed(self): + list1 = next(self.ctx.find_path("/yolo-nodetypes:conf/list1")) + self.assertIsInstance(list1, SList) + pnode = list1.parsed() + self.assertIsInstance(pnode, PList) + self.assertIsNone(next(pnode.musts(), None)) + self.assertIsNone(pnode.when_condition()) + self.assertEqual("leaf1", pnode.key()) + self.assertIsNone(next(pnode.typedefs(), None)) + self.assertIsNone(next(pnode.groupings(), None)) + child = next(iter(pnode)) + self.assertIsInstance(child, PLeaf) + self.assertIsNone(next(pnode.actions(), None)) + self.assertIsNone(next(pnode.notifications(), None)) + self.assertEqual("leaf2 leaf3", next(pnode.uniques())) + self.assertEqual(2, pnode.min_elements()) + self.assertEqual(10, pnode.max_elements()) + self.assertFalse(pnode.ordered()) + # ------------------------------------------------------------------------------------- class RpcTest(unittest.TestCase): @@ -336,12 +509,21 @@ def test_rpc_attrs(self): self.assertEqual(self.rpc.nodetype(), SNode.RPC) self.assertEqual(self.rpc.keyword(), "rpc") self.assertEqual(self.rpc.schema_path(), "/yolo-system:format-disk") + choice = next(self.rpc.input().children((SNode.CHOICE,), with_choice=True)) + self.assertIsInstance(choice, SChoice) 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) + self.assertIsInstance(ext, ExtensionCompiled) + self.assertIsInstance(ext.parent_node(), SRpc) + self.assertIsNone(next(ext.extensions(), None)) + parsed = self.rpc.parsed() + ext = parsed.get_extension("require-admin", prefix="omg-extensions") + self.assertIsInstance(ext, ExtensionParsed) + self.assertIsInstance(ext.parent_node(), PAction) + self.assertIsNone(next(ext.extensions(), None)) def test_rpc_params(self): leaf = next(self.rpc.children()) @@ -353,11 +535,26 @@ def test_rpc_params(self): def test_rpc_no_parent(self): self.assertIsNone(self.rpc.parent()) + def test_rpc_parsed(self): + self.assertIsInstance(self.rpc, SRpc) + pnode = self.rpc.parsed() + self.assertIsInstance(pnode, PAction) + self.assertIsNone(next(pnode.typedefs(), None)) + self.assertIsNone(next(pnode.groupings(), None)) + pnode2 = pnode.input() + self.assertIsInstance(pnode2, PActionInOut) + self.assertIsInstance(pnode.output(), PActionInOut) + self.assertIsNone(next(pnode2.musts(), None)) + self.assertIsNone(next(pnode2.typedefs(), None)) + self.assertIsNone(next(pnode2.groupings(), None)) + pnode3 = next(iter(pnode2)) + self.assertIsInstance(pnode3, PLeaf) + # ------------------------------------------------------------------------------------- class LeafTypeTest(unittest.TestCase): def setUp(self): - self.ctx = Context(YANG_DIR) + self.ctx = Context(YANG_DIR, compile_obsolete=True) self.ctx.load_module("yolo-system") def tearDown(self): @@ -371,9 +568,13 @@ def test_leaf_type_derived(self): self.assertIsInstance(t, Type) self.assertEqual(t.name(), "types:host") self.assertEqual(t.base(), Type.STRING) - # mod = t.module() - # self.assertIsNot(mod, None) - # self.assertEqual(mod.name(), "wtf-types") + mod = t.module() + self.assertIsNot(mod, None) + self.assertEqual(mod.name(), "yolo-system") + self.assertEqual(mod.get_module_from_prefix("types").name(), "wtf-types") + self.assertEqual(t.typedef().name(), "host") + self.assertEqual(t.typedef().description(), "my host type.") + self.assertEqual(t.description(), "my host type.") def test_leaf_type_status(self): leaf = next(self.ctx.find_path("/yolo-system:conf/yolo-system:hostname")) @@ -397,6 +598,15 @@ def test_leaf_type_pattern(self): t = leaf.type() self.assertIsInstance(t, Type) self.assertEqual(list(t.patterns()), [("[a-z.]+", False), ("1", True)]) + patterns = list(t.all_pattern_details()) + self.assertEqual(len(patterns), 2) + self.assertIsInstance(patterns[0], Pattern) + self.assertEqual(patterns[0].expression(), "[a-z.]+") + self.assertFalse(patterns[0].inverted()) + self.assertEqual(patterns[0].error_message(), "ERROR1") + self.assertEqual(patterns[1].expression(), "1") + self.assertTrue(patterns[1].inverted()) + self.assertIsNone(patterns[1].error_message()) def test_leaf_type_union(self): leaf = next(self.ctx.find_path("/yolo-system:conf/yolo-system:number")) @@ -406,7 +616,15 @@ def test_leaf_type_union(self): self.assertEqual(t.name(), "types:number") self.assertEqual(t.base(), Type.UNION) types = set(u.name() for u in t.union_types()) + types2 = set(u.name() for u in t.union_types(with_typedefs=True)) self.assertEqual(types, set(["int16", "int32", "uint16", "uint32"])) + self.assertEqual(types2, set(["signed", "unsigned"])) + for u in t.union_types(): + ext = u.get_extension( + "type-desc", prefix="omg-extensions", arg_value=f"<{u.name()}>" + ) + self.assertIsInstance(ext, Extension) + self.assertEqual(len(list(u.extensions())), 2) bases = set(t.basenames()) self.assertEqual(bases, set(["int16", "int32", "uint16", "uint32"])) @@ -419,6 +637,7 @@ def test_leaf_type_extensions(self): "type-desc", prefix="omg-extensions", arg_value="<protocol>" ) self.assertIsInstance(ext, Extension) + self.assertIsNone(ext.parent_node()) def test_leaf_type_enum(self): leaf = next( @@ -431,6 +650,9 @@ def test_leaf_type_enum(self): self.assertEqual(t.base(), Type.ENUM) enums = [e.name() for e in t.enums()] self.assertEqual(enums, ["http", "https", "ftp", "sftp"]) + enum = next(t.enums()) + self.assertIsNone(next(enum.extensions(), None)) + self.assertIsNone(enum.get_extension("test", prefix="test")) def test_leaf_type_bits(self): leaf = next(self.ctx.find_path("/yolo-system:chmod/yolo-system:perms")) @@ -450,3 +672,347 @@ def test_leaf_parent(self): self.assertIsNotNone(parent) self.assertIsInstance(parent, SList) self.assertEqual(parent.name(), "url") + + def test_iter_tree(self): + leaf = next(self.ctx.find_path("/yolo-system:conf")) + self.assertEqual(len(list(leaf.iter_tree(full=True))), 23) + + def test_leaf_type_fraction_digits(self): + self.ctx.load_module("yolo-nodetypes") + leaf = next(self.ctx.find_path("/yolo-nodetypes:conf/percentage")) + self.assertIsInstance(leaf, SLeaf) + t = leaf.type() + self.assertIsInstance(t, Type) + self.assertEqual(next(t.all_fraction_digits(), None), 2) + + def test_leaf_type_require_instance(self): + leaf = next(self.ctx.find_path("/yolo-system:conf/hostname-ref")) + self.assertIsInstance(leaf, SLeaf) + t = leaf.type() + self.assertIsInstance(t, Type) + self.assertFalse(t.require_instance()) + + def test_leaf_type_parsed(self): + leaf = next(self.ctx.find_path("/yolo-system:conf/yolo-system:hostname")) + self.assertIsInstance(leaf, SLeaf) + t = leaf.type() + self.assertIsInstance(t, Type) + pnode = t.parsed() + self.assertIsInstance(pnode, PType) + self.assertEqual("types:host", pnode.name()) + self.assertIsNone(pnode.range()) + self.assertIsNone(pnode.length()) + self.assertIsNone(next(pnode.patterns(), None)) + self.assertIsNone(next(pnode.enums(), None)) + self.assertIsNone(next(pnode.bits(), None)) + self.assertIsNone(pnode.path()) + self.assertIsNone(next(pnode.bases(), None)) + self.assertIsNone(next(pnode.types(), None)) + self.assertIsNone(next(pnode.extensions(), None)) + self.assertIsNotNone(pnode.pmod()) + self.assertIsNone(pnode.compiled()) + self.assertEqual(0, pnode.fraction_digits()) + self.assertFalse(pnode.require_instance()) + + +# ------------------------------------------------------------------------------------- +class LeafTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module("yolo-nodetypes") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_must(self): + leaf = next(self.ctx.find_path("/yolo-nodetypes:conf/percentage")) + self.assertIsInstance(leaf, SLeaf) + must = next(leaf.musts(), None) + self.assertIsInstance(must, Must) + self.assertEqual(must.error_message(), "ERROR1") + must = next(leaf.must_conditions(), None) + self.assertIsInstance(must, str) + + def test_leaf_default(self): + leaf = next(self.ctx.find_path("/yolo-nodetypes:conf/percentage")) + self.assertIsInstance(leaf.default(), float) + leaf = next(self.ctx.find_path("/yolo-nodetypes:conf/leafref1")) + self.assertIsInstance(leaf.default(), str) + self.assertEqual("ASD", leaf.default()) + + def test_leaf_parsed(self): + leaf = next(self.ctx.find_path("/yolo-nodetypes:conf/percentage")) + self.assertIsInstance(leaf, SLeaf) + pnode = leaf.parsed() + self.assertIsInstance(pnode, PLeaf) + must = next(pnode.musts()) + self.assertIsInstance(must, Must) + self.assertEqual(must.error_message(), "ERROR1") + must = next(leaf.must_conditions()) + self.assertIsInstance(must, str) + self.assertIsNone(pnode.when_condition()) + self.assertIsInstance(pnode.type(), PType) + self.assertIsNone(pnode.units()) + self.assertEqual("10.2", pnode.default()) + self.assertFalse(pnode.is_key()) + + # test basic PNode settings + self.assertIsNotNone(pnode.parent()) + self.assertEqual(PNode.LEAF, pnode.nodetype()) + self.assertIsNotNone(next(pnode.siblings())) + self.assertEqual("<libyang.schema.PLeaf: percentage>", repr(pnode)) + self.assertIsNone(pnode.description()) + self.assertIsNone(pnode.reference()) + self.assertIsNone(next(pnode.if_features(), None)) + self.assertIsNone(next(pnode.extensions(), None)) + self.assertIsNone(pnode.get_extension("test", prefix="test")) + self.assertFalse(pnode.config_set()) + self.assertFalse(pnode.config_false()) + self.assertFalse(pnode.mandatory()) + self.assertFalse(pnode.deprecated()) + self.assertFalse(pnode.obsolete()) + self.assertEqual("current", pnode.status()) + + NODETYPE_CLASS = {} + + +# ------------------------------------------------------------------------------------- +class LeafListTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module("yolo-nodetypes") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_leaflist_defaults(self): + leaflist = next(self.ctx.find_path("/yolo-nodetypes:conf/ratios")) + for d in leaflist.defaults(): + self.assertIsInstance(d, float) + leaflist = next(self.ctx.find_path("/yolo-nodetypes:conf/bools")) + for d in leaflist.defaults(): + self.assertIsInstance(d, bool) + leaflist = next(self.ctx.find_path("/yolo-nodetypes:conf/integers")) + for d in leaflist.defaults(): + self.assertIsInstance(d, int) + leaflist3 = next(self.ctx.find_path("/yolo-nodetypes:conf/leaf-list3")) + for d in leaflist3.defaults(): + self.assertIsInstance(d, str) + + def test_leaf_list_min_max(self): + leaflist1 = next(self.ctx.find_path("/yolo-nodetypes:conf/leaf-list1")) + self.assertIsInstance(leaflist1, SLeafList) + self.assertEqual(leaflist1.min_elements(), 3) + self.assertEqual(leaflist1.max_elements(), 11) + + leaflist2 = next(self.ctx.find_path("/yolo-nodetypes:conf/leaf-list2")) + self.assertIsInstance(leaflist2, SLeafList) + self.assertEqual(leaflist2.min_elements(), 0) + self.assertEqual(leaflist2.max_elements(), None) + + def test_leaf_list_parsed(self): + leaflist = next(self.ctx.find_path("/yolo-nodetypes:conf/ratios")) + self.assertIsInstance(leaflist, SLeafList) + pnode = leaflist.parsed() + self.assertIsInstance(pnode, PLeafList) + self.assertIsNone(next(pnode.musts(), None)) + self.assertIsNone(pnode.when_condition()) + self.assertIsInstance(pnode.type(), PType) + self.assertIsNone(pnode.units()) + self.assertEqual("2.5", next(pnode.defaults())) + self.assertEqual(0, pnode.min_elements()) + self.assertIsNone(pnode.max_elements()) + self.assertFalse(pnode.ordered()) + + +# ------------------------------------------------------------------------------------- +class BacklinksTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module("yolo-leafref-search") + self.ctx.load_module("yolo-leafref-search-extmod") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_backlinks_all_nodes(self): + expected = [ + "/yolo-leafref-search-extmod:my_extref_list/my_extref", + "/yolo-leafref-search:refstr", + "/yolo-leafref-search:refnum", + "/yolo-leafref-search-extmod:my_extref_list/my_extref_union", + ] + refs = self.ctx.find_backlinks_paths() + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + def test_backlinks_one(self): + expected = [ + "/yolo-leafref-search-extmod:my_extref_list/my_extref", + "/yolo-leafref-search:refstr", + "/yolo-leafref-search-extmod:my_extref_list/my_extref_union", + ] + refs = self.ctx.find_backlinks_paths( + match_path="/yolo-leafref-search:my_list/my_leaf_string" + ) + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + def test_backlinks_children(self): + expected = [ + "/yolo-leafref-search-extmod:my_extref_list/my_extref", + "/yolo-leafref-search:refstr", + "/yolo-leafref-search:refnum", + "/yolo-leafref-search-extmod:my_extref_list/my_extref_union", + ] + refs = self.ctx.find_backlinks_paths( + match_path="/yolo-leafref-search:my_list", match_ancestors=True + ) + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + def test_backlinks_leafref_target_paths(self): + expected = ["/yolo-leafref-search:my_list/my_leaf_string"] + refs = self.ctx.find_leafref_path_target_paths( + "/yolo-leafref-search-extmod:my_extref_list/my_extref" + ) + expected.sort() + refs.sort() + self.assertEqual(expected, refs) + + +# ------------------------------------------------------------------------------------- +class ChoiceTest(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_choice_default(self): + conf = next(self.ctx.find_path("/yolo-system:conf")) + choice = next(conf.children((SNode.CHOICE,), with_choice=True)) + self.assertIsInstance(choice, SChoice) + self.assertIsInstance(choice.default(), SCase) + + def test_choice_parsed(self): + conf = next(self.ctx.find_path("/yolo-system:conf")) + choice = next(conf.children((SNode.CHOICE,), with_choice=True)) + self.assertIsInstance(choice, SChoice) + pnode = choice.parsed() + self.assertIsInstance(pnode, PChoice) + + case_pnode = next(iter(pnode)) + self.assertIsInstance(case_pnode, PCase) + self.assertIsNotNone(next(iter(case_pnode))) + self.assertIsNone(case_pnode.when_condition()) + + self.assertIsNone(pnode.when_condition()) + self.assertEqual("red", pnode.default()) + + +# ------------------------------------------------------------------------------------- +class AnydataTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module("yolo-nodetypes") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_anydata(self): + snode = next(self.ctx.find_path("/yolo-nodetypes:any1")) + self.assertIsInstance(snode, SAnydata) + assert next(snode.when_conditions()) is not None + snode2 = next(snode.when_conditions_nodes()) + assert isinstance(snode2, SAnydata) + assert snode2.cdata == snode.cdata + + def test_anydata_parsed(self): + snode = next(self.ctx.find_path("/yolo-nodetypes:any1")) + self.assertIsInstance(snode, SAnydata) + pnode = snode.parsed() + self.assertIsInstance(pnode, PAnydata) + self.assertIsNone(next(pnode.musts(), None)) + self.assertEqual("../cont2", pnode.when_condition()) + + +# ------------------------------------------------------------------------------------- +class NotificationTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.ctx.load_module("yolo-nodetypes") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_notification_parsed(self): + snode = next(self.ctx.find_path("/yolo-nodetypes:cont2")) + self.assertIsInstance(snode, SContainer) + pnode = snode.parsed() + self.assertIsInstance(pnode, PContainer) + pnode = next(pnode.notifications()) + self.assertIsInstance(pnode, PNotif) + self.assertIsNone(next(pnode.musts(), None)) + self.assertIsNone(next(pnode.typedefs(), None)) + self.assertIsNone(next(pnode.groupings(), None)) + self.assertIsNotNone(next(iter(pnode))) + + +# ------------------------------------------------------------------------------------- +class IdentityTest(unittest.TestCase): + def setUp(self): + self.ctx = Context(YANG_DIR) + self.module = self.ctx.load_module("yolo-nodetypes") + + def tearDown(self): + self.ctx.destroy() + self.ctx = None + + def test_identity_compiled(self): + sidentity = next(self.module.identities()) + self.assertIsInstance(sidentity, Identity) + self.assertEqual(sidentity.name(), "base1") + self.assertEqual(sidentity.description(), "Base 1.") + self.assertEqual(sidentity.reference(), "Some reference.") + self.assertIsInstance(sidentity.module(), Module) + derived = list(sidentity.derived()) + self.assertEqual(2, len(derived)) + for i in derived: + self.assertIsInstance(i, Identity) + self.assertEqual(derived[0].name(), "derived1") + self.assertEqual(derived[1].name(), "derived2") + self.assertEqual(next(derived[1].extensions()).name(), "identity-name") + self.assertIsNone(next(sidentity.extensions(), None)) + self.assertIsNone(sidentity.get_extension("ext1")) + self.assertFalse(sidentity.deprecated()) + self.assertFalse(sidentity.obsolete()) + self.assertEqual("current", sidentity.status()) + + snode = next(self.ctx.find_path("/yolo-nodetypes:identity_ref")) + identities = list(snode.type().identity_bases()) + self.assertEqual(identities[0].name(), sidentity.name()) + self.assertEqual(identities[1].name(), "base2") + + def test_identity_parsed(self): + pidentity = next(self.module.parsed_identities()) + self.assertIsInstance(pidentity, PIdentity) + self.assertEqual(pidentity.name(), "base1") + self.assertIsNone(next(pidentity.if_features(), None)) + self.assertIsNone(next(pidentity.bases(), None)) + self.assertEqual(pidentity.description(), "Base 1.") + self.assertEqual(pidentity.reference(), "Some reference.") + self.assertIsNone(next(pidentity.extensions(), None)) + self.assertFalse(pidentity.deprecated()) + self.assertFalse(pidentity.obsolete()) + self.assertEqual("current", pidentity.status()) diff --git a/tests/test_xpath.py b/tests/test_xpath.py index bf7c7bc7..0901a7ec 100644 --- a/tests/test_xpath.py +++ b/tests/test_xpath.py @@ -41,6 +41,11 @@ def test_xpath_set(self): ) ly.xpath_set(d, "/lstnum[.='100']", 100) ly.xpath_set(d, "/lstnum[.='1']", 1, after="") + ly.xpath_set(d, "/lstnum[5]", 33, after="4") + ly.xpath_set(d, "/lstnum[5]", 34, after="4") + ly.xpath_set(d, "/lstnum[5]", 35, after="4") + ly.xpath_set(d, "/lstnum[7]", 101, after="6") + ly.xpath_set(d, "/lstnum[8]", 102, after="7") with self.assertRaises(ValueError): ly.xpath_set(d, "/lstnum[.='1000']", 1000, after="1000000") with self.assertRaises(ValueError): @@ -101,7 +106,7 @@ def test_xpath_set(self): {"name": "eth3", "mtu": 1000}, ], "lst2": ["a", "b", "c"], - "lstnum": [1, 10, 20, 30, 40, 100], + "lstnum": [1, 10, 20, 30, 35, 100, 101, 102], "val": 43, }, ) diff --git a/tests/yang/omg/omg-extensions.yang b/tests/yang/omg/omg-extensions.yang index fe20e7e5..926bf3db 100644 --- a/tests/yang/omg/omg-extensions.yang +++ b/tests/yang/omg/omg-extensions.yang @@ -18,4 +18,14 @@ module omg-extensions { "Extend a type to add a desc."; argument name; } + + extension parse-validation { + description + "Example of parse-validation extension which should be put only under leaf nodes."; + } + + extension compile-validation { + description + "Example of compile-validation extension which should be put only under leaf nodes."; + } } diff --git a/tests/yang/wtf/wtf-types.yang b/tests/yang/wtf/wtf-types.yang index 6d712854..75a0a778 100644 --- a/tests/yang/wtf/wtf-types.yang +++ b/tests/yang/wtf/wtf-types.yang @@ -2,6 +2,8 @@ module wtf-types { namespace "urn:yang:wtf:types"; prefix t; + import omg-extensions { prefix ext; } + typedef str { type string; } @@ -10,19 +12,36 @@ module wtf-types { type str { pattern "[a-z]+"; } + description + "my host type."; } + extension signed; + extension unsigned; + typedef unsigned { type union { - type uint16; - type uint32; + type uint16 { + t:unsigned; + ext:type-desc "<uint16>"; + } + type uint32 { + ext:type-desc "<uint32>"; + t:unsigned; + } } } typedef signed { type union { - type int16; - type int32; + type int16 { + ext:type-desc "<int16>"; + t:signed; + } + type int32 { + ext:type-desc "<int32>"; + t:signed; + } } } diff --git a/tests/yang/yang-library.json b/tests/yang/yang-library.json index 3dbf2568..f9664845 100644 --- a/tests/yang/yang-library.json +++ b/tests/yang/yang-library.json @@ -11,6 +11,21 @@ ] }, "ietf-yang-library:yang-library": { + "module-set": [ + { + "name": "complete", + "module": [ + { + "name": "yang", + "revision": "2025-01-29", + "namespace": "urn:ietf:params:xml:ns:yang:1", + "location": [ + "file://@internal" + ] + } + ] + } + ], "content-id": "321566" } } diff --git a/tests/yang/yolo/yolo-leafref-extended.yang b/tests/yang/yolo/yolo-leafref-extended.yang new file mode 100644 index 00000000..0aa8bd2f --- /dev/null +++ b/tests/yang/yolo/yolo-leafref-extended.yang @@ -0,0 +1,31 @@ +module yolo-leafref-extended { + yang-version 1.1; + namespace "urn:yang:yolo:leafref-extended"; + prefix leafref-ext; + + revision 2025-01-25 { + description + "Initial version."; + } + + list list1 { + key leaf1; + leaf leaf1 { + type string; + } + leaf-list leaflist2 { + type string; + } + } + + leaf ref1 { + type leafref { + path "../list1/leaf1"; + } + } + leaf ref2 { + type leafref { + path "deref(../ref1)/../leaflist2"; + } + } +} diff --git a/tests/yang/yolo/yolo-leafref-search-extmod.yang b/tests/yang/yolo/yolo-leafref-search-extmod.yang new file mode 100644 index 00000000..046ceec5 --- /dev/null +++ b/tests/yang/yolo/yolo-leafref-search-extmod.yang @@ -0,0 +1,39 @@ +module yolo-leafref-search-extmod { + yang-version 1.1; + namespace "urn:yang:yolo:leafref-search-extmod"; + prefix leafref-search-extmod; + + import wtf-types { prefix types; } + + import yolo-leafref-search { + prefix leafref-search; + } + + revision 2025-02-11 { + description + "Initial version."; + } + + list my_extref_list { + key my_leaf_string; + leaf my_leaf_string { + type string; + } + leaf my_extref { + type leafref { + path "/leafref-search:my_list/leafref-search:my_leaf_string"; + } + } + leaf my_extref_union { + type union { + type leafref { + path "/leafref-search:my_list/leafref-search:my_leaf_string"; + } + type leafref { + path "/leafref-search:my_list/leafref-search:my_leaf_number"; + } + type types:number; + } + } + } +} diff --git a/tests/yang/yolo/yolo-leafref-search.yang b/tests/yang/yolo/yolo-leafref-search.yang new file mode 100644 index 00000000..5f4af488 --- /dev/null +++ b/tests/yang/yolo/yolo-leafref-search.yang @@ -0,0 +1,36 @@ +module yolo-leafref-search { + yang-version 1.1; + namespace "urn:yang:yolo:leafref-search"; + prefix leafref-search; + + import wtf-types { prefix types; } + + revision 2025-02-11 { + description + "Initial version."; + } + + list my_list { + key my_leaf_string; + leaf my_leaf_string { + type string; + } + leaf my_leaf_number { + description + "A number."; + type types:number; + } + } + + leaf refstr { + type leafref { + path "../my_list/my_leaf_string"; + } + } + + leaf refnum { + type leafref { + path "../my_list/my_leaf_number"; + } + } +} diff --git a/tests/yang/yolo/yolo-nodetypes.yang b/tests/yang/yolo/yolo-nodetypes.yang new file mode 100644 index 00000000..9926b1aa --- /dev/null +++ b/tests/yang/yolo/yolo-nodetypes.yang @@ -0,0 +1,185 @@ +module yolo-nodetypes { + yang-version 1.1; + namespace "urn:yang:yolo:nodetypes"; + prefix sys; + + import ietf-inet-types { + prefix inet; + revision-date 2013-07-15; + } + + description + "YOLO Nodetypes."; + + revision 2024-01-25 { + description + "Initial version."; + } + + list records { + key id; + leaf id { + type string; + } + leaf name { + type string; + default "ASD"; + } + ordered-by user; + } + + container conf { + presence "enable conf"; + description + "Configuration."; + leaf percentage { + type decimal64 { + fraction-digits 2; + } + default 10.2; + must ". = 10.6" { + error-message "ERROR1"; + } + } + + leaf-list ratios { + type decimal64 { + fraction-digits 2; + } + default 2.5; + default 2.6; + } + + leaf-list bools { + type boolean; + default true; + } + + leaf-list integers { + type uint32; + default 10; + default 20; + } + + list list1 { + key leaf1; + unique "leaf2 leaf3"; + min-elements 2; + max-elements 10; + leaf leaf1 { + type string; + } + leaf leaf2 { + type string; + } + leaf leaf3 { + type string; + } + } + + list list2 { + key leaf1; + leaf leaf1 { + type string; + } + } + + leaf-list leaf-list1 { + type string; + min-elements 3; + max-elements 11; + } + + leaf-list leaf-list2 { + type string; + } + + leaf leafref1 { + type leafref { + path "/records/name"; + } + default "ASD"; + } + + leaf-list leaf-list3 { + type leafref { + path "/records/name"; + } + default "ASD"; + } + } + + leaf test1 { + type uint8 { + range "2..20"; + } + } + + grouping grp1 { + container cont3 { + leaf leaf1 { + type string; + } + } + } + + container cont2 { + presence "special container enabled"; + uses grp1 { + refine cont3/leaf1 { + mandatory true; + } + augment cont3 { + leaf leaf2 { + type int8; + } + } + } + notification interface-enabled { + leaf by-user { + type string; + } + } + } + + anydata any1 { + when "../cont2"; + } + + extension identity-name { + description + "Extend an identity to provide an alternative name."; + argument name; + } + + identity base1 { + description + "Base 1."; + reference "Some reference."; + } + identity base2; + + identity derived1 { + base base1; + } + + identity derived2 { + base base1; + sys:identity-name "Derived2"; + } + + identity derived3 { + base derived1; + } + + leaf identity_ref { + type identityref { + base base1; + base base2; + } + } + + leaf ip-address { + type inet:ipv4-address; + } +} diff --git a/tests/yang/yolo/yolo-system.yang b/tests/yang/yolo/yolo-system.yang index 9ee49f04..a02f310c 100644 --- a/tests/yang/yolo/yolo-system.yang +++ b/tests/yang/yolo/yolo-system.yang @@ -43,6 +43,7 @@ module yolo-system { leaf hostname-ref { type leafref { path "../hostname"; + require-instance false; } } @@ -60,6 +61,20 @@ module yolo-system { type string; } + choice pill { + case red { + leaf out { + type boolean; + } + } + case blue { + leaf in { + type boolean; + } + } + default red; + } + list url { description "An URL."; @@ -68,10 +83,13 @@ module yolo-system { type types:protocol { ext:type-desc "<protocol>"; } + ext:parse-validation; } leaf host { type string { - pattern "[a-z.]+"; + pattern "[a-z.]+" { + error-message "ERROR1"; + } pattern "1" { modifier "invert-match"; } @@ -97,6 +115,7 @@ module yolo-system { type string; } } + ext:compile-validation; } } @@ -136,6 +155,10 @@ module yolo-system { } } + ext:compile-validation "module-level" { + ext:compile-validation "module-sub-level"; + } + container conf { description "Configuration."; @@ -172,7 +195,14 @@ module yolo-system { ext:human-name "Disk"; type types:str; } - anyxml html-info; + choice xml-or-json { + case xml { + anyxml html-info; + } + case json { + anydata json-info; + } + } } output { leaf duration { @@ -191,4 +221,12 @@ module yolo-system { type uint32; } } + + notification config-change { + list edit { + leaf target { + type string; + } + } + } } diff --git a/tox-install.sh b/tox-install.sh index 428659d9..56e6e2a7 100755 --- a/tox-install.sh +++ b/tox-install.sh @@ -32,7 +32,7 @@ download() # build and install libyang into the virtualenv src="${LIBYANG_SRC:-$venv/.src}" if ! [ -d "$src" ]; then - libyang_branch="${LIBYANG_BRANCH:-devel}" + libyang_branch="${LIBYANG_BRANCH:-master}" download "https://github.com/CESNET/libyang" "$libyang_branch" "$src" fi diff --git a/tox.ini b/tox.ini index c3186ae7..524ad49c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = format,lint,py{36,37,38,39,310,py3},coverage +envlist = format,lint,py{39,310,311,312,313,py3},lydevel,coverage skip_missing_interpreters = true isolated_build = true distdir = {toxinidir}/dist @@ -11,12 +11,20 @@ basepython = python3 description = Compile extension and run tests against {envname}. changedir = tests/ install_command = {toxinidir}/tox-install.sh {envdir} {opts} {packages} +allowlist_externals = + {toxinidir}/tox-install.sh commands = python -Wd -m unittest discover -c +[testenv:lydevel] +setenv = + LIBYANG_BRANCH=devel + [testenv:coverage] changedir = . deps = coverage install_command = {toxinidir}/tox-install.sh {envdir} {opts} {packages} +allowlist_externals = + {toxinidir}/tox-install.sh commands = python -Wd -m coverage run -m unittest discover -c tests/ python -m coverage report @@ -28,11 +36,11 @@ basepython = python3 description = Format python code using isort and black. changedir = . deps = - black==22.10.0 - isort + black~=25.1.0 + isort~=6.0.0 skip_install = true install_command = python3 -m pip install {opts} {packages} -whitelist_externals = +allowlist_externals = /bin/sh /usr/bin/sh commands = @@ -44,13 +52,18 @@ basepython = python3 description = Run coding style checks. changedir = . deps = - black==22.10.0 - flake8 - isort - pylint -whitelist_externals = + astroid~=3.3.8 + black~=25.1.0 + flake8~=7.1.1 + isort~=6.0.0 + pycodestyle~=2.12.1 + pyflakes~=3.2.0 + pylint~=3.3.4 + setuptools~=75.8.0 +allowlist_externals = /bin/sh /usr/bin/sh + {toxinidir}/tox-install.sh commands = sh -ec 'python3 -m black -t py36 --diff --check $(git ls-files "*.py")' sh -ec 'python3 -m flake8 $(git ls-files "*.py")'