Repository: SINTEF/dlite (language: python)

Issue:
Avoid creating a new internal interpreter if DLite is called from Python
# Description
If DLite is called from Python, run the Python storage plugins from the calling interpreter. 

**Edit (new since first review):** The suggestion in the [comment](https://github.com/SINTEF/dlite/pull/907#issuecomment-2283582422) below is now implemented. By default is the old behavior kept for now. The new behavior will be the default from v0.7.0. 

When importing DLite from Python, the user will now get follow warning message printed to stderr:

> Warning: Behavior `singleInterpreter` is not configured. It will be enabled by default from v0.7.0. The old behavior is
> scheduled for removal in v0.9.0.
> 
> To configure the behavior, set environment variable `DLITE_BEHAVIOR_singleInterpreter` to a boolean value.  True will
> enable the new behavior and false will disable it.
> 
>You can also configure it from Python by setting `dlite.Behavior.singleInterpreter` or from C by calling `dlite_behavior_set()`.

You can get rid of this warning by defining the environment variable `DLITE_BEHAVIOR_singleInterpreter` (or `DLITE_BEHAVIOR`). 
- Is the warning message understandable? 
- Should the description of the new behavior be included in the warning? 
- Is it too long (could e.g. be shortened by referring to the documentation)? 
- Is it OK to get this warning by default, or is it something the user explicitly should ask for to get (e.g. by defining an environment variable)?

**Edit2:** Shortened the warning message to:

> Warning: Behavior `<NAME>` is not configured. It will be enabled by default from v0.7.0. See https://sintef.github.io/dlite/user_guide/configure_behavior_changes.html for more info.

Added new page about how to configure behavior changes to the user guide.

---

The motivation for this PR was to investigate the following benefits:
* When DLite is called from Python, DLite is not explicitly shutting down any internal interpreter, which could solve the issues with segfault during shutdown. 
* This PR has the potential of improving error reporting from plugins.
  - Interactive testing indicates that the error reporting is not improved...

Both of these benefits should be investigated further. Possible drawbacks should also be checked.

## Type of change
- [ ] Bug fix & code cleanup
- [x] New feature
- [ ] Documentation update
- [ ] Test update

## Checklist for the reviewer
This checklist should be used as a help for the reviewer.

- [ ] Is the change limited to one issue?
- [ ] Does this PR close the issue?
- [ ] Is the code easy to read and understand?
- [ ] Do all new feature have an accompanying new test?
- [ ] Has the documentation been updated as necessary?



Discussion / hints:
From discussion with @francescalb:

This is a fundamental change in DLite that we should be carefully thought and tested before merged in. At the same time, we believe that this PR provides a better solution for running plugins that will create less confusion in the long run. 

To avoid having to maintain two development branches, we suggest the following:
* By default, keep the old behaviour for now. 
* Add a new environment variable. When set, switch to the new behaviour.
* Add tests for both the old and new behaviour.
* Make a plan for when to switch to the new behaviour.

This way of handling possible breaking changes could be formalised and standardised. We are For example already using deprecation warnings, but not very formalised. We should add a plan for when removing deprecated features.

Can the new environment variable be use for other behaviour changes as well? Or is it simpler to make a series of related environment variables, one for each feature to change (and test for)? If we go for the latter, would `DLITE_BEHAVIOUR_*` be good prefix for such environment variables. There should be an index of such variables with of what behaviour changes they control. A list of these variables should also be grammatically accessible for matrix testing.

Fix patch:
diff --git a/bindings/python/dlite-behavior-python.i b/bindings/python/dlite-behavior-python.i
new file mode 100644
index 000000000..554929059
--- /dev/null
+++ b/bindings/python/dlite-behavior-python.i
@@ -0,0 +1,61 @@
+/* -*- Python -*-  (not really, but good for syntax highlighting) */
+
+/* Python-specific extensions to dlite-behavior.i */
+
+
+%extend _DLiteBehavior {
+  %pythoncode %{
+    def asdict(self):
+        return {
+            "name": self.name,
+            "value": bool(self.value),
+            "version_added": self.version_added,
+            "version_new": self.version_new,
+            "version_remove": self.version_remove,
+            "description": self.description,
+        }
+
+    def __str__(self):
+        import json
+        return json.dumps(self.asdict(), indent=2)
+  %}
+}
+
+
+%pythoncode %{
+
+class _Behavior():
+    """A class that provides easy access for setting and getting behavior
+    settings.
+
+    It is used via the singleton `dlite.Behavior`.
+
+    The different behavior values can be accessed as attributes.
+
+    """
+    def __getattr__(self, name):
+        return bool(_dlite._behavior_get(name))
+
+    def __setattr__(self, name, value):
+        _dlite._behavior_set(name, 1 if value else 0)
+
+    def __dir__(self):
+        return object.__dir__(self) + list(self.get_names())
+
+    def get_names(self):
+        """Return a generator over all registered behavior names."""
+        for i in range(_dlite._behavior_nrecords()):
+            yield _dlite._behavior_recordno(i).name
+
+    def get_record(self, name):
+        """Return a record with information about the given behavior."""
+        rec = _dlite._behavior_record(name)
+        if not rec:
+            raise DLiteKeyError(f"No such behavior record: {name}")
+        return rec
+
+
+# A singleton for accessing behavior settings.
+Behavior = _Behavior()
+
+%}
diff --git a/bindings/python/dlite-behavior.i b/bindings/python/dlite-behavior.i
new file mode 100644
index 000000000..462caa825
--- /dev/null
+++ b/bindings/python/dlite-behavior.i
@@ -0,0 +1,59 @@
+/* -*- C -*-  (not really, but good for syntax highlighting) */
+%{
+#include "dlite-behavior.h"
+%}
+
+%rename(_BehaviorRecord) _DLiteBehavior;
+struct _DLiteBehavior {
+  %immutable;
+  char *name;
+  char *version_added;
+  char *version_new;
+  char *version_remove;
+  char *description;
+  %mutable;
+  int value;
+};
+
+%rename("%(strip:[dlite])s") "";
+
+
+%feature("docstring", "\
+Return the number of registered behaviors.
+") dlite_behavior_nrecords;
+size_t dlite_behavior_nrecords(void);
+
+%feature("docstring", "\
+Return a pointer to record with the given number or NULL if `n` is out of
+range.
+") dlite_behavior_recordno;
+const struct _DLiteBehavior *dlite_behavior_recordno(size_t n);
+
+%feature("docstring", "\
+Return a pointer to the given behavior record, or NULL if `name` is
+not in the behavior table.
+") dlite_behavior_record;
+const struct _DLiteBehavior *dlite_behavior_record(const char *name);
+
+%feature("docstring", "\
+Get value of given behavior.
+
+Returns 1 if the behavior is on, 0 if it is off and a negative
+value on error.
+") dlite_behavior_get;
+int dlite_behavior_get(const char *name);
+
+%feature("docstring", "\
+Enable given behavior if `value` is non-zero.  Disable if `value` is zero.
+
+Returns non-zero on error.
+") dlite_behavior_set;
+int dlite_behavior_set(const char *name, int value);
+
+
+/* -----------------------------------
+ * Target language-spesific extensions
+ * ----------------------------------- */
+#ifdef SWIGPYTHON
+%include "dlite-behavior-python.i"
+#endif
diff --git a/bindings/python/dlite-misc-python.i b/bindings/python/dlite-misc-python.i
index f8f011664..866a222d6 100644
--- a/bindings/python/dlite-misc-python.i
+++ b/bindings/python/dlite-misc-python.i
@@ -5,6 +5,8 @@
 %pythoncode %{
 
 import atexit
+import warnings
+
 
 atexit.register(_mark_python_atexit)
 
@@ -77,4 +79,40 @@ class errctl():
 
 
 silent = errctl(filename="None")
+
+# A set for keeping track of already issued deprecation warnings
+_deprecation_warning_record = set()
+
+def deprecation_warning(version_removed, descr):
+    """Issues a deprecation warning.
+
+    Arguments:
+        version_removed: The version the deprecated feature is
+            expected to be finally removed.
+        descr: Description of the deprecated feature.
+
+    Returns non-zero on error.
+    """
+    if descr in _deprecation_warning_record:
+        return
+    _deprecation_warning_record.add(descr)
+
+    warnings.warn(descr, DeprecationWarning, stacklevel=2)
+
+    dlite_version = get_version()
+    if chk_semver(version_removed) < 0:
+        raise SystemError(
+            f"argument `version_removed='{version_removed}'` must be a "
+            "valid semantic version number",
+        )
+    if chk_semver(dlite_version) < 0:
+        raise SystemError(
+            f"DLite version number is not semantic: {dlite_version}"
+        )
+    if cmp_semver(version_removed, dlite_version) <= 0:
+        raise SystemError(
+            "deprecated feature was supposed to be removed in version "
+            f"{version_removed}: {descr}"
+        )
+
 %}
diff --git a/bindings/python/dlite-misc.i b/bindings/python/dlite-misc.i
index 9501b5605..063aa43d9 100644
--- a/bindings/python/dlite-misc.i
+++ b/bindings/python/dlite-misc.i
@@ -34,6 +34,16 @@
   /* Just check for errors, do nothing else. */
   void errcheck(void) {}
 
+  int chk_semver(const char *version, int n) {
+    if (n >= 0) return strnchk_semver(version, n);
+    return strchk_semver(version);
+  }
+
+  int cmp_semver(const char *v1, const char *v2, int n) {
+    if (n >= 0) return strncmp_semver(v1, v2, n);
+    return strcmp_semver(v1, v2);
+  }
+
 %}
 
 %include <stdint.i>
@@ -211,6 +221,28 @@ Set error stream.
 bool asbool(const char *str);
 
 
+%feature("docstring", "\
+Check if `version` is a valid semantic version 2.0.0 number.
+
+By default, the whole string is checked.  If `n` is non-negative, only
+the initial part of `version` is checked, at most `n` characters.
+
+Returns the length of the valid part of `version` or -1 if `version` is
+not a valid semantic number.
+") chk_semver;
+int chk_semver(const char *version, int n=-1);
+
+%feature("docstring", "\
+Compare semantic version strings `v1` and `v2`.
+
+Returns -1 if `v1 < v2`, 0 if `v1 == v2` and 1 if `v1 > v2`.
+
+By default, the whole of `v1` and `v2` are compared.  If `n` is non-negative,
+only the initial part of `v1` and `v2` are compared, at most `n` characters.
+") cmp_semver;
+int cmp_semver(const char *v1, const char *v2, int n=-1);
+
+
 
 /* -----------------------------------
  * Target language-spesific extensions
diff --git a/bindings/python/dlite.i b/bindings/python/dlite.i
index 9ef5ab00f..1043d99a0 100644
--- a/bindings/python/dlite.i
+++ b/bindings/python/dlite.i
@@ -112,3 +112,4 @@
 %include "dlite-collection.i"
 %include "dlite-path.i"
 %include "dlite-mapping.i"
+%include "dlite-behavior.i"
diff --git a/doc/contributors_guide/code_changes.md b/doc/contributors_guide/code_changes.md
index f5ddb479f..e67b1520c 100644
--- a/doc/contributors_guide/code_changes.md
+++ b/doc/contributors_guide/code_changes.md
@@ -1,15 +1,16 @@
 Code changes
 ============
-Although that DLite haven't reached version 1.0.0, it strives to be stable and not introduce unnecessary backward-incompatible changes.
+Although that DLite has not reached version 1.0.0, the intention is for it to be stable and not introduce unnecessary backward-incompatible changes.
 
 
 Versioning
 ----------
-Before reaching v 1.0.0, DLite follows the following versioning rules that are well-known from Python:
+Before reaching version 1.0.0, DLite follows the following versioning rules that are well-known from Python:
 
 * **patch release**: Only backward-compatible changes.
   - Addition of new backward-compatible features.
   - New deprecation warnings can be added.
+  - Bug fixes.
 
 * **minor release**: Backward-incompatible changes.
   - Addition of new backward-incompatible features.
@@ -21,11 +22,10 @@ After version 1.0.0, DLite should strictly follow the [semantic versioning] rule
 
 Feature deprecation
 -------------------
-In Python, use `dlite.deprecation_warning()` (function to be added) to mark deprecation warnings.
+In Python, use `dlite.deprecation_warning()` to mark deprecation warnings.
 This function is a simple wrapper around `warnings.warn(msg, DeprecationWarning, stacklevel=2)` with the release version when the feature is planned to be removed as an additional argument.
 
-In C, use the `deprecation_warning()` function (to be added) to mark a
-deprecation warning.
+In C, use the `dlite_deprecation_warning()` function to mark a deprecation warning.
 
 The idea with these functions is to make it easy to grep for deprecation warnings that should be remove in a new backward-incompatible release.
 The following command can e.g. be used to find all deprecation warnings:
@@ -43,7 +43,7 @@ Adding new arguments to an existing function or new methods to existing classes
 **Note**, new (positional) arguments should be added to the end of the argument list, such that existing code using positional arguments will be unaffected by the change.
 
 ### New behavior
-All changes in behavior should be considered backward-incompatible.
+All changes in behavior in existing code should be considered backward-incompatible.
 
 Where it make sense, DLite will try to optionally keep the new/old behaviour over some releases in order for users to adapt to the new behavior.
 
@@ -53,23 +53,29 @@ Each optional new behavior should have a:
 - release number for when the behavior was introduced (keeping old behavior as default)
 - release number for when the new behavior should be the default (managed automatically)
 - expected release number for when the old behavior should be removed (require developer effort)
+- value: True means that the behavior is enabled.
 
-All behaviors are described in a single source file `src/dlite-behavior.c`.
+All behavior changes will be described in a single source file `src/dlite-behavior.c`.
 
-Behavior can be selected in several ways:
+Whether to enable a behavior can be configured in several ways:
 
 - **programmatically from Python**:
 
-      >>> from dlite.behavior import Behavior
-      >>> Behavior.<NAME> = True  # true means use the new behavior
+      >>> dlite.Behavior.<NAME> = True
 
 - **programmatically from C**:
 
-      dlite_behavior("<NAME>", 1);
+      dlite_behavior_set("<NAME>", 1);
 
 - **via environment variables**:
 
-      export DLITE_BEHAVIOR_<NAME>
+      export DLITE_BEHAVIOR_<NAME>=1
+
+  The empty string or any of the following values will enable the behavior:
+  "true", ".true.", "on", "yes", 1
+
+  Any of the following values will disable the behavior:
+  "false", ".false.", "off", "no", 0
 
 A warning will automatically be issued if a behavior is not selected explicitly.
 
diff --git a/doc/user_guide/environment_variables.md b/doc/user_guide/environment_variables.md
index 35a5b5833..ce8d32937 100644
--- a/doc/user_guide/environment_variables.md
+++ b/doc/user_guide/environment_variables.md
@@ -52,6 +52,14 @@ DLite-specific environment variables
   - **DLITE_ATEXIT_FREE**: Free memory at exit.  This might be useful to avoid
     getting false positive when tracking down memory leaks with tools like valgrind.
 
+  - **DLITE_BEHAVIOR_<name>**: Enables/disables the behavior `<name>`.
+
+    The empty string or any of the following values will enable the behavior:
+    "true", ".true.", "on", "yes", 1
+
+    Any of the following values will disable the behavior:
+    "false", ".false.", "off", "no", 0
+
 
 ### Specific paths
 These environment variables can be used to provide additional search
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1b0bcf5aa..95ea44342 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -45,6 +45,7 @@ set(sources
   dlite-getlicense.c
   dlite-json.c
   dlite-bson.c
+  dlite-behavior.c
   getuuid.c
   pathshash.c
   triple.c
diff --git a/src/dlite-behavior.c b/src/dlite-behavior.c
new file mode 100644
index 000000000..6fe6596d7
--- /dev/null
+++ b/src/dlite-behavior.c
@@ -0,0 +1,153 @@
+#include <assert.h>
+
+#include "utils/strtob.h"
+#include "utils/strutils.h"
+#include "dlite.h"
+#include "dlite-behavior.h"
+
+
+/* Table listing all defined behaviors.
+
+   name: Name of behavior.  Should be a unique and valid Python identifier
+       of at most 48 characters length.
+   v_added: Version number when the behavior was added.
+   v_new: Version number when the new behavior will be the default.
+   v_removed: Expected version number when the behavior is removed.
+   description: Full description of the new feature.
+   value: Just set it to zero. It will be initialised the first time the table
+       is accessed.
+*/
+
+//  name                           v_added    v_new      v_removed
+//  description
+static DLiteBehavior behavior_table[] = {
+  { "singleInterpreter",           "0.5.17",  "0.7.0",   "0.9.0",
+    "Plugins are executed by the calling interpreter when DLite is called "
+    "from Python.", -1 },
+  { NULL,                          NULL,      NULL,      NULL,     NULL, 0 }
+};
+
+
+
+/*
+  Initialise the behavior table. Will be called automatically, so no need
+  to call it explicitly.  It a noop after the first call.
+
+  Default values can currently only be configured via environment
+  variables.  If the environment variable `DLITE_BEHAVIOR_<name>` is
+  defined, the behavior whos name is `<name>` is assigned.  An
+  environment variable with no value is interpreted as true.
+
+  If no default is given, the behavior will be disabled (value=0) if
+  the current DLite version is less than `version_new` and enabled
+  (value=1) otherwise.
+*/
+void dlite_behavior_table_init(void)
+{
+  static int initialised=0;
+
+  if (!initialised) {
+    DLiteBehavior *b = (DLiteBehavior *)behavior_table;
+    while (b->name) {
+      b->value = -1;
+
+      /* Check environment */
+      char buf[64];
+      snprintf(buf, sizeof(buf), "DLITE_BEHAVIOR_%s", b->name);
+      const char *env = getenv(buf);
+      if (env) b->value = (*env) ? atob(env) : 1;
+
+      /* Warn if behavior is expected to be removed. */
+      if (strcmp_semver(dlite_get_version(), b->version_remove) >= 0)
+        dlite_warn("Behavior `%s` was scheduled for removal since version %s",
+                   b->name, b->version_remove);
+
+      b++;
+    }
+    initialised = 1;
+  }
+}
+
+
+/*
+  Return the number of registered behaviors.
+*/
+size_t dlite_behavior_nrecords(void)
+{
+  return sizeof(behavior_table) / sizeof(DLiteBehavior) - 1;
+}
+
+
+/*
+  Return a pointer to record with the given number or NULL if `n` is out of
+  range.
+*/
+const DLiteBehavior *dlite_behavior_recordno(size_t n)
+{
+  size_t N = dlite_behavior_nrecords();
+  if (n > N) return NULL;
+  return behavior_table + n;
+}
+
+
+/*
+  Return a pointer to the given behavior record, or NULL if `name` is
+  not in the behavior table.
+
+  Note: Please use dlite_behavior_get() to access the record value,
+  since it may not be fully initialised by this function.
+ */
+const DLiteBehavior *dlite_behavior_record(const char *name)
+{
+  const DLiteBehavior *b = behavior_table;
+  dlite_behavior_table_init();
+  while (b->name) {
+    if (strcmp(b->name, name) == 0) return b;
+    b++;
+  }
+  return NULL;
+}
+
+
+/*
+  Return the value of given behavior or a netative error code on error.
+ */
+int dlite_behavior_get(const char *name)
+{
+  DLiteBehavior *b = (DLiteBehavior *)dlite_behavior_record(name);
+  if (!b) return dlite_err(dliteKeyError, "No behavior with name: %s", name);
+
+  /* If value is unset, enable behavior if DLite version > version_new */
+  if (b->value < 0) {
+    const char *ver = dlite_get_version();  // current version
+    b->value = (strcmp_semver(ver, b->version_new) >= 0) ? 1 : 0;
+
+    dlite_warn("Behavior `%s` is not configured. "
+               "It will be enabled by default from v%s. "
+               "The old behavior is scheduled for removal in v%s.\n\n"
+               "To configure the behavior, set environment variable "
+               "`DLITE_BEHAVIOR_%s` to a boolean value.  True will "
+               "enable the new behavior and false will disable it.\n\n"
+               "You can also configure it from Python by setting "
+               "`dlite.Behavior.%s` or from C by calling "
+               "`dlite_behavior_set()`.",
+               b->name, b->version_new, b->version_remove, b->name, b->name);
+  }
+
+  assert(b->value >= 0);
+  return b->value;
+}
+
+
+/*
+  Assign value of given behavior: 1=on, 0=off.
+
+  Returns non-zero on error.
+*/
+int dlite_behavior_set(const char *name, int value)
+{
+  DLiteBehavior *b = (DLiteBehavior *)dlite_behavior_record(name);
+  if (!b) return dlite_err(dliteKeyError, "No behavior with name: %s", name);
+  b->value = value;
+  return 0;
+}
diff --git a/src/dlite-behavior.h b/src/dlite-behavior.h
new file mode 100644
index 000000000..25f64edb8
--- /dev/null
+++ b/src/dlite-behavior.h
@@ -0,0 +1,57 @@
+#ifndef _DLITE_BEHAVIOR_H
+#define _DLITE_BEHAVIOR_H
+
+/**
+  @file
+  @brief Handling of behavior changes in the code
+
+  See https://sintef.github.io/dlite/contributors_guide/code_changes.html
+*/
+
+
+/** Columns in the behavior_table. */
+typedef struct _DLiteBehavior {
+  char *name;           /*!< Name of behavior.  Should be a unique identifier. */
+  char *version_added;  /*!< Version number when the behavior was added. */
+  char *version_new;    /*!< Version number when the new behavior is default. */
+  char *version_remove; /*!< Expected version when the behavior is removed. */
+  char *description;    /*!< Description of the behavior. */
+  int value;            /*!< Behavior value: 1=on, 0=off, -1=unset */
+} DLiteBehavior;
+
+
+/**
+  Return the number of registered behaviors.
+*/
+size_t dlite_behavior_nrecords(void);
+
+/**
+  Return a pointer to record with the given number or NULL if `n` is out of
+  range.
+*/
+const DLiteBehavior *dlite_behavior_recordno(size_t n);
+
+/**
+  Return a pointer to the given behavior record, or NULL if `name` is
+  not in the behavior table.
+
+  Note: Please use dlite_behavior_get() to access the record value,
+  since it not be fully initialised by this function.
+ */
+const DLiteBehavior *dlite_behavior_record(const char *name);
+
+/**
+  Return the value of given behavior or a netative error code on error.
+ */
+int dlite_behavior_get(const char *name);
+
+/**
+  Assign value of given behavior: 1=on, 0=off.
+
+  Returns non-zero on error.
+*/
+int dlite_behavior_set(const char *name, int value);
+
+
+
+#endif /* _DLITE_BEHAVIOR_H */
diff --git a/src/dlite-macros.h b/src/dlite-macros.h
index 2d2f6e7fc..a5063bb9f 100644
--- a/src/dlite-macros.h
+++ b/src/dlite-macros.h
@@ -36,26 +36,21 @@
 #define FAIL5(msg, a1, a2, a3, a4, a5) do {		\
     dlite_err(1, msg, a1, a2, a3, a4, a5); goto fail; } while (0)
 
-/* Error macros for when DLiteDataModel instance d is available */
-#define DFAIL0(code, d, msg) \
-  do {err(code, "%s/%s: " msg, d->s->location, d->uuid); goto fail;} while (0)
-
-#define DFAIL1(code, d, msg, a1) \
-  do {err(code, "%s/%s: " msg, d->s->location, d->uuid, a1); goto fail;} while (0)
-
-#define DFAIL2(code, d, msg, a1, a2) \
-  do {err(code, "%s/%s: " msg, d->s->location, d->uuid, a1, a2); \
+/** Error macros for when DLiteDataModel instance d is available */
+#define DFAIL0(code, d, msg) do { \
+  err(code, "%s/%s: " msg, d->s->location, d->uuid); goto fail;} while (0)
+#define DFAIL1(code, d, msg, a1) do { \
+  err(code, "%s/%s: " msg, d->s->location, d->uuid, a1); goto fail;} while (0)
+#define DFAIL2(code, d, msg, a1, a2) do { \
+  err(code, "%s/%s: " msg, d->s->location, d->uuid, a1, a2); \
     goto fail;} while (0)
-
-#define DFAIL3(code, d, msg, a1, a2, a3) \
-  do {err(code, "%s/%s: " msg, d->s->location, d->uuid, a1, a2, a3); \
+#define DFAIL3(code, d, msg, a1, a2, a3) do { \
+  err(code, "%s/%s: " msg, d->s->location, d->uuid, a1, a2, a3); \
     goto fail;} while (0)
-
-#define DFAIL4(code, d, msg, a1, a2, a3, a4)                            \
-  do {err(code, "%s/%s: " msg, d->s->location, d->uuid, a1, a2, a3, a4); \
+#define DFAIL4(code, d, msg, a1, a2, a3, a4) do { \
+  err(code, "%s/%s: " msg, d->s->location, d->uuid, a1, a2, a3, a4); \
     goto fail;} while (0)
 
-
 /** Failure macros with explicit error codes **/
 #define FAILCODE(code, msg) do { \
     dlite_err(code, msg); goto fail; } while (0)
@@ -85,7 +80,6 @@
     dlite_warn(1, msg, a1, a2, a3, a4, a5); goto fail; } while (0)
 
 
-
 /** Debugging messages.  Printed if compiled with WITH_DEBUG */
 #if defined(WITH_DEBUG) && defined(HAVE___VA_ARGS__)
 # define DEBUG_LOG(msg, ...) fprintf(stderr, msg, __VA_ARGS__)
diff --git a/src/dlite-misc.c b/src/dlite-misc.c
index 19b7b3465..1cef30b22 100644
--- a/src/dlite-misc.c
+++ b/src/dlite-misc.c
@@ -12,6 +12,7 @@
 #include "utils/strtob.h"
 #include "utils/tgen.h"
 #include "utils/fileutils.h"
+#include "utils/strutils.h"
 #include "utils/session.h"
 #include "utils/rng.h"
 #include "getuuid.h"
@@ -790,3 +791,50 @@ int dlite_err_set_debug_mode(int mode) { return err_set_debug_mode(mode); }
 int dlite_err_get_debug_mode(void) { return err_get_debug_mode(); }
 int dlite_err_set_override_mode(int mode) {return err_set_override_mode(mode);}
 int dlite_err_get_override_mode(void) { return err_get_override_mode(); }
+
+
+
+/*
+  Issues a deprecation warning.
+
+  `version_removed` is the version the deprecated feature is expected
+  to be finally removed.
+  `descr` is a description of the deprecated feature.
+
+  Returns non-zero on error.
+ */
+int _dlite_deprecation_warning(const char *version_removed,
+                               const char *filepos, const char *func,
+                               const char *descr)
+{
+  static int initialised=0;
+  static map_int_t reported;
+
+  /* Return if this deprecation warning has already been issued. */
+  if (!initialised) {
+    map_init(&reported);
+    initialised = 1;
+  }
+  if (map_get(&reported, filepos)) return 0;
+  map_set(&reported, filepos, 1);
+
+  dlite_warnx("deprecated: %s", descr);
+
+  /* Check that version numbers are semantic.  */
+  if (strchk_semver(version_removed) < 0)
+    return dlite_errx(dliteSystemError,
+                      "argument version_removed=\"%s\" of %s must be a valid "
+                      "semantic version number. In %s",
+                      version_removed, func, filepos);
+  if (strchk_semver(dlite_VERSION) < 0)
+    return dlite_errx(dliteSystemError,
+                      "DLite version number is not semantic '%s'",
+                      dlite_VERSION);
+
+  /* Issue a system error if `version_removed` has passed.  */
+  if (strcmp_semver(version_removed, dlite_VERSION) <= 0)
+    return dlite_errx(dliteSystemError, "Deprecated feature was supposed to be "
+                "removed in version %s: %s", version_removed, descr);
+
+  return 0;
+}
diff --git a/src/dlite-misc.h b/src/dlite-misc.h
index fbfb50c88..68b22c271 100644
--- a/src/dlite-misc.h
+++ b/src/dlite-misc.h
@@ -389,6 +389,24 @@ int dlite_err_set_override_mode(int mode);
 int dlite_err_get_override_mode(void);
 
 
+/*
+  Issues a deprecation warning.
+
+  `version_removed` is the version the deprecated feature is expected
+  to be finally removed.
+  expected to be finally removed.
+  `descr` is a description of the deprecated feature.
+
+  Returns non-zero if `version_removed` has passed.
+ */
+#define dlite_deprecation_warning(version_removed, descr) \
+  _dlite_deprecation_warning(version_removed, ERR_FILEPOS, _err_func, descr)
+
+int _dlite_deprecation_warning(const char *version_removed,
+                               const char *filepos, const char *func,
+                               const char *descr);
+
+
 /** @} */
 
 
diff --git a/src/utils/err.h b/src/utils/err.h
index ffe103c25..f8abc2131 100644
--- a/src/utils/err.h
+++ b/src/utils/err.h
@@ -98,7 +98,7 @@
 #elif defined HAVE___FUNCTION__
 # define _err_func __FUNCTION__
 #else
-# define _err_func
+# define _err_func ""
 #endif
 
 /** Error levels */
diff --git a/src/utils/strutils.c b/src/utils/strutils.c
index 0b3020174..6a9e6567f 100644
--- a/src/utils/strutils.c
+++ b/src/utils/strutils.c
@@ -17,6 +17,10 @@
 #include "strutils.h"
 
 
+#define MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define MAX(a, b) (((a) > (b)) ? (a) : (b))
+
+
 /*
   A convinient variant of asnprintf() that returns the allocated string,
   or NULL on error.
@@ -787,3 +791,167 @@ char *strlst_pop(char **strlst, int i)
   for (j=i; strlst[j]; j++) strlst[j] = strlst[j+1];
   return p;
 }
+
+
+/*
+  A version of atoi() that reads at most `n` bytes.
+
+  Returns zero if `n` is negative.
+ */
+int natoi(const char *s, int n)
+{
+  int x=0, sign=1;
+  while (isspace(*s)) s++, n--;
+  if (*s == '-') {
+    sign = -1;
+    s++, n--;
+  }
+  if (*s == '+') s++;
+  while(isdigit(*s) && n-- > 0) {
+    x = 10*x + (*s - '0');
+    s++;
+  }
+  return sign*x;
+}
+
+
+/*
+  Checks if `v` points to a valid semantic version 2.0.0 number.
+
+  Returns -1 if `v` is not a valid semantic version number.
+  Otherwise, the length of `v` is returned.
+ */
+int strchk_semver(const char *v)
+{
+  int n = strlen(v);
+  int m = strnchk_semver(v, n);
+  if (m != n) return -1;
+  return m;
+}
+
+/*
+  Check if the initial part of `v` is a valid semantic version 2.0.0 number.
+
+  Only the first `n` bytes of `v` are checked.
+
+  Returns the length of the semantic version number or -1 if `v` is
+  not a valid semantic version number.
+*/
+int strnchk_semver(const char *v, size_t n)
+{
+  size_t m=0;
+  /* Major number */
+  if (n < 5) return -1;
+  if (!isdigit(v[m])) return -1;
+  if (v[0] == '0' && isdigit(v[1])) return -1;
+  while (isdigit(v[m]) && m < n) m++;
+  /* Minor number */
+  if (v[m] != '.') return -1;
+  if (m < n) m++;
+  if (!isdigit(v[m])) return -1;
+  if (n >= m+2 && v[m] == '0' && isdigit(v[m+1])) return -1;
+  while (isdigit(v[m]) && m < n) m++;
+  /* Patch number */
+  if (v[m] != '.') return -1;
+  if (m < n) m++;
+  if (!isdigit(v[m])) return -1;
+  if (n >= m+2 && v[m] == '0' && isdigit(v[m+1])) return -1;
+  while (isdigit(v[m]) && m < n) m++;
+  /* Pre-release */
+  if (m == n) return m;
+  if (v[m] == '-') {
+    do {
+      if (n <= ++m) return -1;
+      if (!isalnum(v[m]) && v[m] != '-') return -1;
+      while ((isalnum(v[m]) || v[m] == '-') && m < n) m++;
+    } while (v[m] == '.');
+  }
+  /* Build */
+  if (m == n) return m;
+  if (v[m] == '+') {
+    do {
+      if (n <= ++m ) return -1;
+      if (!isalnum(v[m]) && v[m] != '-') return -1;
+      while ((isalnum(v[m]) || v[m] == '-') && m < n) m++;
+    } while (v[m] == '.');
+  }
+  if (m < n && v[m]) return -1;
+  return m;
+}
+
+
+/*
+  Compare strings `v1` and `v2` using semantic versioning 2.0.0 order.
+
+  Returns
+      -1 if v1 < v2
+       0 if v1 == v2
+       1 if v1 > v2
+  If `v1` or `v2` are not valid semantic version numbers, the return value
+  is undefined.
+
+  See also: https://semver.org/
+ */
+int strcmp_semver(const char *v1, const char *v2)
+{
+  int n1 = strlen(v1);
+  int n2 = strlen(v2);
+  return strncmp_semver(v1, v2, MAX(n1, n2));
+}
+
+
+/*
+  Like strcmp_semver(), but compares only the first `n` bytes of `v1` and `v2`.
+*/
+int strncmp_semver(const char *v1, const char *v2, size_t n)
+{
+  int d, pr1=0, pr2=0;
+  size_t m, n1=0, n2=0;
+  /* Compare major number */
+  d = natoi(v1, n) - natoi(v2, n);
+  if (d) return (d < 0) ? -1 : 1;
+  while (isdigit(v1[n1]) && n1 < n) n1++;
+  while (isdigit(v2[n2]) && n2 < n) n2++;
+  /* Compare minor number */
+  if (n1 < n) n1++;
+  if (n2 < n) n2++;
+  d = natoi(v1+n1, n-n1) - natoi(v2+n2, n-n2);
+  if (d) return (d < 0) ? -1 : 1;
+  while (isdigit(v1[n1]) && n1 < n) n1++;
+  while (isdigit(v2[n2]) && n2 < n) n2++;
+  /* Compare patch number */
+  if (n1 < n) n1++;
+  if (n2 < n) n2++;
+  d = natoi(v1+n1, n-n1) - natoi(v2+n2, n-n2);
+  if (d) return (d < 0) ? -1 : 1;
+  while (isdigit(v1[n1]) && n1 < n) n1++;
+  while (isdigit(v2[n2]) && n2 < n) n2++;
+  assert(n1 == n2);
+  m = n1;
+  /* Compare pre-release */
+  if (m > n) return 0;
+  if (v1[m] == '-') pr1 = 1;
+  if (v2[m] == '-') pr2 = 1;
+  if (!pr1 && pr2) return -1;
+  if (pr1 && !pr2) return 1;
+  if (pr1 && pr2) {
+    do {
+      size_t d1, d2;
+      if (m < n) m++;
+      for (n1=m; (isalnum(v1[n1]) || v1[n1] == '-') && n1 < n; n1++);
+      for (n2=m; (isalnum(v2[n2]) || v2[n2] == '-') && n2 < n; n2++);
+      for (d1=m; (isdigit(v1[d1])) && d1 < n; d1++);
+      for (d2=m; (isdigit(v2[d2])) && d2 < n; d2++);
+      if (d1 == n1 && d2 == n2) {
+        d = natoi(v1+m, n-m) - natoi(v2+m, n-m);
+      } else {
+        d = strncmp(v1+m, v2+m, MIN(n1, n2)-m);
+        if (d == 0) d = n1 - n2;
+      }
+      if (d) return (d < 0) ? -1 : 1;
+      assert(n1 == n2);
+      m = n1;
+    } while (v1[n1] == '.' && v2[n2] == '.');
+  }
+  return 0;
+}
diff --git a/src/utils/strutils.h b/src/utils/strutils.h
index 040931d08..bfce51a34 100644
--- a/src/utils/strutils.h
+++ b/src/utils/strutils.h
@@ -342,6 +342,45 @@ int strlst_remove(char **strlst, int i);
 */
 char *strlst_pop(char **strlst, int i);
 
+/**
+  A version of atoi() that reads at most `n` bytes.
+ */
+int natoi(const char *s, int n);
+
+/**
+  Checks if `v` points to a valid semantic version 2.0.0 number.
+
+  Returns -1 if `v` is not a valid semantic version number.
+  Otherwise, the length of the version number is returned.
+ */
+int strchk_semver(const char *v);
+
+/**
+  Check if the initial part of `v` is a valid semantic version 2.0.0 number.
+
+  Only the first `n` bytes of `v` are checked.
+
+  Returns the length of the semantic version number or -1 if `v` is
+  not a valid semantic version number.
+*/
+int strnchk_semver(const char *v, size_t n);
+
+/**
+  Compare strings `v1` and `v2` using semantic versioning 2.0.0 order.
+
+  Returns -1 if v1 < v2
+           0 if v1 == v2
+           1 if v1 > v2
+
+  See also: https://semver.org/
+ */
+int strcmp_semver(const char *v1, const char *v2);
+
+/**
+  Like strcmp_version(), but compares only the first `n` bytes of `v1` and `v2`.
+*/
+int strncmp_semver(const char *v1, const char *v2, size_t n);
+
 
 /** @} */
 


Test patch:
diff --git a/bindings/python/tests/test_misc.py b/bindings/python/tests/test_misc.py
index 95508ade7..ebe40630b 100755
--- a/bindings/python/tests/test_misc.py
+++ b/bindings/python/tests/test_misc.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 import dlite
+from dlite.testutils import raises
 
 
 assert dlite.get_uuid_version() == 4
@@ -63,3 +64,32 @@
         dlite.Instance.from_location("-", "__non-existing__")
 except dlite.DLiteStorageOpenError:
     pass
+
+
+# Test checking and comparing semantic version numbers
+assert dlite.chk_semver(dlite.__version__) > 0
+assert dlite.chk_semver("1.12.2-rc1.alpha+34") == 19
+assert dlite.chk_semver("1.12.2-rc1.alpha+34 ") < 0
+assert dlite.chk_semver("1.12.2-rc1.alpha+34 ", 6) == 6
+assert dlite.chk_semver("1.12.2-rc1.alpha+34 ", 19) == 19
+
+assert dlite.cmp_semver("1.3.11", "1.3.3") > 0
+assert dlite.cmp_semver("1.3.11", "1.3.13") < 0
+assert dlite.cmp_semver("1.3.11", "1.3.13", 5) == 0
+
+
+# Test deprecation warnings
+dlite.deprecation_warning("100.3.2", "My deprecated feature...")
+dlite.deprecation_warning("100.3.2", "My deprecated feature...")
+dlite.deprecation_warning("100.3.2", "My deprecated feature...")
+
+with raises(SystemError):
+    dlite.deprecation_warning("0.0.1", "My deprecated feature 2...")
+
+# Issuing the same deprecation warning a second or third time should not
+# raise an exception
+dlite.deprecation_warning("0.0.1", "My deprecated feature 2...")
+dlite.deprecation_warning("0.0.1", "My deprecated feature 2...")
+
+with raises(SystemError):
+    dlite.deprecation_warning("0.0.x", "My deprecated feature 3...")
diff --git a/src/tests/test_misc.c b/src/tests/test_misc.c
index de7ad95a5..be39d3e6f 100644
--- a/src/tests/test_misc.c
+++ b/src/tests/test_misc.c
@@ -158,18 +158,45 @@ MU_TEST(test_split_url)
   mu_assert_string_eq(NULL, options);
 }
 
+int _deprecated_call(void) {
+  err_clear();
+  printf("\n");
+  return dlite_deprecation_warning("100.0.1", "my old feature");
+}
+
+MU_TEST(test_deprecation_warning)
+{
+  /* Warning message should only be shown once */
+  mu_assert_int_eq(0, _deprecated_call());
+  mu_assert_int_eq(0, _deprecated_call());
+  mu_assert_int_eq(0, _deprecated_call());
+
+  err_clear();
+  printf("\n");
+  mu_assert_int_eq(dliteSystemError,
+                   dlite_deprecation_warning("0.0.1", "my old feature 2"));
+
+  err_clear();
+  printf("\n");
+  mu_assert_int_eq(dliteSystemError,
+                   dlite_deprecation_warning("0.1.x", "my old feature 3"));
+}
+
 
 /***********************************************************************/
 
 
 MU_TEST_SUITE(test_suite)
 {
+  dlite_init();
+
   MU_RUN_TEST(test_get_uuid);
   MU_RUN_TEST(test_get_uuidn);
   MU_RUN_TEST(test_join_split_metadata);
   MU_RUN_TEST(test_option_parse);
   MU_RUN_TEST(test_join_url);
   MU_RUN_TEST(test_split_url);
+  MU_RUN_TEST(test_deprecation_warning);
 }
 
 int main()
diff --git a/src/utils/tests/test_strutils.c b/src/utils/tests/test_strutils.c
index 04b70111b..5935eadaf 100644
--- a/src/utils/tests/test_strutils.c
+++ b/src/utils/tests/test_strutils.c
@@ -445,6 +445,98 @@ MU_TEST(test_strlst)
 }
 
 
