Repository: nvaccess/nvda

Issue:
Infinite recursion when logging from a property
STR:
1. Open the NVDA Python console.
2. Type the following:
   
   ```
   class C(object):
     @property
       def p(self):
         log.info("p", stack_info=True)
   
   C().p
   ```
3. Look at the log.

You get something like this:

```
  File "<console>", line 4, in p
  File "C:\Python27\lib\logging\__init__.py", line 1159, in info
    self._log(INFO, msg, args, **kwargs)
  File "logHandler.py", line 91, in _log
    codepath=getCodePath(f)
  File "logHandler.py", line 55, in getCodePath
    attr=getattr(arg0,funcName)
  File "<console>", line 4, in p
  File "C:\Python27\lib\logging\__init__.py", line 1159, in info
    self._log(INFO, msg, args, **kwargs)
  File "logHandler.py", line 91, in _log
    codepath=getCodePath(f)
  File "logHandler.py", line 55, in getCodePath
    attr=getattr(arg0,funcName)
  File "<console>", line 4, in p
  File "C:\Python27\lib\logging\__init__.py", line 1159, in info
    self._log(INFO, msg, args, **kwargs)
  File "logHandler.py", line 91, in _log
    codepath=getCodePath(f)
  File "logHandler.py", line 55, in getCodePath
    attr=getattr(arg0,funcName)
...
```

And so the recursion continues. The reason is that `logHandler.getCodePath` retrieves the `p` attribute from the class, but in this case, the attribute is a property. Retrieving the property runs the logging code again... and thus begins the infinite recursion. I'm pretty sure we exceed maximum recursion depth, but `getCodePath` catches that exception.

As to why we aren't hitting this all the time, I think this only applies to properties which use decorators. In contrast, most of our properties use the `_get_foo` magic methods, and in that case, the function name from the code attribute will point at the `_get_foo` method, not the `foo` property itself. So, we've just been lucky enough not to log within a decorated property.

We should be able to solve this by trying to retrieve the attribute from the class first. If that attribute has a `__get__` attribute, we know it's a descriptor (usually a property). If so, we shouldn't try to retrieve the attribute from the instance.



Discussion:
[Originally reported in this nvda-devel thread](http://nabble.nvda-project.org/Building-NVDA-from-source-nvda-exe-is-missing-tp40536p40547.html) by @leonardder.


Resolving patch:
diff --git a/source/logHandler.py b/source/logHandler.py
--- a/source/logHandler.py
+++ b/source/logHandler.py
@@ -10,7 +10,7 @@
 import inspect
 import winsound
 import traceback
-from types import MethodType
+from types import MethodType, FunctionType
 import globalVars
 
 ERROR_INVALID_WINDOW_HANDLE = 1400
@@ -51,12 +51,33 @@ def getCodePath(f):
 	#Code borrowed from http://mail.python.org/pipermail/python-list/2000-January/020141.html
 	if f.f_code.co_argcount:
 		arg0=f.f_locals[f.f_code.co_varnames[0]]
-		try:
-			attr=getattr(arg0,funcName)
-		except:
-			attr=None
-		if attr and type(attr) is MethodType and attr.im_func.func_code is f.f_code:
-			className=arg0.__class__.__name__
+		# #6122: Check if this function is a member of its first argument's class (and specifically which base class if any) 
+		# Rather than an instance member of its first argument.
+		# This stops infinite recursions if fetching data descriptors,
+		# And better reflects the actual source code definition.
+		topCls=arg0 if isinstance(arg0,type) else type(arg0)
+		# find the deepest class this function's name is reachable as a method from
+		if hasattr(topCls,funcName):
+			for cls in topCls.__mro__:
+				member=cls.__dict__.get(funcName)
+				if not member:
+					continue
+				memberType=type(member)
+				if memberType is FunctionType and member.func_code is f.f_code:
+					# the function was found as a standard method
+					className=cls.__name__
+				elif memberType is classmethod and type(member.__func__) is FunctionType and member.__func__.func_code is f.f_code:
+					# function was found as a class method
+					className=cls.__name__
+				elif memberType is property:
+					if type(member.fget) is FunctionType and member.fget.func_code is f.f_code:
+						# The function was found as a property getter
+						className=cls.__name__
+					elif type(member.fset) is FunctionType and member.fset.func_code is f.f_code:
+						# the function was found as a property setter
+						className=cls.__name__
+				if className:
+					break
 	return ".".join([x for x in path,className,funcName if x])
 
 # Function to strip the base path of our code from traceback text to improve readability.


Test patch: