Repository: google/openhtf

Issue:
Plug logger raises AttributeError when called in constructor
plug logger raises AttributeError when called in constructor

added logging statement to example_plug constructor:

 @conf.InjectPositionalArgs
  def **init**(self, example_plug_increment):
    self.logger.info('init')
    self.increment = example_plug_increment
    self.value = 0

AttributeError: 'ExamplePlug' object has no attribute 'logger'
Exception in thread TestExecutorThread:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 810, in **bootstrap_inner
    self.run()
  File "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/util/threads.py", line 59, in run
    self._ThreadProc()
  File "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/exe/__init**.py", line 124, in _ThreadProc
    self.test_state.plug_manager.InitializePlugs()
  File "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/plugs/__init__.py", line 373, in InitializePlugs
    plug_instance = plug_type()
  File "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/conf.py", line 562, in SelfWrapper
    return method_wrapper(**kwargs)
  File "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/conf.py", line 553, in method_wrapper
    return method(**final_kwargs)
  File "/home/openhtf/examples/example_plug.py", line 70, in **init**
    self.logger.info('init')
AttributeError: 'ExamplePlug' object has no attribute 'logger



Discussion:
You have to call the super() constructor first, then the logger is
available.

On Aug 16, 2016 10:27 PM, "wallacbe" notifications@github.com wrote:

> plug logger raises AttributeError when called in constructor
> 
> added logging statement to example_plug constructor:
> 
> @conf.InjectPositionalArgs
> def _init_(self, example_plug_increment):
> self.logger.info('init')
> self.increment = example_plug_increment
> self.value = 0
> 
> AttributeError: 'ExamplePlug' object has no attribute 'logger'
> Exception in thread TestExecutorThread:
> Traceback (most recent call last):
> File "/usr/lib/python2.7/threading.py", line 810, in
> 
> _bootstrap_inner self.run() File
> "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/util/threads.py",
> line 59, in run self._ThreadProc() File
> "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/exe/__init_.py",
> line 124, in
> 
> _ThreadProc self.test_state.plug_manager.InitializePlugs() File
> "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/plugs/__init__.py",
> line 373, in InitializePlugs
> plug_instance = plug_type()
> File "/home/source/venv/local/lib/python2.7/site-packages/
> openhtf-0.9.2-py2.7.egg/openhtf/conf.py", line 562, in SelfWrapper
> return method_wrapper(
> 
> _kwargs) File
> "/home/source/venv/local/lib/python2.7/site-packages/openhtf-0.9.2-py2.7.egg/openhtf/conf.py",
> line 553, in method_wrapper return method(_final_kwargs)
> File "/home/openhtf/examples/example_plug.py", line 70, in _init_
> self.logger.info('init')
> AttributeError: 'ExamplePlug' object has no attribute 'logger
> 
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> https://github.com/google/openhtf/issues/403, or mute the thread
> https://github.com/notifications/unsubscribe-auth/AASrtBPFlkr07kjbulZrt6PFwqVA8ss3ks5qgpuxgaJpZM4JmGqm
> .

I added super and still observe AttributeError: 'ExamplePlug' object has no attribute 'logger

@conf.InjectPositionalArgs
  def **init**(self, example_plug_increment):
    super(self.**class**, self).**init**()
    self.logger.info('init')
    self.increment = example_plug_increment
    self.value = 0

Oh, I didn't check the code. Looks like madsci dropped it in this commit:
https://github.com/google/openhtf/commit/031b14907f87f6c09d91db9889a3ec9d13ca1874
Looks like you added it the first time. Want to add it back?

On Wed, Aug 17, 2016 10:17 PM, wallacbe notifications@github.com wrote:
I added super and still observe AttributeError: 'ExamplePlug' object has no
attribute 'logger

@conf.InjectPositionalArgs
def init (self, example_plug_increment):
super(self. class , self). init ()
self.logger.info('init')
self.increment = example_plug_increment
self.value = 0

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub , or mute the thread .

The logger isn't accessible in `__init__` because the `PlugManager` sets it.  I considered having the `PlugManager` pass it into `__init__`, but that forces all plug implementations to accept it as an arg, whether they use it in `__init__` or not.

I'd be okay with doing something like a try-except block where you  try to pass a `logger=` argument to the plug's `__init__` and if it raises you simply don't pass it in.  This would allow you to take a `logger` arg in `__init__` and access it directly there.

I'd rather avoid the try-except because you might get that error due to some other code and swallow-retry and really confuse folks. Why not use `inspect` to check the args like we do with phases?