+MU_TEST(test_natoi)
+{
+  mu_assert_int_eq(1,  natoi("1", 10));
+  mu_assert_int_eq(2,  natoi("2 ", 10));
+  mu_assert_int_eq(3,  natoi(" 3", 10));
+  mu_assert_int_eq(1,  natoi("+1", 10));
+  mu_assert_int_eq(-2, natoi("-2", 10));
+  mu_assert_int_eq(0,  natoi("*2", 10));
+  mu_assert_int_eq(12, natoi("012345", 3));
+  mu_assert_int_eq(0,  natoi("012345", 0));
+  mu_assert_int_eq(0,  natoi("012345", -3));
+}
+
+
+MU_TEST(test_strchk_semver)
+{
+  mu_assert_int_eq(5,  strchk_semver("1.0.0"));
+  mu_assert_int_eq(-1, strchk_semver(" 1.0.0"));
+  mu_assert_int_eq(-1, strchk_semver("1.0.0 "));
+  mu_assert_int_eq(-1, strchk_semver("1"));
+  mu_assert_int_eq(-1, strchk_semver("1."));
+  mu_assert_int_eq(-1, strchk_semver("1.0"));
+  mu_assert_int_eq(-1, strchk_semver("1.0."));
+  mu_assert_int_eq(-1, strchk_semver("v1.0.0"));
+  mu_assert_int_eq(-1, strchk_semver("1.0.0-"));
+  mu_assert_int_eq(-1, strchk_semver("1.0.0x"));
+  mu_assert_int_eq(-1, strchk_semver("1.0.0 "));
+  mu_assert_int_eq(11, strchk_semver("5.12.17-rc1"));
+  mu_assert_int_eq(-1, strchk_semver("5.12.17-rc1 "));
+  mu_assert_int_eq(-1, strchk_semver("5.12.17-rc1."));
+  mu_assert_int_eq(13, strchk_semver("5.12.17-rc1.x"));
+  mu_assert_int_eq(-1, strchk_semver("5.12.17-rc1..x"));
+  mu_assert_int_eq(11, strchk_semver("5.12.17+001"));
+  mu_assert_int_eq(15, strchk_semver("5.12.17-rc1+001"));
+  mu_assert_int_eq(-1, strchk_semver("5.12.17-rc1+001 "));
+}
+
+
+MU_TEST(test_strnchk_semver)
+{
+  mu_assert_int_eq(5,  strnchk_semver("1.0.0", 10));
+  mu_assert_int_eq(5,  strnchk_semver("1.0.0", 10));
+  mu_assert_int_eq(5,  strnchk_semver("1.0.0", 5));
+  mu_assert_int_eq(-1, strnchk_semver("1.0.0", 4));
+  mu_assert_int_eq(5,  strnchk_semver("1.0.0 ", 5));
+  mu_assert_int_eq(-1, strnchk_semver("1.0.0 ", 6));
+  mu_assert_int_eq(-1, strnchk_semver(" 1.0.0", 6));
+  mu_assert_int_eq(15, strnchk_semver("5.12.17-rc1+001", 20));
+  mu_assert_int_eq(6,  strnchk_semver("5.12.17-rc1+001 ", 6));
+  mu_assert_int_eq(7,  strnchk_semver("5.12.17-rc1+001 ", 7));
+  mu_assert_int_eq(-1, strnchk_semver("5.12.17-rc1+001 ", 8));
+  mu_assert_int_eq(9,  strnchk_semver("5.12.17-rc1+001 ", 9));
+  mu_assert_int_eq(11, strnchk_semver("5.12.17-rc1+001 ", 11));
+  mu_assert_int_eq(-1, strnchk_semver("5.12.17-rc1+001 ", 12));
+  mu_assert_int_eq(13, strnchk_semver("5.12.17-rc1+001 ", 13));
+  mu_assert_int_eq(15, strnchk_semver("5.12.17-rc1+001 ", 15));
+  mu_assert_int_eq(-1, strnchk_semver("5.12.17-rc1+001 ", 16));
+  mu_assert_int_eq(-1, strnchk_semver("5.12.17-rc1+001 ", 20));
+}
+
+
+MU_TEST(test_strcmp_semver)
+{
+  mu_assert_int_eq(0,  strcmp_semver("0.1.1", "0.1.1"));
+  mu_assert_int_eq(-1, strcmp_semver("0.1.2", "0.1.11"));
+  mu_assert_int_eq(-1, strcmp_semver("0.1.2", "0.2.11"));
+  mu_assert_int_eq(1,  strcmp_semver("0.5.2", "0.2.11"));
+  mu_assert_int_eq(-1, strcmp_semver("5.12.17", "5.12.17-rc1+001"));
+  mu_assert_int_eq(0,  strcmp_semver("5.12.17", "5.12.17+001"));
+  mu_assert_int_eq(1,  strcmp_semver("5.12.17", "5.11.17+001"));
+  mu_assert_int_eq(1,  strcmp_semver("5.12.17-rc1.12", "5.12.17-rc1.4"));
+}
+
+
+MU_TEST(test_strncmp_semver)
+{
+  mu_assert_int_eq(0,  strncmp_semver("0.1.1", "0.1.1", 5));
+  mu_assert_int_eq(0,  strncmp_semver("0.1.1", "0.1.1", 6));
+  mu_assert_int_eq(0,  strncmp_semver("0.1.1", "0.1.12", 5));
+  mu_assert_int_eq(-1, strncmp_semver("0.1.1", "0.1.12", 6));
+  mu_assert_int_eq(1,  strncmp_semver("0.1.2", "0.1.11", 5));
+  mu_assert_int_eq(-1, strncmp_semver("0.1.2", "0.1.11", 6));
+  mu_assert_int_eq(0,  strncmp_semver("5.12.17", "5.12.17-rc1+001", 6));
+  mu_assert_int_eq(-1, strncmp_semver("5.12.17", "5.12.17-rc1+001", 7));
+  mu_assert_int_eq(-1, strncmp_semver("5.12.17", "5.12.17-rc1+001", 9));
+  mu_assert_int_eq(-1, strncmp_semver("5.12.17", "5.12.17-rc1+001", 20));
+  mu_assert_int_eq(0,  strncmp_semver("5.12.17", "5.12.17+001", 7));
+  mu_assert_int_eq(0,  strncmp_semver("5.12.17", "5.12.17+001", 9));
+  mu_assert_int_eq(0,  strncmp_semver("5.12.17", "5.12.17+001", 20));
+}
+
+
 /***********************************************************************/
 
 MU_TEST_SUITE(test_suite)
@@ -462,6 +554,11 @@ MU_TEST_SUITE(test_suite)
   MU_RUN_TEST(test_strcategory);
   MU_RUN_TEST(test_strcatspn);
   MU_RUN_TEST(test_strlst);
+  MU_RUN_TEST(test_natoi);
+  MU_RUN_TEST(test_strchk_semver);
+  MU_RUN_TEST(test_strnchk_semver);
+  MU_RUN_TEST(test_strcmp_semver);
+  MU_RUN_TEST(test_strncmp_semver);
 }