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 4c308738..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,18 +30,25 @@ 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 @@ -41,13 +56,15 @@ jobs: 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 @@ -57,22 +74,6 @@ jobs: - run: python -m pip install --upgrade tox - run: python -m tox -e ${{ matrix.toxenv }} - libyang_devel: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.x - - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: pip - restore-keys: pip - - run: python -m pip install --upgrade pip setuptools wheel - - run: python -m pip install --upgrade tox - - run: python -m tox -e lydevel - coverage: runs-on: ubuntu-latest steps: @@ -93,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 90147aaa..fc9a6ea3 100644 --- a/Makefile +++ b/Makefile @@ -12,4 +12,9 @@ tests: 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 0387cf57..4ea8977f 100644 --- a/README.rst +++ b/README.rst @@ -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 304ae55d..82ec9748 100644 --- a/cffi/cdefs.h +++ b/cffi/cdefs.h @@ -15,6 +15,9 @@ struct ly_ctx; #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 { @@ -172,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 ... @@ -209,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 { @@ -238,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 { @@ -261,11 +271,13 @@ 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); @@ -287,29 +299,34 @@ 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 ... @@ -334,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, @@ -350,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; @@ -362,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 { @@ -420,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 { @@ -447,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; @@ -526,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; @@ -538,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; @@ -551,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; ...; }; @@ -581,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; ...; @@ -613,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; @@ -641,6 +826,7 @@ struct lysp_type { struct lysp_qname { const char *str; const struct lysp_module *mod; + ...; }; struct lysp_node { @@ -680,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; }; @@ -697,17 +882,18 @@ const struct lysc_node* lys_find_child(const struct lysc_node *, const struct ly 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 { @@ -771,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 *); @@ -798,7 +989,6 @@ typedef struct pcre2_real_code pcre2_code; struct lysc_pattern { const char *expr; - pcre2_code *code; const char *dsc; const char *ref; const char *emsg; @@ -820,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; @@ -838,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; @@ -859,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; @@ -935,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; @@ -948,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 { @@ -957,7 +1173,6 @@ union lyd_any_value { const char *str; const char *xml; const char *json; - char *mem; }; struct lyd_node_any { @@ -988,6 +1203,8 @@ 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 ... @@ -1051,9 +1268,10 @@ LY_ERR lyd_merge_module(struct lyd_node **, const struct lyd_node *, const struc #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; @@ -1106,5 +1324,53 @@ struct lyd_attr { 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 2682dd88..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 < 37) -#error "Need at least libyang 2.37" +#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 bc79e2f0..d8f95556 100644 --- a/libyang/__init__.py +++ b/libyang/__init__.py @@ -63,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, @@ -76,8 +81,28 @@ 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, @@ -120,12 +145,17 @@ "DefaultRemoved", "DescriptionAdded", "DescriptionRemoved", + "Enum", "EnumAdded", "EnumRemoved", "Extension", "ExtensionAdded", + "ExtensionCompiled", + "ExtensionParsed", + "ExtensionPlugin", "ExtensionRemoved", "Feature", + "Identity", "IfAndFeatures", "IfFeature", "IfFeatureExpr", @@ -150,6 +180,24 @@ "NodeTypeRemoved", "OrderedByUserAdded", "OrderedByUserRemoved", + "PAction", + "PActionInOut", + "PAnydata", + "PAugment", + "PCase", + "PChoice", + "PContainer", + "PEnum", + "PGrouping", + "PIdentity", + "PLeaf", + "PLeafList", + "PList", + "PNode", + "PNotif", + "PRefine", + "PType", + "PUses", "Pattern", "PatternAdded", "PatternRemoved", @@ -158,6 +206,8 @@ "RangeAdded", "RangeRemoved", "Revision", + "SAnydata", + "SAnyxml", "SCase", "SChoice", "SContainer", diff --git a/libyang/context.py b/libyang/context.py index eefc4e05..d0309d70 100644 --- a/libyang/context.py +++ b/libyang/context.py @@ -4,46 +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, +) + + +# ------------------------------------------------------------------------------------- +@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) + + 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", "__dict__") + __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 @@ -84,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) @@ -110,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, @@ -147,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]) @@ -160,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") @@ -182,7 +400,7 @@ def find_path( self, path: str, output: bool = False, - root_node: Optional["libyang.SNode"] = None, + root_node: Optional[SNode] = None, ) -> Iterator[SNode]: if self.cdata is None: raise RuntimeError("context already destroyed") @@ -212,12 +430,44 @@ def find_path( 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: @@ -234,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]: @@ -245,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( @@ -259,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: @@ -294,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 **") @@ -303,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") @@ -321,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( @@ -332,7 +595,6 @@ 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, @@ -340,16 +602,21 @@ def parse_data( 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, @@ -366,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: @@ -399,7 +652,6 @@ 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, @@ -407,13 +659,15 @@ def parse_data_mem( 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, @@ -421,6 +675,9 @@ def parse_data_mem( 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( @@ -428,7 +685,6 @@ 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, @@ -436,13 +692,15 @@ def parse_data_file( 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, @@ -450,8 +708,119 @@ def parse_data_file( 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 @@ -463,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 f0caf240..230c51af 100644 --- a/libyang/data.py +++ b/libyang/data.py @@ -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,22 +174,10 @@ 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 # ------------------------------------------------------------------------------------- @@ -266,7 +279,7 @@ def __init__(self, context: "libyang.Context", cdata): 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: @@ -278,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: @@ -290,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: @@ -298,14 +311,17 @@ 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: @@ -323,6 +339,7 @@ def add_defaults( no_state: bool = False, output: bool = False, only_node: bool = False, + only_module: Optional[Module] = None, ): flags = implicit_flags( no_config=no_config, @@ -336,7 +353,15 @@ def add_defaults( else: 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_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") @@ -350,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 @@ -365,21 +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") @@ -394,6 +419,16 @@ def insert_sibling(self, node): 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) @@ -488,6 +523,7 @@ def validate( rpc: bool = False, rpcreply: bool = False, notification: bool = False, + dep_tree: Optional["DNode"] = None, ) -> None: dtype = None if rpc: @@ -500,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, @@ -522,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") @@ -960,6 +1000,41 @@ def free(self, with_siblings: bool = True) -> None: finally: 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__ return "<%s.%s: %s>" % (cls.__module__, cls.__name__, str(self)) @@ -1005,10 +1080,14 @@ def _get_path(cdata) -> str: @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]: @@ -1052,38 +1131,30 @@ def cdata_leaf_value(cdata, context: "libyang.Context" = None) -> Any: 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 # ------------------------------------------------------------------------------------- @@ -1132,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 @@ -1158,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 @@ -1179,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: @@ -1211,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 b2a15118..37441f14 100644 --- a/libyang/diff.py +++ b/libyang/diff.py @@ -23,7 +23,7 @@ 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: 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/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 d49c39a7..d3f54654 100644 --- a/libyang/schema.py +++ b/libyang/schema.py @@ -6,7 +6,15 @@ from typing import IO, Any, Dict, Iterator, List, Optional, Tuple, Union from _libyang import ffi, lib -from .util import IOType, LibyangError, c2str, init_output, ly_array_iter, str2c +from .util import ( + IOType, + LibyangError, + c2str, + init_output, + ly_array_iter, + ly_list_iter, + str2c, +) # ------------------------------------------------------------------------------------- @@ -102,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: @@ -144,6 +157,57 @@ def 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() @@ -213,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 @@ -248,6 +313,7 @@ def parse_data_dict( rpc=rpc, rpcreply=rpcreply, notification=notification, + store_only=store_only, ) @@ -300,7 +366,7 @@ class Import: def __init__(self, context: "libyang.Context", cdata, module): self.context = context - self.cdata = cdata # C type: "struct lysp_revision *" + self.cdata = cdata # C type: "struct lysp_import *" self.module = module def name(self) -> str: @@ -344,7 +410,7 @@ def __str__(self): class Extension: __slots__ = ("context", "cdata", "__dict__") - def __init__(self, context: "libyang.Context", cdata, module_parent: Module = None): + def __init__(self, context: "libyang.Context", cdata): self.context = context self.cdata = cdata @@ -372,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) @@ -383,6 +451,32 @@ 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): @@ -400,6 +494,21 @@ 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: @@ -421,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) @@ -454,19 +580,26 @@ class Bit(_EnumBit): # ------------------------------------------------------------------------------------- class Pattern: - __slots__ = ("context", "cdata") + __slots__ = ("context", "cdata", "cdata_parsed") - def __init__(self, context: "libyang.Context", cdata): + 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 @@ -569,6 +702,13 @@ def leafref_path(self) -> Optional["str"]: lr = ffi.cast("struct lysc_type_leafref *", self.cdata) return c2str(lib.lyxp_get_expr(lr.path)) + 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(":") @@ -681,8 +821,20 @@ def all_lengths(self) -> Iterator[str]: yield length def patterns(self) -> Iterator[Tuple[str, bool]]: - for pattern in self.pattern_details(): - yield pattern.expression(), pattern.inverted() + if not self.cdata_parsed or self.cdata.basetype != self.STRING: + return + if self.cdata_parsed.patterns == ffi.NULL: + return + for p in ly_array_iter(self.cdata_parsed.patterns): + if not p: + continue + # in case of pattern restriction, the first byte has a special meaning: + # 0x06 (ACK) for regular match and 0x15 (NACK) for invert-match + invert_match = p.arg.str[0] == b"\x15" + # yield tuples like: + # ('[a-zA-Z_][a-zA-Z0-9\-_.]*', False) + # ('[xX][mM][lL].*', True) + yield c2str(p.arg.str + 1), invert_match def all_patterns(self) -> Iterator[Tuple[str, bool]]: if self.cdata.basetype == lib.LY_TYPE_UNION: @@ -744,6 +896,11 @@ 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 Typedef: @@ -798,6 +955,68 @@ def __str__(self): return self.name() +# ------------------------------------------------------------------------------------- +class Identity: + __slots__ = ("context", "cdata") + + def __init__(self, context: "libyang.Context", cdata): + self.context = context + self.cdata = cdata # C type: "struct lysc_ident *" + + 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 module(self) -> Module: + return Module(self.context, self.cdata.module) + + def derived(self) -> Iterator["Identity"]: + for i in ly_array_iter(self.cdata.derived): + yield Identity(self.context, i) + + 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) + + 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__") @@ -1066,16 +1285,21 @@ def __str__(self): # ------------------------------------------------------------------------------------- class Must: - __slots__ = ("context", "cdata") + __slots__ = ("context", "cdata", "cdata_parsed") - def __init__(self, context: "libyang.Context", cdata): + 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 @@ -1221,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. @@ -1288,21 +1527,35 @@ 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, float]: - if not self.cdata_leaf.dflt: + 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: @@ -1315,7 +1568,11 @@ 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: @@ -1329,7 +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) @@ -1337,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) @@ -1346,35 +1604,57 @@ 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[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 - val = c2str(val) - val_type = Type(self.context, self.cdata_leaflist.dflts[i].realtype, None) - if val_type == Type.BOOL: + continue + val = c2str(dflt.str) + if val_type.base() == Type.BOOL: yield val == "true" - elif val_type in Type.NUM_TYPES: + 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) -> int: + def max_elements(self) -> Optional[int]: return ( - self.cdata_leaflist.max - if self.cdata_leaflist.max != (2**32 - 1) - else None + self.cdata_leaflist.max if self.cdata_leaflist.max != (2**32 - 1) else None ) def min_elements(self) -> int: @@ -1494,7 +1774,7 @@ def uniques(self) -> Iterator[List[SNode]]: nodes.append(SNode.new(self.context, node)) yield nodes - def max_elements(self) -> int: + 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: @@ -1511,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 + ) # ------------------------------------------------------------------------------------- @@ -1542,11 +1826,17 @@ 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, output=True) + yield from iter_children( + self.context, self.cdata, types=types, output=True, with_choice=with_choice + ) # ------------------------------------------------------------------------------------- @@ -1555,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 + ) # ------------------------------------------------------------------------------------- @@ -1635,6 +1929,8 @@ 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: @@ -1665,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 16a9f0ae..cd4638b7 100644 --- a/pylintrc +++ b/pylintrc @@ -74,6 +74,7 @@ disable= too-many-branches, too-many-lines, too-many-locals, + too-many-positional-arguments, too-many-return-statements, too-many-statements, unused-argument, @@ -493,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 diff --git a/tests/test_context.py b/tests/test_context.py index 59839284..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, SLeaf, SLeafList +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") @@ -87,6 +99,22 @@ def test_ctx_find_path(self): 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: ctx.load_module("yolo-system") @@ -111,3 +139,38 @@ 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 10d9045f..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 @@ -21,6 +22,7 @@ DRpc, IOType, LibyangError, + Module, ) from libyang.data import dict_to_dnode @@ -31,7 +33,7 @@ # ------------------------------------------------------------------------------------- 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"), @@ -49,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": [ @@ -74,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) @@ -89,18 +93,18 @@ def test_data_parse_config_json(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": [ @@ -127,10 +131,9 @@ def test_data_parse_config_json_without_yang_lib(self): "hostname": "foo", "url": [ { - "proto": "https", - "host": "github.com", - "path": "/CESNET/libyang-python", - "enabled": false + "proto": "http", + "host": "barfoo.com", + "path": "/barfoo/index.html" }, { "proto": "http", @@ -140,9 +143,10 @@ def test_data_parse_config_json_without_yang_lib(self): "enabled": true }, { - "proto": "http", - "host": "barfoo.com", - "path": "/barfoo/index.html" + "proto": "https", + "host": "github.com", + "path": "/CESNET/libyang-python", + "enabled": false } ], "number": [ @@ -252,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) @@ -282,9 +288,23 @@ def test_data_parse_config_xml_multi_error(self): self.assertEqual( str(cm.exception), 'failed to parse data tree: Invalid boolean value "abcd".: ' - 'List instance is missing its key "host".', + "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> @@ -493,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 @@ -808,7 +829,7 @@ def test_data_to_dict_keyless_list(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> @@ -837,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']", @@ -890,12 +911,12 @@ 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): - JSON = '{"yolo-nodetypes:records": [{"id": "rec1"}]}' + JSON = '{"yolo-nodetypes:records": [{"id": "rec1"}], "yolo-nodetypes:conf": {}}' dnode = self.ctx.parse_data_mem( JSON, "json", validate_present=True, parse_only=True ) @@ -904,12 +925,25 @@ def test_add_defaults(self): self.assertIsInstance(node, DLeaf) node = dnode.find_one("name") self.assertIsNone(node) + 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_path("/yolo-nodetypes:conf/speed") + 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) @@ -951,6 +985,23 @@ def test_dnode_insert_sibling(self): 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( @@ -1034,3 +1085,77 @@ def test_dnode_attrs_set_and_remove_multiple(self): 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 49bf77a2..2263e344 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -12,6 +12,7 @@ EnumRemoved, EnumStatusAdded, EnumStatusRemoved, + ExtensionAdded, NodeTypeAdded, NodeTypeRemoved, SNodeAdded, @@ -82,6 +83,8 @@ class DiffTest(unittest.TestCase): (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"), @@ -94,7 +97,9 @@ class DiffTest(unittest.TestCase): ) 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 64a8e30e..2c6ae1bc 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -7,14 +7,35 @@ 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, @@ -87,6 +108,11 @@ 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") @@ -108,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): @@ -176,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) @@ -195,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) @@ -202,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) @@ -213,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) @@ -241,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")) @@ -287,13 +323,86 @@ def test_iter_tree(self): 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 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)) + + +# ------------------------------------------------------------------------------------- +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[host='%s'][proto='%s']", + "DATA_PATTERN": "/yolo-system:conf/url[proto='%s'][host='%s']", } def setUp(self): @@ -363,6 +472,25 @@ def test_list_min_max(self): 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): @@ -381,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()) @@ -398,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): @@ -485,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( @@ -497,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")) @@ -536,6 +692,28 @@ def test_leaf_type_require_instance(self): 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): @@ -559,6 +737,44 @@ def test_must(self): 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 = {} # ------------------------------------------------------------------------------------- @@ -575,6 +791,15 @@ 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")) @@ -587,6 +812,80 @@ def test_leaf_list_min_max(self): 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): @@ -603,3 +902,117 @@ def test_choice_default(self): 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/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-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 index a456ae1d..9926b1aa 100644 --- a/tests/yang/yolo/yolo-nodetypes.yang +++ b/tests/yang/yolo/yolo-nodetypes.yang @@ -3,6 +3,11 @@ module yolo-nodetypes { namespace "urn:yang:yolo:nodetypes"; prefix sys; + import ietf-inet-types { + prefix inet; + revision-date 2013-07-15; + } + description "YOLO Nodetypes."; @@ -20,6 +25,7 @@ module yolo-nodetypes { type string; default "ASD"; } + ordered-by user; } container conf { @@ -44,6 +50,17 @@ module yolo-nodetypes { 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"; @@ -76,9 +93,93 @@ module yolo-nodetypes { 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; + 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 ef612546..a02f310c 100644 --- a/tests/yang/yolo/yolo-system.yang +++ b/tests/yang/yolo/yolo-system.yang @@ -83,6 +83,7 @@ module yolo-system { type types:protocol { ext:type-desc "<protocol>"; } + ext:parse-validation; } leaf host { type string { @@ -114,6 +115,7 @@ module yolo-system { type string; } } + ext:compile-validation; } } @@ -153,6 +155,10 @@ module yolo-system { } } + ext:compile-validation "module-level" { + ext:compile-validation "module-sub-level"; + } + container conf { description "Configuration."; @@ -189,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 { diff --git a/tox.ini b/tox.ini index 9fd15d15..524ad49c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = format,lint,py{36,37,38,39,310,311,312,py3,3},lydevel,coverage +envlist = format,lint,py{39,310,311,312,313,py3},lydevel,coverage skip_missing_interpreters = true isolated_build = true distdir = {toxinidir}/dist @@ -36,8 +36,8 @@ basepython = python3 description = Format python code using isort and black. changedir = . deps = - black~=23.12.1 - isort~=5.13.2 + black~=25.1.0 + isort~=6.0.0 skip_install = true install_command = python3 -m pip install {opts} {packages} allowlist_externals = @@ -52,14 +52,14 @@ basepython = python3 description = Run coding style checks. changedir = . deps = - astroid~=3.0.2 - black~=23.12.1 - flake8~=7.0.0 - isort~=5.13.2 - pycodestyle~=2.11.1 + 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.0.3 - setuptools~=69.0.3 + pylint~=3.3.4 + setuptools~=75.8.0 allowlist_externals = /bin/sh /usr/bin/sh