try-except is the Python-recommended way of doing something like this, surprisingly ([EAFP](https://docs.python.org/2/glossary.html#term-eafp)).  catching the right exception (TypeError, I believe?) should be specific enough.  this is, for example, how `hasattr` is implemented.

Yeah, and `hasattr()` is confusing when you have an `@property` that fails with
AttributeError but it exists. Such as this:

```py
class A(object):
  @property
  def fails_on_hasattr(self):
    return self.x
hasattr(A(), 'fails_on_hasattr') == False
```

Except you look at the code and sure enough, A has `fails_on_hasattr` but
`hasattr()` says it doesn't! I'd expect `hasattr()` to be True, but `getattr()`
to fail, but that's just me and not the CPython writers, apparently.


What's the preferred solution?  Do we have to pass a different logger object (other than openhtf.plugs) to **init** to get the log statement to appear?  The logging statement is useful since it shows when plug **init** occurs w.r.t. starting a test.

 def **init**(self, plug_types=None, logger=None):
    self._plug_types = plug_types or set()
    self._logger = logger

You need to use the logger that's held in the PlugManager if you want the log lines to show up in the 'TestRecord'; output.  For debugging/understanding openhtf flow, you can use any old logger.


Resolving patch:
diff --git a/openhtf/plugs/__init__.py b/openhtf/plugs/__init__.py
--- a/openhtf/plugs/__init__.py
+++ b/openhtf/plugs/__init__.py
@@ -171,6 +171,10 @@ class BasePlug(object):
   enable_remote = False
   # Allow explicitly disabling remote access to specific attributes.
   disable_remote_attrs = set()
+  # Default logger to be used only in __init__ of subclasses..
+  # This is overwritten both on the class and the instance so don't store
+  # a copy of it anywhere.
+  logger = _LOG
 
   @classproperty
   def placeholder(cls):
@@ -416,15 +420,25 @@ def initialize_plugs(self, plug_types=None):
         if not issubclass(plug_type, BasePlug):
           raise InvalidPlugError(
               'Plug type "%s" is not an instance of BasePlug' % plug_type)
+        if plug_type.logger != _LOG:
+          # They put a logger attribute on the class itself, overriding ours.
+          raise InvalidPlugError(
+              'Do not override "logger" in your plugs.', plug_type)
+
+        # Override the logger so that __init__'s logging goes into the record.
+        plug_type.logger = self._logger
         plug_instance = plug_type()
+        # Now set it back since we'll give the instance a logger in a moment.
+        plug_type.logger = _LOG
         # Set the logger attribute directly (rather than in BasePlug) so we
         # don't depend on subclasses' implementation of __init__ to have it
         # set.
-        if hasattr(plug_instance, 'logger'):
+        if plug_instance.logger != _LOG:
           raise InvalidPlugError(
-              'Plugs must not override the logger attribute.', plug_type)
+              'Do not set "self.logger" in __init__ in your plugs', plug_type)
         else:
-          setattr(plug_instance, 'logger', self._logger)
+          # Now the instance has its own copy of the test logger.
+          plug_instance.logger = self._logger
       except Exception:  # pylint: disable=broad-except
         _LOG.error('Exception instantiating plug type %s', plug_type)
         self.tear_down_plugs()


Test patch:
diff --git a/test/plugs_test.py b/test/plugs_test.py
--- a/test/plugs_test.py
+++ b/test/plugs_test.py
@@ -106,6 +106,24 @@ def dummy_phase(test_api, adder_plug, other_plug):
       self.assertIs(AdderPlug.LAST_INSTANCE, adder_plug)
     yield dummy_phase
 
+  @test.yields_phases
+  def test_plug_logging(self):
+    """Test that both __init__ and other functions get the good logger."""
+    class LoggingPlug(plugs.BasePlug):
+      def __init__(self):
+        self.logger_seen_init = self.logger
+
+      def action(self):
+        self.logger_seen_action = self.logger
+
+    @plugs.plug(logger=LoggingPlug)
+    def dummy_phase(test_api, logger):
+      logger.action()
+      self.assertIs(logger.logger_seen_init, logger.logger_seen_action)
+      self.assertIs(logger.logger_seen_init, self.logger)
+
+    yield dummy_phase
+
   def test_tear_down_raises(self):
     """Test that all plugs get torn down even if some raise."""
     self.plug_manager.initialize_plugs({
@@ -140,6 +158,11 @@ def test_invalid_plug(self):
     with self.assertRaises(plugs.InvalidPlugError):
       self.plug_manager.initialize_plugs({
           type('BadPlug', (plugs.BasePlug,), {'logger': None})})
+    with self.assertRaises(plugs.InvalidPlugError):
+      class BadPlugInit(plugs.BasePlug):
+        def __init__(self):
+          self.logger = None
+      self.plug_manager.initialize_plugs({BadPlugInit})
     with self.assertRaises(plugs.InvalidPlugError):
       self.plug_manager.wait_for_plug_update('invalid', {}, 0)