From 9e9adf213476fcf0e0f57691bc69e7a0b061ce92 Mon Sep 17 00:00:00 2001 From: Nico Krapp Date: Fri, 20 Oct 2023 14:07:04 +0200 Subject: [PATCH] Add python bindings (#188) * add python bindings --- .github/workflows/python-tests.yml | 30 + bindings/python3/docs/python-libeconf.3 | 822 +++++++++++++++ bindings/python3/econf.py | 968 ++++++++++++++++++ bindings/python3/pyproject.toml | 22 + bindings/python3/test/test_econf.py | 337 ++++++ bindings/python3/test/test_econf_getters.py | 147 +++ .../python3/test/test_econf_getters_def.py | 128 +++ bindings/python3/test/test_econf_setters.py | 116 +++ .../test/testdata/examples/encoding_test.conf | 1 + .../test/testdata/examples/example.conf | 20 + .../examples/example.conf.d/snippet.conf | 5 + .../test/testdata/examples/invalid.conf | 6 + .../test/testdata/examples2/example.conf | 2 + 13 files changed, 2604 insertions(+) create mode 100644 .github/workflows/python-tests.yml create mode 100644 bindings/python3/docs/python-libeconf.3 create mode 100644 bindings/python3/econf.py create mode 100644 bindings/python3/pyproject.toml create mode 100644 bindings/python3/test/test_econf.py create mode 100644 bindings/python3/test/test_econf_getters.py create mode 100644 bindings/python3/test/test_econf_getters_def.py create mode 100644 bindings/python3/test/test_econf_setters.py create mode 100644 bindings/python3/test/testdata/examples/encoding_test.conf create mode 100644 bindings/python3/test/testdata/examples/example.conf create mode 100644 bindings/python3/test/testdata/examples/example.conf.d/snippet.conf create mode 100644 bindings/python3/test/testdata/examples/invalid.conf create mode 100644 bindings/python3/test/testdata/examples2/example.conf diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..a6eac0f --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,30 @@ +name: Execute tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths: + - bindings/python3/* + +jobs: + test: + runs-on: ubuntu-latest + container: registry.opensuse.org/opensuse/tumbleweed:latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup libeconf + run: | + zypper ref + zypper --non-interactive in libeconf0 python3 python3-pip python3-pytest + + - name: Install econf + run: python3 -m pip install -e . --break-system-packages + + - name: Run tests + run: pytest -v test/ \ No newline at end of file diff --git a/bindings/python3/docs/python-libeconf.3 b/bindings/python3/docs/python-libeconf.3 new file mode 100644 index 0000000..64c5341 --- /dev/null +++ b/bindings/python3/docs/python-libeconf.3 @@ -0,0 +1,822 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "PYTHON-LIBECONF" "3" "Oct 20, 2023" "" "python-libeconf" +.sp +\fBpython\-libeconf\fP is a Python Library which offers Python bindings for +\fI\%libeconf\fP\&. +.sp +libeconf is a highly flexible and configurable library to parse and manage key=value configuration files. +It reads configuration file snippets from different directories and builds the final configuration file for +the application from it. +.SH CONTENTS +.SS Usage +.SS install +.sp +\fBWARNING:\fP +.INDENT 0.0 +.INDENT 3.5 +The recommended way to install this project is to get the rpm package. +If you install this project from pypi libeconf will not be automatically installed. +To use this project please make sure you have libeconf installed on your system! +.UNINDENT +.UNINDENT +.sp +You can install this project from pypi with +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +pip install python\-libeconf +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +and then import it into your python project with +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +import econf +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +For information about the functions provided by this library, have a look at \fI\%API\fP +.SS API +.TS +center; +||. +_ +.TE +.SS Functions to interact with config files +.INDENT 0.0 +.TP +.B econf.read_file(file_name: str | bytes, delim: str | bytes, comment: str | bytes) -> EconfFile +Read a config file and write the key\-value pairs into a keyfile object +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBfile_name\fP – absolute path of file to be parsed +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.UNINDENT +.TP +.B Returns +Key\-Value storage object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.read_file_with_callback(file_name: str | bytes, delim: str | bytes, comment: str | bytes, callback: Callable[[any], bool], callback_data: any) -> EconfFile +Read a config file and write the key\-value pairs into a keyfile object +.sp +A user defined function will be called in order e.g. to check the correct file permissions. +If the function returns False the parsing will be aborted and an Exception will be raised +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBfile_name\fP – absolute path of file to be parsed +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.IP \(bu 2 +\fBcallback\fP – User defined function which will be called and returns a boolean +.IP \(bu 2 +\fBcallback_data\fP – argument to be give to the callback function +.UNINDENT +.TP +.B Returns +Key\-Value storage object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.new_key_file(delim: str | bytes, comment: str | bytes) -> EconfFile +Create a new empty keyfile +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.UNINDENT +.TP +.B Returns +created EconfFile object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.new_ini_file() -> EconfFile +Create a new empty keyfile with delimiter ‘=’ and comment ‘#’ +.INDENT 7.0 +.TP +.B Returns +created EconfFile object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.merge_files(usr_file: EconfFile, etc_file: EconfFile) -> EconfFile +Merge the content of 2 keyfile objects +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBusr_file\fP – first EconfFile object +.IP \(bu 2 +\fBetc_file\fP – second EconfFile object +.UNINDENT +.TP +.B Returns +merged EconfFile object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.read_dirs(usr_conf_dir: str | bytes, etc_conf_dir: str | bytes, project_name: str | bytes, config_suffix: str | bytes, delim: str | bytes, comment: str | bytes) -> EconfFile +Read configuration from the first found config file and merge with snippets from conf.d/ directory +.sp +e.g. searches /usr/etc/ and /etc/ for an example.conf file and merges it with the snippets in either +/usr/etc/example.conf.d/ or /etc/example.conf.d +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBusr_conf_dir\fP – absolute path of the first directory to be searched +.IP \(bu 2 +\fBetc_conf_dir\fP – absolute path of the second directory to be searched +.IP \(bu 2 +\fBproject_name\fP – basename of the configuration file +.IP \(bu 2 +\fBconfig_suffix\fP – suffix of the configuration file +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.UNINDENT +.TP +.B Returns +merged EconfFile object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.read_dirs_with_callback(usr_conf_dir: str | bytes, etc_conf_dir: str | bytes, project_name: str | bytes, config_suffix: str | bytes, delim: str | bytes, comment: str | bytes, callback: Callable[[any], bool], callback_data: any) -> EconfFile +Read configuration from the first found config file and merge with snippets from conf.d/ directory +.sp +For every file a user defined function will be called in order e.g. to check the correct file permissions. +If the function returns False the parsing will be aborted and an Exception will be raised +.sp +e.g. searches /usr/etc/ and /etc/ for an example.conf file and merges it with the snippets in either +/usr/etc/example.conf.d/ or /etc/example.conf.d +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBusr_conf_dir\fP – absolute path of the first directory to be searched +.IP \(bu 2 +\fBetc_conf_dir\fP – absolute path of the second directory to be searched +.IP \(bu 2 +\fBproject_name\fP – basename of the configuration file +.IP \(bu 2 +\fBconfig_suffix\fP – suffix of the configuration file +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.IP \(bu 2 +\fBcallback\fP – User defined function which will be called for each file and returns a boolean +.IP \(bu 2 +\fBcallback_data\fP – argument to be give to the callback function +.UNINDENT +.TP +.B Returns +merged EconfFile object +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.read_dirs_history(usr_conf_dir: str | bytes, etc_conf_dir: str | bytes, project_name: str | bytes, config_suffix: str | bytes, delim: str | bytes, comment: str | bytes) -> list[EconfFile] +Read configuration from the first found config file and snippets from conf.d/ directory +.sp +e.g. searches /usr/etc/ and /etc/ for an example.conf file and the snippets in either +/usr/etc/example.conf.d/ or /etc/example.conf.d +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBusr_conf_dir\fP – absolute path of the first directory to be searched +.IP \(bu 2 +\fBetc_conf_dir\fP – absolute path of the second directory to be searched +.IP \(bu 2 +\fBproject_name\fP – basename of the configuration file +.IP \(bu 2 +\fBconfig_suffix\fP – suffix of the configuration file +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.UNINDENT +.TP +.B Returns +list of EconfFile objects +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.read_dirs_history_with_callback(usr_conf_dir: str | bytes, etc_conf_dir: str | bytes, project_name: str | bytes, config_suffix: str | bytes, delim: str | bytes, comment: str | bytes, callback: Callable[[any], bool], callback_data: any) -> EconfFile +Read configuration from the first found config file and snippets from conf.d/ directory +.sp +For every file a user defined function will be called in order e.g. to check the correct file permissions. +If the function returns False the parsing will be aborted and an Exception will be raised +.sp +e.g. searches /usr/etc/ and /etc/ for an example.conf file and the snippets in either +/usr/etc/example.conf.d/ or /etc/example.conf.d +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBusr_conf_dir\fP – absolute path of the first directory to be searched +.IP \(bu 2 +\fBetc_conf_dir\fP – absolute path of the second directory to be searched +.IP \(bu 2 +\fBproject_name\fP – basename of the configuration file +.IP \(bu 2 +\fBconfig_suffix\fP – suffix of the configuration file +.IP \(bu 2 +\fBdelim\fP – delimiter of a key/value e.g. ‘=’ +.IP \(bu 2 +\fBcomment\fP – string that defines the start of a comment e.g. ‘#’ +.IP \(bu 2 +\fBcallback\fP – User defined function which will be called for each file and returns a boolean +.IP \(bu 2 +\fBcallback_data\fP – argument to be give to the callback function +.UNINDENT +.TP +.B Returns +list of EconfFile objects +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.comment_tag(ef: EconfFile) -> str +Get the comment tag of the specified EconfFile +.INDENT 7.0 +.TP +.B Parameters +\fBef\fP – Key\-Value storage object +.TP +.B Returns +The comment tag of the EconfFile +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_comment_tag(ef: EconfFile, comment: str | bytes) -> None +Set the comment tag of the specified EconfFile +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBcomment\fP – The desired comment tag character +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.delimiter_tag(ef: EconfFile) -> str +Get the delimiter tag of the specified EconfFile +.INDENT 7.0 +.TP +.B Parameters +\fBef\fP – Key\-Value storage object +.TP +.B Returns +the delimiter tag of the EconfFile +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_delimiter_tag(ef: EconfFile, delimiter: str | bytes) -> None +Set the delimiter tag of the specified EconfFile +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBdelimiter\fP – The desired delimiter character +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.write_file(ef: EconfFile, save_to_dir: str, file_name: str) -> None +Write content of a keyfile to specified location +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBsave_to_dir\fP – directory into which the file has to be written +.IP \(bu 2 +\fBfile_name\fP – filename with suffix of the to be written file +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_path(ef: EconfFile) -> str +Get the path of the source of the given key file +.INDENT 7.0 +.TP +.B Parameters +\fBef\fP – Key\-Value storage object +.TP +.B Returns +path of the config file as string +.UNINDENT +.UNINDENT +.SS Functions for getting values +.INDENT 0.0 +.TP +.B econf.get_groups(ef: EconfFile) -> list[str] +List all the groups of given keyfile +.INDENT 7.0 +.TP +.B Parameters +\fBef\fP – Key\-Value storage object +.TP +.B Returns +list of groups in the keyfile +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_keys(ef: EconfFile, group: str) -> list[str] +List all the keys of a given group or all keys in a keyfile +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – group of the keys to be returned or None for keys without a group +.UNINDENT +.TP +.B Returns +list of keys in the given group +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_int_value(ef: EconfFile, group: str, key: str) -> int +Return an integer value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_uint_value(ef: EconfFile, group: str, key: str) -> int +Return an unsigned integer value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_float_value(ef: EconfFile, group: str, key: str) -> float +Return a float value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_string_value(ef: EconfFile, group: str, key: str) -> str +Return a string value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_bool_value(ef: EconfFile, group: str, key: str) -> bool +Return a boolean value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.SS Functions for getting values with defaults +.INDENT 0.0 +.TP +.B econf.get_int_value_def(ef: EconfFile, group: str, key: str, default: int) -> int +Return an integer value for given group/key or return a default value if key is not found +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBdefault\fP – value to be returned if no key is found +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_uint_value_def(ef: EconfFile, group: str, key: str, default: int) -> int +Return an unsigned integer value for given group/key or return a default value if key is not found +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBdefault\fP – value to be returned if no key is found +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_float_value_def(ef: EconfFile, group: str, key: str, default: float) -> float +Return a float value for given group/key or return a default value if key is not found +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBdefault\fP – value to be returned if no key is found +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_string_value_def(ef: EconfFile, group: str, key: str, default: str) -> str +Return a string value for given group/key or return a default value if key is not found +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBdefault\fP – value to be returned if no key is found +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.get_bool_value_def(ef: EconfFile, group: str, key: str, default: bool) -> bool +Return a boolean value for given group/key or return a default value if key is not found +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBdefault\fP – value to be returned if no key is found +.UNINDENT +.TP +.B Returns +value of the key +.UNINDENT +.UNINDENT +.SS Functions for setting values +.INDENT 0.0 +.TP +.B econf.set_value(ef: EconfFile, group: str | bytes, key: str | bytes, value: int | float | str | bool) -> None +Dynamically set a value in a keyfile and returns a status code +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – EconfFile object to set value in +.IP \(bu 2 +\fBgroup\fP – group of the key to be changed +.IP \(bu 2 +\fBkey\fP – key to be changed +.IP \(bu 2 +\fBvalue\fP – desired value +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_int_value(ef: EconfFile, group: str, key: str, value: int) -> None +Setting an integer value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBvalue\fP – value to be set for given key +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_uint_value(ef: EconfFile, group: str, key: str, value: int) -> None +Setting an unsigned integer value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBvalue\fP – value to be set for given key +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_float_value(ef: EconfFile, group: str, key: str, value: float) -> None +Setting a float value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBvalue\fP – value to be set for given key +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_string_value(ef: EconfFile, group: str, key: str, value: str | bytes) -> None +Setting a string value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBvalue\fP – value to be set for given key +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.set_bool_value(ef: EconfFile, group: str, key: str, value: bool) -> None +Setting a boolean value for given group/key +.INDENT 7.0 +.TP +.B Parameters +.INDENT 7.0 +.IP \(bu 2 +\fBef\fP – Key\-Value storage object +.IP \(bu 2 +\fBgroup\fP – desired group +.IP \(bu 2 +\fBkey\fP – key of the value that is requested +.IP \(bu 2 +\fBvalue\fP – value to be set for given key +.UNINDENT +.TP +.B Returns +Nothing +.UNINDENT +.UNINDENT +.SS Functions for memory management +.INDENT 0.0 +.TP +.B econf.free_file(ef: EconfFile) +Free the memory of a given keyfile +.sp +This function is called automatically at the end of every objects lifetime and should not be used otherwise +.INDENT 7.0 +.TP +.B Parameters +\fBef\fP – EconfFile to be freed +.TP +.B Returns +None +.UNINDENT +.UNINDENT +.SS Functions for handling error codes +.INDENT 0.0 +.TP +.B econf.err_string(error: int) -> str +Convert an error code into error message +.INDENT 7.0 +.TP +.B Parameters +\fBerror\fP – error code as integer +.TP +.B Returns +error string +.UNINDENT +.UNINDENT +.INDENT 0.0 +.TP +.B econf.err_location() -> Tuple[str, int] +Info about the line where an error happened +.INDENT 7.0 +.TP +.B Returns +path to the last handled file and number of last handled line +.UNINDENT +.UNINDENT +.SH AUTHOR +Nico Krapp +.SH COPYRIGHT +2023, Nico Krapp +.\" Generated by docutils manpage writer. +. diff --git a/bindings/python3/econf.py b/bindings/python3/econf.py new file mode 100644 index 0000000..fba92b6 --- /dev/null +++ b/bindings/python3/econf.py @@ -0,0 +1,968 @@ +""" +Econf provides functionality for interacting with Key-Value config files, like getting and setting values for read config files. + +For more information please have a look at the API +""" +import ctypes.util +from enum import Enum +from dataclasses import dataclass +from typing import * +from ctypes import * + +LIBNAME = ctypes.util.find_library("econf") +LIBECONF = CDLL(LIBNAME) + + +@dataclass +class EconfFile: + """ + Class which points to the Key Value storage object + """ + + _ptr: c_void_p + + def __del__(self): + free_file(self) + + +class EconfError(Enum): + SUCCESS = 0 + ERROR = 1 + NOMEM = 2 + NOFILE = 3 + NOGROUP = 4 + NOKEY = 5 + EMPTYKEY = 6 + WRITEERROR = 7 + PARSE_ERROR = 8 + MISSING_BRACKET = 9 + MISSING_DELIMITER = 10 + EMPTY_SECTION_NAME = 11 + TEXT_AFTER_SECTION = 12 + FILE_LIST_IS_NULL = 13 + WRONG_BOOLEAN_VALUE = 14 + KEY_HAS_NULL_VALUE = 15 + WRONG_OWNER = 16 + WRONG_GROUP = 17 + WRONG_FILE_PERMISSION = 18 + WRONG_DIR_PERMISSION = 19 + ERROR_FILE_IS_SYM_LINK = 20 + PARSING_CALLBACK_FAILED = 21 + + +ECONF_EXCEPTION = { + EconfError.ERROR: Exception, + EconfError.NOMEM: MemoryError, + EconfError.NOFILE: FileNotFoundError, + EconfError.NOGROUP: KeyError, + EconfError.NOKEY: KeyError, + EconfError.EMPTYKEY: KeyError, + EconfError.WRITEERROR: OSError, + EconfError.PARSE_ERROR: Exception, + EconfError.MISSING_BRACKET: SyntaxError, + EconfError.MISSING_DELIMITER: SyntaxError, + EconfError.EMPTY_SECTION_NAME: SyntaxError, + EconfError.TEXT_AFTER_SECTION: SyntaxError, + EconfError.FILE_LIST_IS_NULL: ValueError, + EconfError.WRONG_BOOLEAN_VALUE: ValueError, + EconfError.KEY_HAS_NULL_VALUE: ValueError, + EconfError.WRONG_OWNER: PermissionError, + EconfError.WRONG_GROUP: PermissionError, + EconfError.WRONG_FILE_PERMISSION: PermissionError, + EconfError.WRONG_DIR_PERMISSION: PermissionError, + EconfError.ERROR_FILE_IS_SYM_LINK: PermissionError, + EconfError.PARSING_CALLBACK_FAILED: Exception +} + + +def _encode_str(string: str | bytes) -> bytes: + if isinstance(string, str): + string = string.encode("utf-8") + elif not isinstance(string, bytes): + raise TypeError("Input must be a string or bytes") + return string + + +def _ensure_valid_char(char: str | bytes) -> bytes: + char = _encode_str(char) + if len(char) > 1: + raise ValueError("Only single characters are allowed as comment and delimiter") + return char + + +def _ensure_valid_int(val: int) -> int: + if isinstance(val, int): + c_val = c_int64(val) + if not c_val.value == val: + raise ValueError( + "Integer overflow found, only up to 64 bit signed integers are supported" + ) + return c_val + else: + raise TypeError(f"parameter {val} is not an integer") + + +def _ensure_valid_uint(val: int) -> int: + if isinstance(val, int) and (val >= 0): + c_val = c_uint64(val) + if not c_val.value == val: + raise ValueError( + "Integer overflow found, only up to 64 bit unsigned integers are supported" + ) + return c_val + else: + raise TypeError(f"parameter {val} is not an unsigned integer") + + +def set_value( + ef: EconfFile, group: str | bytes, key: str | bytes, value: int | float | str | bool +) -> None: + """ + Dynamically set a value in a keyfile and returns a status code + + :param ef: EconfFile object to set value in + :param group: group of the key to be changed + :param key: key to be changed + :param value: desired value + :return: Nothing + """ + if isinstance(value, int): + if value >= 0: + set_uint_value(ef, group, key, value) + else: + set_int_value(ef, group, key, value) + elif isinstance(value, float): + set_float_value(ef, group, key, value) + elif isinstance(value, str) | isinstance(value, bytes): + set_string_value(ef, group, key, value) + elif isinstance(value, bool): + set_bool_value(ef, group, key, value) + else: + raise TypeError(f"parameter {val} is not one of the supported types") + + +def read_file( + file_name: str | bytes, delim: str | bytes, comment: str | bytes +) -> EconfFile: + """ + Read a config file and write the key-value pairs into a keyfile object + + :param file_name: absolute path of file to be parsed + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :return: Key-Value storage object + """ + result = EconfFile(c_void_p(None)) + file_name = _encode_str(file_name) + delim = _ensure_valid_char(delim) + comment = _ensure_valid_char(comment) + err = LIBECONF.econf_readFile(byref(result._ptr), file_name, delim, comment) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"read_file failed with error: {err_string(err)}") + return result + + +def read_file_with_callback( + file_name: str | bytes, + delim: str | bytes, + comment: str | bytes, + callback: Callable[[any], bool], + callback_data: any, +) -> EconfFile: + """ + Read a config file and write the key-value pairs into a keyfile object + + A user defined function will be called in order e.g. to check the correct file permissions. + If the function returns False the parsing will be aborted and an Exception will be raised + + :param file_name: absolute path of file to be parsed + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :param callback: User defined function which will be called and returns a boolean + :param callback_data: argument to be give to the callback function + :return: Key-Value storage object + """ + result = EconfFile(c_void_p(None)) + file_name = _encode_str(file_name) + delim = _ensure_valid_char(delim) + comment = _ensure_valid_char(comment) + + def callback_proxy(fake_data: c_void_p) -> c_bool: + return callback(callback_data) + + CBFUNC = CFUNCTYPE(c_bool, c_void_p) + cb_func = CBFUNC(callback_proxy) + + err = LIBECONF.econf_readFileWithCallback( + byref(result._ptr), file_name, delim, comment, cb_func, c_void_p(None) + ) + if err: + raise ECONF_EXCEPTION[EconfError(err)]( + f"read_file_with_callback failed with error: {err_string(err)}" + ) + return result + + +def merge_files(usr_file: EconfFile, etc_file: EconfFile) -> EconfFile: + """ + Merge the content of 2 keyfile objects + + :param usr_file: first EconfFile object + :param etc_file: second EconfFile object + :return: merged EconfFile object + """ + merged_file = EconfFile(c_void_p()) + err = LIBECONF.econf_mergeFiles( + byref(merged_file._ptr), + usr_file._ptr, + etc_file._ptr, + ) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"merge_files failed with error: {err_string(err)}") + return merged_file + + +def read_dirs( + usr_conf_dir: str | bytes, + etc_conf_dir: str | bytes, + project_name: str | bytes, + config_suffix: str | bytes, + delim: str | bytes, + comment: str | bytes, +) -> EconfFile: + """ + Read configuration from the first found config file and merge with snippets from conf.d/ directory + + e.g. searches /usr/etc/ and /etc/ for an example.conf file and merges it with the snippets in either + /usr/etc/example.conf.d/ or /etc/example.conf.d + + :param usr_conf_dir: absolute path of the first directory to be searched + :param etc_conf_dir: absolute path of the second directory to be searched + :param project_name: basename of the configuration file + :param config_suffix: suffix of the configuration file + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :return: merged EconfFile object + """ + result = EconfFile(c_void_p()) + usr_conf_dir = _encode_str(usr_conf_dir) + etc_conf_dir = _encode_str(etc_conf_dir) + project_name = _encode_str(project_name) + config_suffix = _encode_str(config_suffix) + delim = _ensure_valid_char(delim) + comment = _ensure_valid_char(comment) + err = LIBECONF.econf_readDirs( + byref(result._ptr), + usr_conf_dir, + etc_conf_dir, + project_name, + config_suffix, + delim, + comment, + ) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"read_dirs failed with error: {err_string(err)}") + return result + + +def read_dirs_with_callback( + usr_conf_dir: str | bytes, + etc_conf_dir: str | bytes, + project_name: str | bytes, + config_suffix: str | bytes, + delim: str | bytes, + comment: str | bytes, + callback: Callable[[any], bool], + callback_data: any, +) -> EconfFile: + """ + Read configuration from the first found config file and merge with snippets from conf.d/ directory + + For every file a user defined function will be called in order e.g. to check the correct file permissions. + If the function returns False the parsing will be aborted and an Exception will be raised + + e.g. searches /usr/etc/ and /etc/ for an example.conf file and merges it with the snippets in either + /usr/etc/example.conf.d/ or /etc/example.conf.d + + :param usr_conf_dir: absolute path of the first directory to be searched + :param etc_conf_dir: absolute path of the second directory to be searched + :param project_name: basename of the configuration file + :param config_suffix: suffix of the configuration file + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :param callback: User defined function which will be called for each file and returns a boolean + :param callback_data: argument to be give to the callback function + :return: merged EconfFile object + """ + result = EconfFile(c_void_p()) + usr_conf_dir = _encode_str(usr_conf_dir) + etc_conf_dir = _encode_str(etc_conf_dir) + project_name = _encode_str(project_name) + config_suffix = _encode_str(config_suffix) + delim = _ensure_valid_char(delim) + comment = _ensure_valid_char(comment) + + def callback_proxy(fake_data: c_void_p): + return callback(callback_data) + + CBFUNC = CFUNCTYPE(c_bool, c_void_p) + cb_func = CBFUNC(callback_proxy) + + err = LIBECONF.econf_readDirsWithCallback( + byref(result._ptr), + usr_conf_dir, + etc_conf_dir, + project_name, + config_suffix, + delim, + comment, + cb_func, + c_void_p(None), + ) + if err: + raise ECONF_EXCEPTION[EconfError(err)]( + f"read_dirs_with_callback failed with error: {err_string(err)}" + ) + return result + + +def read_dirs_history( + usr_conf_dir: str | bytes, + etc_conf_dir: str | bytes, + project_name: str | bytes, + config_suffix: str | bytes, + delim: str | bytes, + comment: str | bytes, +) -> list[EconfFile]: + """ + Read configuration from the first found config file and snippets from conf.d/ directory + + e.g. searches /usr/etc/ and /etc/ for an example.conf file and the snippets in either + /usr/etc/example.conf.d/ or /etc/example.conf.d + + :param usr_conf_dir: absolute path of the first directory to be searched + :param etc_conf_dir: absolute path of the second directory to be searched + :param project_name: basename of the configuration file + :param config_suffix: suffix of the configuration file + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :return: list of EconfFile objects + """ + key_files = c_void_p(None) + size = c_size_t() + usr_conf_dir = _encode_str(usr_conf_dir) + etc_conf_dir = _encode_str(etc_conf_dir) + project_name = _encode_str(project_name) + config_suffix = _encode_str(config_suffix) + delim = _ensure_valid_char(delim) + comment = _ensure_valid_char(comment) + err = LIBECONF.econf_readDirsHistory( + byref(key_files), + byref(size), + usr_conf_dir, + etc_conf_dir, + project_name, + config_suffix, + delim, + comment, + ) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"read_dirs_history failed with error: {err_string(err)}") + + arr = cast(key_files, POINTER(c_void_p * size.value)) + result = [EconfFile(c_void_p(i)) for i in arr.contents] + return result + + +def read_dirs_history_with_callback( + usr_conf_dir: str | bytes, + etc_conf_dir: str | bytes, + project_name: str | bytes, + config_suffix: str | bytes, + delim: str | bytes, + comment: str | bytes, + callback: Callable[[any], bool], + callback_data: any, +) -> EconfFile: + """ + Read configuration from the first found config file and snippets from conf.d/ directory + + For every file a user defined function will be called in order e.g. to check the correct file permissions. + If the function returns False the parsing will be aborted and an Exception will be raised + + e.g. searches /usr/etc/ and /etc/ for an example.conf file and the snippets in either + /usr/etc/example.conf.d/ or /etc/example.conf.d + + :param usr_conf_dir: absolute path of the first directory to be searched + :param etc_conf_dir: absolute path of the second directory to be searched + :param project_name: basename of the configuration file + :param config_suffix: suffix of the configuration file + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :param callback: User defined function which will be called for each file and returns a boolean + :param callback_data: argument to be give to the callback function + :return: list of EconfFile objects + """ + key_files = c_void_p(None) + size = c_size_t() + usr_conf_dir = _encode_str(usr_conf_dir) + etc_conf_dir = _encode_str(etc_conf_dir) + project_name = _encode_str(project_name) + config_suffix = _encode_str(config_suffix) + delim = _ensure_valid_char(delim) + comment = _ensure_valid_char(comment) + + def callback_proxy(fake_data: c_void_p): + return callback(callback_data) + + CBFUNC = CFUNCTYPE(c_bool, c_void_p) + cb_func = CBFUNC(callback_proxy) + + err = LIBECONF.econf_readDirsHistoryWithCallback( + byref(key_files), + byref(size), + usr_conf_dir, + etc_conf_dir, + project_name, + config_suffix, + delim, + comment, + cb_func, + c_void_p(None), + ) + if err: + raise ECONF_EXCEPTION[EconfError(err)]( + f"read_dirs_history_with_callback failed with error: {err_string(err)}" + ) + + arr = cast(key_files, POINTER(c_void_p * size.value)) + result = [EconfFile(c_void_p(i)) for i in arr.contents] + return result + + +def new_key_file(delim: str | bytes, comment: str | bytes) -> EconfFile: + """ + Create a new empty keyfile + + :param delim: delimiter of a key/value e.g. '=' + :param comment: string that defines the start of a comment e.g. '#' + :return: created EconfFile object + """ + result = EconfFile(c_void_p()) + delim = c_char(_ensure_valid_char(delim)) + comment = c_char(_ensure_valid_char(comment)) + err = LIBECONF.econf_newKeyFile(byref(result._ptr), delim, comment) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"new_key_file failed with error: {err_string(err)}") + return result + + +def new_ini_file() -> EconfFile: + """ + Create a new empty keyfile with delimiter '=' and comment '#' + + :return: created EconfFile object + """ + result = EconfFile(c_void_p()) + err = LIBECONF.econf_newIniFile(byref(result._ptr)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"new_ini_file failed with error: {err_string(err)}") + return result + + +def comment_tag(ef: EconfFile) -> str: + """ + Get the comment tag of the specified EconfFile + + :param ef: Key-Value storage object + :return: The comment tag of the EconfFile + """ + LIBECONF.econf_comment_tag.restype = c_char + result = LIBECONF.econf_comment_tag(ef._ptr) + return result.decode("utf-8") + + +def delimiter_tag(ef: EconfFile) -> str: + """ + Get the delimiter tag of the specified EconfFile + + :param ef: Key-Value storage object + :return: the delimiter tag of the EconfFile + """ + LIBECONF.econf_delimiter_tag.restype = c_char + result = LIBECONF.econf_delimiter_tag(ef._ptr) + return result.decode("utf-8") + + +def set_comment_tag(ef: EconfFile, comment: str | bytes) -> None: + """ + Set the comment tag of the specified EconfFile + + :param ef: Key-Value storage object + :param comment: The desired comment tag character + :return: Nothing + """ + comment = _ensure_valid_char(comment) + c_comment = c_char(comment) + LIBECONF.econf_set_comment_tag(ef._ptr, c_comment) + + +def set_delimiter_tag(ef: EconfFile, delimiter: str | bytes) -> None: + """ + Set the delimiter tag of the specified EconfFile + + :param ef: Key-Value storage object + :param delimiter: The desired delimiter character + :return: Nothing + """ + delimiter = _ensure_valid_char(delimiter) + c_delimiter = c_char(delimiter) + LIBECONF.econf_set_delimiter_tag(ef._ptr, c_delimiter) + + +def write_file(ef: EconfFile, save_to_dir: str, file_name: str) -> None: + """ + Write content of a keyfile to specified location + + :param ef: Key-Value storage object + :param save_to_dir: directory into which the file has to be written + :param file_name: filename with suffix of the to be written file + :return: Nothing + """ + c_save_to_dir = _encode_str(save_to_dir) + c_file_name = _encode_str(file_name) + err = LIBECONF.econf_writeFile(byref(ef._ptr), c_save_to_dir, c_file_name) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"write_file failed with error: {err_string(err)}") + + +def get_path(ef: EconfFile) -> str: + """ + Get the path of the source of the given key file + + :param ef: Key-Value storage object + :return: path of the config file as string + """ + # extract from pointer + LIBECONF.econf_getPath.restype = c_char_p + return LIBECONF.econf_getPath(ef._ptr).decode("utf-8") + + +def get_groups(ef: EconfFile) -> list[str]: + """ + List all the groups of given keyfile + + :param ef: Key-Value storage object + :return: list of groups in the keyfile + """ + c_length = c_size_t() + c_groups = c_void_p(None) + err = LIBECONF.econf_getGroups(ef._ptr, byref(c_length), byref(c_groups)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_groups failed with error: {err_string(err)}") + arr = cast(c_groups, POINTER(c_char_p * c_length.value)) + result = [i.decode("utf-8") for i in arr.contents] + return result + + +def get_keys(ef: EconfFile, group: str) -> list[str]: + """ + List all the keys of a given group or all keys in a keyfile + + :param ef: Key-Value storage object + :param group: group of the keys to be returned or None for keys without a group + :return: list of keys in the given group + """ + c_length = c_size_t() + c_keys = c_void_p(None) + if group: + group = _encode_str(group) + err = LIBECONF.econf_getKeys(ef._ptr, group, byref(c_length), byref(c_keys)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_keys failed with error: {err_string(err)}") + arr = cast(c_keys, POINTER(c_char_p * c_length.value)) + result = [i.decode("utf-8") for i in arr.contents] + return result + + +def get_int_value(ef: EconfFile, group: str, key: str) -> int: + """ + Return an integer value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_int64() + err = LIBECONF.econf_getInt64Value(ef._ptr, group, c_key, byref(c_result)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_int64_value failed with error: {err_string(err)}") + return c_result.value + + +def get_uint_value(ef: EconfFile, group: str, key: str) -> int: + """ + Return an unsigned integer value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_uint64() + err = LIBECONF.econf_getUInt64Value(ef._ptr, group, c_key, byref(c_result)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_uint64_value failed with error: {err_string(err)}") + return c_result.value + + +def get_float_value(ef: EconfFile, group: str, key: str) -> float: + """ + Return a float value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_double() + err = LIBECONF.econf_getDoubleValue(ef._ptr, group, c_key, byref(c_result)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_double_value failed with error: {err_string(err)}") + return c_result.value + + +def get_string_value(ef: EconfFile, group: str, key: str) -> str: + """ + Return a string value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_char_p() + err = LIBECONF.econf_getStringValue(ef._ptr, group, c_key, byref(c_result)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_string_value failed with error: {err_string(err)}") + return c_result.value.decode("utf-8") + + +def get_bool_value(ef: EconfFile, group: str, key: str) -> bool: + """ + Return a boolean value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_bool() + err = LIBECONF.econf_getBoolValue(ef._ptr, group, c_key, byref(c_result)) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"get_bool_value failed with error: {err_string(err)}") + return c_result.value + + +def get_int_value_def(ef: EconfFile, group: str, key: str, default: int) -> int: + """ + Return an integer value for given group/key or return a default value if key is not found + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param default: value to be returned if no key is found + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_int64() + c_default = _ensure_valid_int(default) + err = LIBECONF.econf_getInt64ValueDef( + ef._ptr, group, c_key, byref(c_result), c_default + ) + if err and EconfError(err) != EconfError.NOKEY: + raise ECONF_EXCEPTION[EconfError(err)](f"get_int64_value_def failed with error: {err_string(err)}") + return c_result.value + + +def get_uint_value_def(ef: EconfFile, group: str, key: str, default: int) -> int: + """ + Return an unsigned integer value for given group/key or return a default value if key is not found + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param default: value to be returned if no key is found + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_uint64() + c_default = _ensure_valid_uint(default) + err = LIBECONF.econf_getUInt64ValueDef( + ef._ptr, group, c_key, byref(c_result), c_default + ) + if err and EconfError(err) != EconfError.NOKEY: + raise ECONF_EXCEPTION[EconfError(err)](f"get_uint64_value_def failed with error: {err_string(err)}") + return c_result.value + + +def get_float_value_def(ef: EconfFile, group: str, key: str, default: float) -> float: + """ + Return a float value for given group/key or return a default value if key is not found + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param default: value to be returned if no key is found + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_double() + if not isinstance(default, float): + raise TypeError('"default" parameter must be of type float') + c_default = c_double(default) + err = LIBECONF.econf_getDoubleValueDef( + ef._ptr, group, c_key, byref(c_result), c_default + ) + if err and EconfError(err) != EconfError.NOKEY: + raise ECONF_EXCEPTION[EconfError(err)](f"get_double_value_def failed with error: {err_string(err)}") + return c_result.value + + +def get_string_value_def(ef: EconfFile, group: str, key: str, default: str) -> str: + """ + Return a string value for given group/key or return a default value if key is not found + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param default: value to be returned if no key is found + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_char_p() + c_default = _encode_str(default) + err = LIBECONF.econf_getStringValueDef( + ef._ptr, group, c_key, byref(c_result), c_default + ) + if err: + if EconfError(err) == EconfError.NOKEY: + return c_default.decode("utf-8") + raise ECONF_EXCEPTION[EconfError(err)](f"get_string_value_def failed with error: {err_string(err)}") + return c_result.value.decode("utf-8") + + +def get_bool_value_def(ef: EconfFile, group: str, key: str, default: bool) -> bool: + """ + Return a boolean value for given group/key or return a default value if key is not found + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param default: value to be returned if no key is found + :return: value of the key + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_result = c_bool() + if not isinstance(default, bool): + raise TypeError('"value" parameter must be of type bool') + c_default = c_bool(default) + err = LIBECONF.econf_getBoolValueDef( + ef._ptr, group, c_key, byref(c_result), c_default + ) + if err and EconfError(err) != EconfError.NOKEY: + raise ECONF_EXCEPTION[EconfError(err)](f"get_bool_value_def failed with error: {err_string(err)}") + return c_result.value + + +def set_int_value(ef: EconfFile, group: str, key: str, value: int) -> None: + """ + Setting an integer value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param value: value to be set for given key + :return: Nothing + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_value = _ensure_valid_int(value) + err = LIBECONF.econf_setInt64Value(ef._ptr, group, c_key, c_value) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"set_int64_value failed with error: {err_string(err)}") + + +def set_uint_value(ef: EconfFile, group: str, key: str, value: int) -> None: + """ + Setting an unsigned integer value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param value: value to be set for given key + :return: Nothing + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_value = _ensure_valid_uint(value) + err = LIBECONF.econf_setUInt64Value(ef._ptr, group, c_key, c_value) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"set_uint64_value failed with error: {err_string(err)}") + + +def set_float_value(ef: EconfFile, group: str, key: str, value: float) -> None: + """ + Setting a float value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param value: value to be set for given key + :return: Nothing + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + if not isinstance(value, float): + raise TypeError('"value" parameter must be of type float') + c_value = c_double(value) + err = LIBECONF.econf_setDoubleValue(ef._ptr, group, c_key, c_value) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"set_double_value failed with error: {err_string(err)}") + + +def set_string_value(ef: EconfFile, group: str, key: str, value: str | bytes) -> None: + """ + Setting a string value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param value: value to be set for given key + :return: Nothing + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + c_value = _encode_str(value) + err = LIBECONF.econf_setStringValue(ef._ptr, group, c_key, c_value) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"set_string_value failed with error: {err_string(err)}") + + +def set_bool_value(ef: EconfFile, group: str, key: str, value: bool) -> None: + """ + Setting a boolean value for given group/key + + :param ef: Key-Value storage object + :param group: desired group + :param key: key of the value that is requested + :param value: value to be set for given key + :return: Nothing + """ + if group: + group = _encode_str(group) + c_key = _encode_str(key) + if not isinstance(value, bool): + raise TypeError('"value" parameter must be of type bool') + c_value = _encode_str(str(value)) + err = LIBECONF.econf_setBoolValue(ef._ptr, group, c_key, c_value) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"set_bool_value failed with error: {err_string(err)}") + + +def err_string(error: int) -> str: + """ + Convert an error code into error message + + :param error: error code as integer + :return: error string + """ + if not isinstance(error, int): + raise TypeError("Error codes must be of type int") + c_int(error) + LIBECONF.econf_errString.restype = c_char_p + return LIBECONF.econf_errString(error).decode("utf-8") + + +def err_location() -> Tuple[str, int]: + """ + Info about the line where an error happened + + :return: path to the last handled file and number of last handled line + """ + c_filename = c_char_p() + c_line_nr = c_uint64() + LIBECONF.econf_errLocation(byref(c_filename), byref(c_line_nr)) + return c_filename.value.decode("utf-8"), c_line_nr.value + + +def free_file(ef: EconfFile): + """ + Free the memory of a given keyfile + + This function is called automatically at the end of every objects lifetime and should not be used otherwise + + :param ef: EconfFile to be freed + :return: None + """ + if not isinstance(ef, EconfFile): + raise TypeError("Parameter must be an EconfFile object") + if not ef._ptr: + return + LIBECONF.econf_freeFile(ef._ptr) + + +def set_conf_dirs(dir_postfix_list: list[str]) -> None: + """ + Set a list of directories (with order) that describe the paths where files have to be parsed + + E.G. with the given list: {"/conf.d/", ".d/", "/", NULL} files in following directories will be parsed: + "/..d/" "//conf.d/" "/.d/" "//" + The entry "/..d/" will be added automatically. + + :param dir_postfix_list: List of directories + :return: None + """ + if type(dir_postfix_list) != list: + raise TypeError("Directories must be passed as a list of strings") + if len(dir_postfix_list) == 0: + return + str_arr = c_char_p * len(dir_postfix_list) + dir_arr = str_arr() + for i in range(len(dir_postfix_list)): + if dir_postfix_list[i] is not None: + dir_postfix_list[i] = _encode_str(dir_postfix_list[i]) + dir_arr[i] = c_char_p(dir_postfix_list[i]) + err = LIBECONF.econf_set_conf_dirs(dir_arr) + if err: + raise ECONF_EXCEPTION[EconfError(err)](f"set_conf_dirs failed with error: {err_string(err)}") diff --git a/bindings/python3/pyproject.toml b/bindings/python3/pyproject.toml new file mode 100644 index 0000000..6b83f0b --- /dev/null +++ b/bindings/python3/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-libeconf" +version = "1.1.0" +description = "Python bindings for libeconf" +authors = [{name="nkrapp", email="nico.krapp@suse.com"}] +readme = "README.md" + +[project.optional-dependencies] +doc_requires = [ + "sphinx", + "sphinx-rtd-theme" +] +test_requires = [ + "pytest" +] + +[tool.isort] +profile = "black" diff --git a/bindings/python3/test/test_econf.py b/bindings/python3/test/test_econf.py new file mode 100644 index 0000000..fe1bcaf --- /dev/null +++ b/bindings/python3/test/test_econf.py @@ -0,0 +1,337 @@ +import pytest +import econf +from contextlib import contextmanager +from pathlib import Path +from ctypes import * + + +FILE = econf.read_file("test/testdata/examples/example.conf", "=", ";") +FILE2 = econf.read_file("test/testdata/examples2/example.conf", "=", "#") + + +@contextmanager +def does_not_raise(): + yield + + +def user_function(value: str) -> bool: + return value == "correct" + + +@pytest.mark.parametrize( + "value,expected,context", + [ + ("foo", b"foo", does_not_raise()), + (b"foo", b"foo", does_not_raise()), + (5, b"", pytest.raises(TypeError)), + ], +) +def test_encode_str(value, expected, context): + with context: + assert econf._encode_str(value) == expected + +@pytest.mark.parametrize( + "context,value", + [ + (does_not_raise(), "#"), + (does_not_raise(), b'+'), + (pytest.raises(TypeError), 3), + (pytest.raises(ValueError), "abc") + ] +) +def test_ensure_valid_char(context, value): + with context: + result = econf._ensure_valid_char(value) + + assert len(result) == 1 + assert isinstance(result, bytes) + + +@pytest.mark.parametrize( + "value,context", + [ + (5, does_not_raise()), + (99999999999999999999, pytest.raises(ValueError)), + ("a", pytest.raises(TypeError)), + ], +) +def test_ensure_valid_int(value, context): + with context: + result = econf._ensure_valid_int(value) + + assert isinstance(result, c_int64) + assert result.value == value + + +@pytest.mark.parametrize( + "value,context", + [ + (5, does_not_raise()), + (99999999999999999999, pytest.raises(ValueError)), + ("a", pytest.raises(TypeError)), + ], +) +def test_ensure_valid_uint(value, context): + with context: + result = econf._ensure_valid_uint(value) + + assert isinstance(result, c_uint64) + assert result.value >= 0 + assert result.value == value + + +@pytest.mark.parametrize( + "file,context", + [ + ("test/testdata/examples/example.conf", does_not_raise()), + ("test/testdata/examples/invalid.conf", pytest.raises(SyntaxError)), + ("test/testdata/examples/fakefile.conf", pytest.raises(FileNotFoundError)) + ] +) +def test_read_file(file, context): + with context: + result = econf.read_file(file, "=", "#") + + assert result._ptr != None + assert econf.get_groups(result) != None + assert econf.get_keys(result, None) != None + assert econf.delimiter_tag(result) == "=" + assert econf.comment_tag(result) == "#" + + +@pytest.mark.parametrize( + "file,context,data", + [ + ("test/testdata/examples/example.conf", does_not_raise(), "correct"), + ("test/testdata/examples/example.conf", pytest.raises(Exception, match="parsing callback has failed"), "wrong"), + ("test/testdata/examples/fakefile.conf", pytest.raises(FileNotFoundError), "correct"), + ("test/testdata/examples/invalid.conf", pytest.raises(SyntaxError), "correct") + ] +) +def test_read_file_with_callback(file, context, data): + with context: + result = econf.read_file_with_callback(file, "=", "#", user_function, data) + + assert result._ptr != None + assert econf.get_groups(result) != None + assert econf.get_keys(result, None) != None + assert econf.delimiter_tag(result) == "=" + assert econf.comment_tag(result) == "#" + +@pytest.mark.parametrize( + "context,delim,comment", + [ + (does_not_raise(), "=", "#"), + (pytest.raises(ValueError), "abc", "def"), + (pytest.raises(TypeError), 1, 2) + ] +) +def test_new_key_file(context, delim, comment): + with context: + result = econf.new_key_file(delim, comment) + + assert result + assert type(result) == econf.EconfFile + assert econf.delimiter_tag(result) == delim + assert econf.comment_tag(result) == comment + + +def test_new_ini_file(): + result = econf.new_ini_file() + + assert result + assert type(result) == econf.EconfFile + assert econf.delimiter_tag(result) == "=" + assert econf.comment_tag(result) == "#" + + +def test_merge_files(): + result = econf.merge_files(FILE, FILE2) + + assert len(econf.get_keys(result, None)) == 4 + assert len(econf.get_keys(result, "Group")) == 3 + assert len(econf.get_groups(result)) == 3 + + +def test_read_dirs(): + result = econf.read_dirs( + "test/testdata/examples2/", + "test/testdata/examples/", + "example", + "conf", + "=", + "#", + ) + + assert len(econf.get_keys(result, None)) == 3 + assert len(econf.get_keys(result, "Group")) == 4 + assert len(econf.get_groups(result)) == 3 + +@pytest.mark.parametrize( + "context,data", + [ + (does_not_raise(), "correct"), + (pytest.raises(Exception, match="parsing callback has failed"), "wrong") + ] +) +def test_read_dirs_with_callback(context, data): + with context: + usr_dir = "test/testdata/examples2/" + etc_dir = "test/testdata/examples" + name = "example" + result = econf.read_dirs_with_callback(usr_dir, etc_dir, name, "conf", "=", "#", user_function, data) + + assert len(econf.get_keys(result, None)) == 3 + assert len(econf.get_keys(result, "Group")) == 4 + assert len(econf.get_groups(result)) == 3 + + +def test_read_dirs_history(): + result = econf.read_dirs_history( + "test/testdata/examples2/", + "test/testdata/examples/", + "example", + "conf", + "=", + "#", + ) + + assert len(result) == 2 + assert len(econf.get_groups(result[0])) == 3 + assert len(econf.get_keys(result[0], None)) == 2 + assert len(econf.get_groups(result[1])) == 1 + +@pytest.mark.parametrize( + "context,data", + [ + (does_not_raise(), "correct"), + (pytest.raises(Exception, match="parsing callback has failed"), "wrong") + ] +) +def test_read_dirs_history_with_callback(context, data): + with context: + usr_dir = "test/testdata/examples2/" + etc_dir = "test/testdata/examples" + name = "example" + result = econf.read_dirs_history_with_callback(usr_dir, etc_dir, name, "conf", "=", "#", user_function, data) + + assert len(result) == 2 + assert len(econf.get_groups(result[0])) == 3 + assert len(econf.get_keys(result[0], None)) == 2 + assert len(econf.get_groups(result[1])) == 1 + + +@pytest.mark.parametrize( + "ef,context,expected", + [ + (FILE, does_not_raise(), ";"), + (FILE2, does_not_raise(), "#"), + ], +) +def test_comment_tag(ef, context, expected): + with context: + result = econf.comment_tag(ef) + + assert result == expected + + +@pytest.mark.parametrize( + "ef,context,expected", + [ + (FILE, does_not_raise(), "="), + (FILE2, does_not_raise(), "="), + ], +) +def test_delimiter_tag(ef, context, expected): + with context: + result = econf.delimiter_tag(ef) + + assert result == expected + + +@pytest.mark.parametrize( + "ef,context,expected", + [ + (FILE, does_not_raise(), "/"), + (FILE, pytest.raises(TypeError), 1), + (FILE, pytest.raises(ValueError), "abc"), + ], +) +def test_set_comment_tag(ef, context, expected): + with context: + econf.set_comment_tag(ef, expected) + result = econf.comment_tag(ef) + + assert result == expected + + +@pytest.mark.parametrize( + "ef,context, expected", + [ + (FILE, does_not_raise(), ":"), + (FILE, pytest.raises(TypeError), 1), + (FILE, pytest.raises(ValueError), "abc"), + ], +) +def test_set_delimiter_tag(ef, context, expected): + with context: + econf.set_delimiter_tag(ef, expected) + result = econf.delimiter_tag(ef) + + assert result == expected + + +def test_write_file(tmp_path): + d = str(tmp_path) + name = "example.conf" + result = econf.write_file(FILE, d, name) + + assert (tmp_path / "example.conf").exists() + + +@pytest.mark.parametrize( + "context,value,expected", + [ + (does_not_raise(), 0, "Success"), + (does_not_raise(), 5, "Key not found"), + (does_not_raise(), 23, "Unknown libeconf error 23"), + (pytest.raises(TypeError), "", "") + ] +) +def test_err_string(context, value, expected): + with context: + result = econf.err_string(value) + + assert result == expected + + +def test_err_location(): + file, line = econf.err_location() + + assert isinstance(file, str) + assert isinstance(line, int) + + +@pytest.mark.parametrize( + "file,context", + [ + #(FILE, does_not_raise()), + (econf.EconfFile(c_void_p(None)), does_not_raise()), + (5, pytest.raises(TypeError)) + ] +) +def test_free_file(file, context): + with context: + econf.free_file(file) + +@pytest.mark.parametrize( + "context,list", + [ + (does_not_raise(), ["/", "/conf.d/", None]), + (does_not_raise(), []), + (pytest.raises(TypeError), "") + ] +) +def test_set_conf_dirs(context, list): + with context: + econf.set_conf_dirs(list) diff --git a/bindings/python3/test/test_econf_getters.py b/bindings/python3/test/test_econf_getters.py new file mode 100644 index 0000000..9548c34 --- /dev/null +++ b/bindings/python3/test/test_econf_getters.py @@ -0,0 +1,147 @@ +import pytest +import econf +from contextlib import contextmanager +from pathlib import Path +from ctypes import * + + +FILE = econf.read_file("test/testdata/examples/example.conf", "=", ";") +FILE2 = econf.read_file("test/testdata/examples2/example.conf", "=", "#") + + +@contextmanager +def does_not_raise(): + yield + + +@pytest.mark.parametrize( + "file,context,example", + [ + (FILE, does_not_raise(), ["Another Group", "First Group", "Group"]), + (FILE2, pytest.raises(KeyError), []), + ], +) +def test_get_groups(file, context, example): + with context: + assert econf.get_groups(file) == example + + +@pytest.mark.parametrize( + "file,context,group,expected", + [ + (FILE, does_not_raise(), "Group", 3), + (FILE, does_not_raise(), None, 2), + (FILE2, does_not_raise(), None, 2), + (FILE, pytest.raises(TypeError), 1, 0), + (FILE, pytest.raises(KeyError), "a", 0), + ], +) +def test_get_keys(file, context, group, expected): + with context: + result = econf.get_keys(file, group) + + assert len(result) == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Bla", 12311), + (FILE, pytest.raises(KeyError), "Group", "a", 0), + (FILE, pytest.raises(KeyError), "a", "Bla", 12311), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, does_not_raise(), None, "foo", 6), + (FILE, does_not_raise(), "Group", "Welcome", 0), + ], +) +def test_get_int_value(file, context, group, key, expected): + with context: + result = econf.get_int_value(file, group, key) + + assert isinstance(result, int) + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Bla", 12311), + (FILE, pytest.raises(KeyError), "Group", "a", 0), + (FILE, pytest.raises(KeyError), "a", "Bla", 12311), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, does_not_raise(), None, "foo", 6), + (FILE, does_not_raise(), "Group", "Welcome", 0), + ], +) +def test_get_uint_value(file, context, group, key, expected): + with context: + result = econf.get_uint_value(file, group, key) + + assert isinstance(result, int) + assert result >= 0 + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Bla", 12311), + (FILE, pytest.raises(KeyError), "Group", "a", 0), + (FILE, pytest.raises(KeyError), "a", "Bla", 12311), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, does_not_raise(), None, "foo", 6.5), + (FILE, does_not_raise(), "Group", "Welcome", 0), + ], +) +def test_get_float_value(file, context, group, key, expected): + with context: + result = econf.get_float_value(file, group, key) + + assert isinstance(result, float) + assert result >= 0 + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Welcome", "Hello"), + ( + FILE, + does_not_raise(), + "First Group", + "Name", + "Keys File Example\\tthis value shows\\nescaping", + ), + (FILE, does_not_raise(), "First Group", "Welcome[de]", "Hallo"), + (FILE, does_not_raise(), "Group", "Bla", "12311"), + (FILE, does_not_raise(), None, "foo", "6.5"), + (FILE, pytest.raises(KeyError), "a", "Bla", "12311"), + (FILE, pytest.raises(KeyError), "Group", "foo", "6.5"), + (FILE, pytest.raises(TypeError), 7, 2, "12311"), + ], +) +def test_get_string_value(file, context, group, key, expected): + with context: + result = econf.get_string_value(file, group, key) + + assert isinstance(result, str) + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Another Group", "Booleans", True), + (FILE, pytest.raises(Exception, match="Parse error"), "Group", "Bla", True), + (FILE, pytest.raises(KeyError), "a", "Booleans", True), + (FILE, pytest.raises(KeyError), "Another Group", "Bools", True), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + ], +) +def test_get_bool_value(file, context, group, key, expected): + with context: + result = econf.get_bool_value(file, group, key) + + assert isinstance(result, bool) + assert result == expected diff --git a/bindings/python3/test/test_econf_getters_def.py b/bindings/python3/test/test_econf_getters_def.py new file mode 100644 index 0000000..25cf5dd --- /dev/null +++ b/bindings/python3/test/test_econf_getters_def.py @@ -0,0 +1,128 @@ +import pytest +import econf +from contextlib import contextmanager +from pathlib import Path +from ctypes import * + + +FILE = econf.read_file("test/testdata/examples/example.conf", "=", ";") +FILE2 = econf.read_file("test/testdata/examples2/example.conf", "=", "#") + + +@contextmanager +def does_not_raise(): + yield + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Bla", 12311), + (FILE, does_not_raise(), "Group", "Invalid Key", 1), + (FILE, does_not_raise(), "Invalid Group", "Bla", 1), + (FILE, does_not_raise(), None, "foo", 6), + (FILE, does_not_raise(), "Group", "Welcome", 0), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, pytest.raises(TypeError), "Group", "Bla", "default"), + (FILE, pytest.raises(TypeError), "Group", "Invalid Key", "default"), + ], +) +def test_get_int_value_def(file, context, group, key, expected): + with context: + result = econf.get_int_value_def(file, group, key, expected) + + assert isinstance(result, int) + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Bla", 12311), + (FILE, does_not_raise(), "Group", "Invalid Key", 0), + (FILE, does_not_raise(), "Invalid Group", "Bla", 12311), + (FILE, does_not_raise(), None, "foo", 6), + (FILE, does_not_raise(), "Group", "Welcome", 0), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, pytest.raises(TypeError), "Group", "Bla", "default"), + (FILE, pytest.raises(TypeError), "Group", "Invalid Key", "default"), + ], +) +def test_get_uint_value_def(file, context, group, key, expected): + with context: + result = econf.get_uint_value_def(file, group, key, expected) + + assert isinstance(result, int) + assert result >= 0 + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Bla", 12311.0), + (FILE, does_not_raise(), "Group", "Invalid Key", 0.1), + (FILE, does_not_raise(), "Invalid Group", "Bla", 12311.1), + (FILE, does_not_raise(), None, "foo", 6.5), + (FILE, does_not_raise(), "Group", "Welcome", 0.0), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, pytest.raises(TypeError), None, "foo", "default"), + (FILE, pytest.raises(TypeError), "Group", "Invalid Key", "default"), + ], +) +def test_get_float_value_def(file, context, group, key, expected): + with context: + result = econf.get_float_value_def(file, group, key, expected) + + assert isinstance(result, float) + assert result >= 0 + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Group", "Welcome", "Hello"), + ( + FILE, + does_not_raise(), + "First Group", + "Name", + "Keys File Example\\tthis value shows\\nescaping", + ), + (FILE, does_not_raise(), "First Group", "Welcome[de]", "Hallo"), + (FILE, does_not_raise(), "Group", "Bla", "12311"), + (FILE, does_not_raise(), None, "foo", "6.5"), + (FILE, does_not_raise(), "Invalid Group", "Bla", "default"), + (FILE, does_not_raise(), "Group", "foo", "default"), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, pytest.raises(TypeError), "Group", "Welcome", 7), + (FILE, pytest.raises(TypeError), "Group", "Invalid Key", 7), + ], +) +def test_get_string_value_def(file, context, group, key, expected): + with context: + result = econf.get_string_value_def(file, group, key, expected) + + assert isinstance(result, str) + assert result == expected + + +@pytest.mark.parametrize( + "file,context,group,key,expected", + [ + (FILE, does_not_raise(), "Another Group", "Booleans", True), + (FILE, pytest.raises(Exception, match="Parse error"), "Group", "Bla", True), + (FILE, does_not_raise(), "Invalid Group", "Booleans", False), + (FILE, does_not_raise(), "Another Group", "Bools", False), + (FILE, pytest.raises(TypeError), 7, 2, 12311), + (FILE, pytest.raises(TypeError), "Another Group", "Booleans", 12311), + (FILE, pytest.raises(TypeError), "Another Group", "Bools", 12311), + ], +) +def test_get_bool_value_def(file, context, group, key, expected): + with context: + result = econf.get_bool_value_def(file, group, key, expected) + + assert isinstance(result, bool) + assert result == expected diff --git a/bindings/python3/test/test_econf_setters.py b/bindings/python3/test/test_econf_setters.py new file mode 100644 index 0000000..c721df3 --- /dev/null +++ b/bindings/python3/test/test_econf_setters.py @@ -0,0 +1,116 @@ +import pytest +import econf +from contextlib import contextmanager +from pathlib import Path +from ctypes import * + + +FILE = econf.read_file("test/testdata/examples/example.conf", "=", ";") +FILE2 = econf.read_file("test/testdata/examples2/example.conf", "=", "#") + + +@contextmanager +def does_not_raise(): + yield + + +@pytest.mark.parametrize( + "file,context,group,key,value", + [ + (FILE, does_not_raise(), "Group", "Bla", 1), + (FILE, does_not_raise(), "Group", "Welcome", 1), + (FILE, does_not_raise(), None, "foo2", 1), + (FILE, pytest.raises(ValueError), "Group", "Bla", 99999999999999999999), + (FILE, does_not_raise(), "New Group", "Bla", 1), + (FILE, pytest.raises(TypeError), 7, 2, 1), + (FILE, pytest.raises(TypeError), "Group", "Bla", "Invalid Value"), + (FILE, pytest.raises(TypeError), "Invalid Group", "Bla", "Invalid Value"), + ], +) +def test_set_int_value(file, context, group, key, value): + with context: + econf.set_int_value(file, group, key, value) + result = econf.get_int_value(file, group, key) + + assert result == value + + +@pytest.mark.parametrize( + "file,context,group,key,value", + [ + (FILE, does_not_raise(), "Group", "Bla", 1), + (FILE, does_not_raise(), "Group", "Welcome", 1), + (FILE, does_not_raise(), "New Group", "Bla", 1), + (FILE, does_not_raise(), None, "foo2", 1), + (FILE, pytest.raises(TypeError), "Group", "Bla", -1), + (FILE, pytest.raises(ValueError), "Group", "Bla", 99999999999999999999), + (FILE, pytest.raises(TypeError), 7, 2, 1), + (FILE, pytest.raises(TypeError), "Group", "Bla", "Invalid Value"), + (FILE, pytest.raises(TypeError), "Invalid Group", "Bal", "Invalid Value"), + ], +) +def test_set_uint_value(file, context, group, key, value): + with context: + econf.set_uint_value(file, group, key, value) + result = econf.get_uint_value(file, group, key) + + assert result == value + + +@pytest.mark.parametrize( + "file,context,group,key,value", + [ + (FILE, does_not_raise(), None, "foo", 1.5), + (FILE, does_not_raise(), "Group", "Welcome", 1.5), + (FILE, does_not_raise(), "New Group", "Bla", 1.5), + (FILE, does_not_raise(), "Group", "Bla", -1.5), + (FILE, pytest.raises(TypeError), 7, 2, 1), + (FILE, pytest.raises(TypeError), "Group", "Bla", "Invalid Value"), + (FILE, pytest.raises(TypeError), "Group", "Bla", 1), + ], +) +def test_set_float_value(file, context, group, key, value): + with context: + econf.set_float_value(file, group, key, value) + result = econf.get_float_value(file, group, key) + + assert result == value + + +@pytest.mark.parametrize( + "file,context,group,key,value", + [ + (FILE, does_not_raise(), "Group", "Welcome", "Bye"), + (FILE, does_not_raise(), "Group", "Bla", "1"), + (FILE, does_not_raise(), "New Group", "Welcome", "Bye"), + (FILE, does_not_raise(), "First Group", "Name", "\nNoname"), + (FILE, pytest.raises(TypeError), 7, 2, 1), + (FILE, pytest.raises(TypeError), "Group", "Bla", 1.5), + (FILE, pytest.raises(TypeError), "Group", "Bla", True), + ], +) +def test_set_string_value(file, context, group, key, value): + with context: + econf.set_string_value(file, group, key, value) + result = econf.get_string_value(file, group, key) + + assert result == value + + +@pytest.mark.parametrize( + "file,context,group,key,value", + [ + (FILE, does_not_raise(), "Another Group", "Booleans", False), + (FILE, does_not_raise(), "Group", "Bla", True), + (FILE, does_not_raise(), "New Group", "Welcome", True), + (FILE, pytest.raises(TypeError), 7, 2, 1), + (FILE, pytest.raises(TypeError), "Group", "Bla", "Invalid Value"), + (FILE, pytest.raises(TypeError), "Group", "Bla", ""), + ], +) +def test_set_bool_value(file, context, group, key, value): + with context: + econf.set_bool_value(file, group, key, value) + result = econf.get_bool_value(file, group, key) + + assert result == value diff --git a/bindings/python3/test/testdata/examples/encoding_test.conf b/bindings/python3/test/testdata/examples/encoding_test.conf new file mode 100644 index 0000000..6fd6074 --- /dev/null +++ b/bindings/python3/test/testdata/examples/encoding_test.conf @@ -0,0 +1 @@ +foo=barŸ# \ No newline at end of file diff --git a/bindings/python3/test/testdata/examples/example.conf b/bindings/python3/test/testdata/examples/example.conf new file mode 100644 index 0000000..d574046 --- /dev/null +++ b/bindings/python3/test/testdata/examples/example.conf @@ -0,0 +1,20 @@ +foo=6.5 +foo2=-6 + +[Another Group] +Numbers=2;20;-200;0;232 +Booleans=true;false + +[First Group] +Name=Keys File Example\tthis value shows\nescaping +Welcome[fr_FR]=Bojour +Welcome[la]=Salve +Welcome[it] = Ci o +Welcome[be@latin]=Hello +Welcome[de]=Hallo +Welcome=Hello + +[Group] +Bla=12311 +Welcome[la]=Salve +Welcome=Hello diff --git a/bindings/python3/test/testdata/examples/example.conf.d/snippet.conf b/bindings/python3/test/testdata/examples/example.conf.d/snippet.conf new file mode 100644 index 0000000..327d73c --- /dev/null +++ b/bindings/python3/test/testdata/examples/example.conf.d/snippet.conf @@ -0,0 +1,5 @@ +abc=5 +foo=baz + +[Group] +Number=5 \ No newline at end of file diff --git a/bindings/python3/test/testdata/examples/invalid.conf b/bindings/python3/test/testdata/examples/invalid.conf new file mode 100644 index 0000000..6be5d48 --- /dev/null +++ b/bindings/python3/test/testdata/examples/invalid.conf @@ -0,0 +1,6 @@ +[valid] +foo +abc + +[empty key += 5 \ No newline at end of file diff --git a/bindings/python3/test/testdata/examples2/example.conf b/bindings/python3/test/testdata/examples2/example.conf new file mode 100644 index 0000000..edcbdde --- /dev/null +++ b/bindings/python3/test/testdata/examples2/example.conf @@ -0,0 +1,2 @@ +fooo=bar +bar=foo \ No newline at end of file