Skip to content

Commit

Permalink
Merge pull request #361 from ajkavanagh/issue/301/supress-attribute-w…
Browse files Browse the repository at this point in the history
…arnings

Add PYLXD_WARNINGS env variable to be able to supress warnings
  • Loading branch information
ChrisMacNaughton authored May 8, 2019
2 parents 8f578fb + 6ba85dc commit 816da55
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 1 deletion.
16 changes: 16 additions & 0 deletions doc/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
21 changes: 21 additions & 0 deletions pylxd/models/_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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__']
Expand All @@ -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(
Expand Down
22 changes: 21 additions & 1 deletion pylxd/models/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 "{}"'
Expand Down
33 changes: 33 additions & 0 deletions pylxd/tests/models/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down
31 changes: 31 additions & 0 deletions pylxd/tests/models/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# under the License.

import json
import mock

from pylxd import exceptions, models
from pylxd.tests import testing
Expand All @@ -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'
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 816da55

Please sign in to comment.