[lxc-devel] [pylxd/master] Add PYLXD_WARNINGS env variable to be able to supress warnings

ajkavanagh on Github lxc-bot at linuxcontainers.org
Fri May 3 14:12:43 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 858 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190503/28a76bb3/attachment.bin>
-------------- next part --------------
From 6ba85dc5ca1db19cef7daaab08938888c008c77b Mon Sep 17 00:00:00 2001
From: Alex Kavanagh <alex.kavanagh at canonical.com>
Date: Fri, 3 May 2019 15:10:16 +0100
Subject: [PATCH] Add PYLXD_WARNINGS env variable to be able to supress
 warnings

If the LXD server that pylxd is connected to supports attributes on
objects that pylxd doesn't yet support, then a warning is issued using
the `warnings` module.  This can fill logs with annoying warnings.  So
this patch adds the ability to set an env variable PYLXD_WARNINGS to
'none' to suppress all the warnings, or to 'always' to get the existing
behaviour.  The new behavior is to issue a warning once for each
instance of an attribute that isn't known for each object.

Closes: #301
Signed-off-by: Alex Kavanagh <alex.kavanagh at canonical.com>
---
 doc/source/usage.rst                 | 16 ++++++++++++++
 pylxd/models/_model.py               | 21 ++++++++++++++++++
 pylxd/models/operation.py            | 22 ++++++++++++++++++-
 pylxd/tests/models/test_model.py     | 33 ++++++++++++++++++++++++++++
 pylxd/tests/models/test_operation.py | 31 ++++++++++++++++++++++++++
 tox.ini                              |  1 +
 6 files changed, 123 insertions(+), 1 deletion(-)

diff --git a/doc/source/usage.rst b/doc/source/usage.rst
index 109071d4..1d2b938b 100644
--- a/doc/source/usage.rst
+++ b/doc/source/usage.rst
@@ -101,3 +101,19 @@ Some changes to LXD will return immediately, but actually occur in the
 background after the http response returns. All operations that happen
 this way will also take an optional `wait` parameter that, when `True`,
 will not return until the operation is completed.
+
+UserWarning: Attempted to set unknown attribute "x" on instance of "y"
+----------------------------------------------------------------------
+
+The LXD server changes frequently, particularly if it is snap installed.  In
+this case it is possible that the LXD server may send back objects with
+attributes that this version of pylxd is not aware of, and in that situation,
+the pylxd library issues the warning above.
+
+The default behaviour is that *one* warning is issued for each unknown
+attribute on *each* object class that it unknown.  Further warnings are then
+surpressed.  The environment variable ``PYLXD_WARNINGS`` can be set to control
+the warnings further:
+
+  - if set to ``none`` then *all* warnings are surpressed all the time.
+  - if set to ``always`` then warnings are always issued for each instance returned from the server.
diff --git a/pylxd/models/_model.py b/pylxd/models/_model.py
index 83fd0673..62887446 100644
--- a/pylxd/models/_model.py
+++ b/pylxd/models/_model.py
@@ -11,6 +11,7 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import os
 import warnings
 
 import six
@@ -83,6 +84,11 @@ def __new__(cls, name, bases, attrs):
         return super(ModelType, cls).__new__(cls, name, bases, attrs)
 
 
+# Global used to record which warnings have been issues already for unknown
+# attributes.
+_seen_attribute_warnings = set()
+
+
 @six.add_metaclass(ModelType)
 class Model(object):
     """A Base LXD object model.
@@ -98,6 +104,13 @@ class Model(object):
     un-initialized attributes are read. When attributes are modified,
     the instance is marked as dirty. `save` will save the changes
     to the server.
+
+    If the LXD server sends attributes that this version of pylxd is unaware of
+    then a warning is printed.  By default the warning is issued ONCE and then
+    supressed for every subsequent attempted setting.  The warnings can be
+    completely suppressed by setting the environment variable PYLXD_WARNINGS to
+    'none', or always displayed by setting the PYLXD_WARNINGS variable to
+    'always'.
     """
     NotFound = exceptions.NotFound
     __slots__ = ['client', '__dirty__']
@@ -110,6 +123,14 @@ def __init__(self, client, **kwargs):
             try:
                 setattr(self, key, val)
             except AttributeError:
+                global _seen_attribute_warnings
+                env = os.environ.get('PYLXD_WARNINGS', '').lower()
+                item = "{}.{}".format(self.__class__.__name__, key)
+                if env != 'always' and item in _seen_attribute_warnings:
+                    continue
+                _seen_attribute_warnings.add(item)
+                if env == 'none':
+                    continue
                 warnings.warn(
                     'Attempted to set unknown attribute "{}" '
                     'on instance of "{}"'.format(
diff --git a/pylxd/models/operation.py b/pylxd/models/operation.py
index a36df32c..ac094f50 100644
--- a/pylxd/models/operation.py
+++ b/pylxd/models/operation.py
@@ -19,8 +19,21 @@
 from six.moves.urllib import parse
 
 
+# Global used to record which warnings have been issues already for unknown
+# attributes.
+_seen_attribute_warnings = set()
+
+
 class Operation(object):
-    """A LXD operation."""
+    """An LXD operation.
+
+    If the LXD server sends attributes that this version of pylxd is unaware of
+    then a warning is printed.  By default the warning is issued ONCE and then
+    supressed for every subsequent attempted setting.  The warnings can be
+    completely suppressed by setting the environment variable PYLXD_WARNINGS to
+    'none', or always displayed by setting the PYLXD_WARNINGS variable to
+    'always'.
+    """
 
     __slots__ = [
         '_client',
@@ -53,6 +66,13 @@ def __init__(self, **kwargs):
             except AttributeError:
                 # ignore attributes we don't know about -- prevent breakage
                 # in the future if new attributes are added.
+                global _seen_attribute_warnings
+                env = os.environ.get('PYLXD_WARNINGS', '').lower()
+                if env != 'always' and key in _seen_attribute_warnings:
+                    continue
+                _seen_attribute_warnings.add(key)
+                if env == 'none':
+                    continue
                 warnings.warn(
                     'Attempted to set unknown attribute "{}" '
                     'on instance of "{}"'
diff --git a/pylxd/tests/models/test_model.py b/pylxd/tests/models/test_model.py
index 9aa1a4f3..244df0d3 100644
--- a/pylxd/tests/models/test_model.py
+++ b/pylxd/tests/models/test_model.py
@@ -11,6 +11,8 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
+import mock
+
 from pylxd.models import _model as model
 from pylxd.tests import testing
 
@@ -76,6 +78,37 @@ def test_init(self):
         self.assertEqual(self.client, item.client)
         self.assertEqual('an-item', item.name)
 
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': ''})
+    @mock.patch('warnings.warn')
+    def test_init_warnings_once(self, mock_warn):
+        with mock.patch('pylxd.models._model._seen_attribute_warnings',
+                        new=set()):
+            Item(self.client, unknown='some_value')
+            mock_warn.assert_called_once_with(mock.ANY)
+            Item(self.client, unknown='some_value_as_well')
+            mock_warn.assert_called_once_with(mock.ANY)
+            Item(self.client, unknown2="some_2nd_value")
+            self.assertEqual(len(mock_warn.call_args_list), 2)
+
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': 'none'})
+    @mock.patch('warnings.warn')
+    def test_init_warnings_none(self, mock_warn):
+        with mock.patch('pylxd.models._model._seen_attribute_warnings',
+                        new=set()):
+            Item(self.client, unknown='some_value')
+            mock_warn.assert_not_called()
+
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': 'always'})
+    @mock.patch('warnings.warn')
+    def test_init_warnings_always(self, mock_warn):
+        with mock.patch('pylxd.models._model._seen_attribute_warnings',
+                        new=set()):
+            Item(self.client, unknown='some_value')
+            mock_warn.assert_called_once_with(mock.ANY)
+            Item(self.client, unknown='some_value_as_well')
+            self.assertEqual(len(mock_warn.call_args_list), 2)
+
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': 'none'})
     def test_init_unknown_attribute(self):
         """Unknown attributes aren't set."""
         item = Item(self.client, name='an-item', nonexistent='SRSLY')
diff --git a/pylxd/tests/models/test_operation.py b/pylxd/tests/models/test_operation.py
index 1e70e869..83790b06 100644
--- a/pylxd/tests/models/test_operation.py
+++ b/pylxd/tests/models/test_operation.py
@@ -13,6 +13,7 @@
 #    under the License.
 
 import json
+import mock
 
 from pylxd import exceptions, models
 from pylxd.tests import testing
@@ -21,6 +22,36 @@
 class TestOperation(testing.PyLXDTestCase):
     """Tests for pylxd.models.Operation."""
 
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': ''})
+    @mock.patch('warnings.warn')
+    def test_init_warnings_once(self, mock_warn):
+        with mock.patch('pylxd.models.operation._seen_attribute_warnings',
+                        new=set()):
+            models.Operation(unknown='some_value')
+            mock_warn.assert_called_once_with(mock.ANY)
+            models.Operation(unknown='some_value_as_well')
+            mock_warn.assert_called_once_with(mock.ANY)
+            models.Operation(unknown2="some_2nd_value")
+            self.assertEqual(len(mock_warn.call_args_list), 2)
+
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': 'none'})
+    @mock.patch('warnings.warn')
+    def test_init_warnings_none(self, mock_warn):
+        with mock.patch('pylxd.models.operation._seen_attribute_warnings',
+                        new=set()):
+            models.Operation(unknown='some_value')
+            mock_warn.assert_not_called()
+
+    @mock.patch.dict('os.environ', {'PYLXD_WARNINGS': 'always'})
+    @mock.patch('warnings.warn')
+    def test_init_warnings_always(self, mock_warn):
+        with mock.patch('pylxd.models.operation._seen_attribute_warnings',
+                        new=set()):
+            models.Operation(unknown='some_value')
+            mock_warn.assert_called_once_with(mock.ANY)
+            models.Operation(unknown='some_value_as_well')
+            self.assertEqual(len(mock_warn.call_args_list), 2)
+
     def test_get(self):
         """Return an operation."""
         name = 'operation-abc'
diff --git a/tox.ini b/tox.ini
index 7f460636..3dbff000 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,6 +8,7 @@ usedevelop = True
 install_command = pip install -U {opts} {packages}
 setenv =
    VIRTUAL_ENV={envdir}
+   PYLXD_WARNINGS=none
 deps = -r{toxinidir}/requirements.txt
        -r{toxinidir}/test-requirements.txt
 commands = nosetests --with-coverage --cover-package=pylxd pylxd


More information about the lxc-devel mailing list