Repository: fniessink/access-modifiers

Issue:
Calling a private method from a private method from a public method raises AccessException

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index af2c6e6..00866cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 <!-- The line "## <square-bracket>Unreleased</square-bracket>" is replaced by the ci/release.py script with the new release version and release date. -->
 
+## [Unreleased]
+
+### Fixed
+
+- A public method calling a private method calling another private method would raise an AccessException. Fixes [#10](https://github.com/fniessink/access-modifiers/issues/10). 
+
 ## [0.2.0] - [2019-08-26]
 
 ### Added
diff --git a/access_modifiers/access_modifiers.py b/access_modifiers/access_modifiers.py
index 18d76bc..77ec865 100644
--- a/access_modifiers/access_modifiers.py
+++ b/access_modifiers/access_modifiers.py
@@ -26,11 +26,13 @@ def privatemethod(method: Callable[..., ReturnType]) -> Callable[..., ReturnType
             caller_frame = caller_frame.f_back
             caller_code = caller_frame.f_code
             caller_name = caller_code.co_name
-        caller_instance = caller_frame.f_locals.get("self")
-        # Look up the calling method to see if it's defined in the same class as the private method
-        for caller_class in caller_instance.__class__.mro():
-            caller = caller_class.__dict__.get(caller_name)
-            if caller and caller.__code__ == caller_code and method_class_qualname == caller_class.__qualname__:
+        caller_class = caller_frame.f_locals.get("self").__class__
+        # Look up the caller method to see if it's defined in the same class as the wrapped method
+        classes = [cls for cls in caller_class.mro() if caller_name in cls.__dict__]
+        for cls in classes:
+            caller = cls.__dict__[caller_name]
+            caller = caller.__dict__["__wrapped__"] if "__wrapped__" in caller.__dict__ else caller
+            if caller.__code__ == caller_code and method_class_qualname == cls.__qualname__:
                 return method(*args, **kwargs)
         raise AccessException(f"Attempted call to private method {method} from outside its class")
     return private_method_wrapper


Test patch:
diff --git a/access_modifiers/tests/test_private.py b/access_modifiers/tests/test_private.py
index a3cea7d..97ac772 100644
--- a/access_modifiers/tests/test_private.py
+++ b/access_modifiers/tests/test_private.py
@@ -5,11 +5,52 @@ import unittest
 from ..access_modifiers import AccessException, privatemethod, protectedmethod
 
 
-class PrivateMethodTestsMixin:
+class PrivateMethodTests(unittest.TestCase):
     """Shared unit tests for the private method and static private method access modifiers."""
 
     # pylint: disable=missing-docstring,too-few-public-methods
 
+    class Class:
+        @privatemethod
+        def private_method(self):  # pylint: disable=no-self-use
+            return "Class.private_method"
+
+        @privatemethod
+        def private_method_calling_private_method(self):
+            return "Class.private_method_calling_private_method -> " + self.private_method()
+
+        def public_method(self):
+            return "Class.public_method -> " + self.private_method()
+
+        def public_method_calling_private_method_via_private_method(self):
+            return "Class.public_method_calling_private_method_via_private_method -> " + \
+                   self.private_method_calling_private_method()
+
+        def public_method_using_list_comprehension(self):
+            return ["Class.public_method -> " + self.private_method() for _ in range(1)][0]
+
+        def public_method_using_nested_lambdas(self):
+            # pylint: disable=unnecessary-lambda
+            inner_lambda_function = lambda: self.private_method()
+            outer_lambda_function = lambda: "Class.public_method -> " + inner_lambda_function()
+            return outer_lambda_function()
+
+        def public_method_using_try_except(self):
+            try:
+                return "Class.public_method -> " + self.private_method()
+            except AttributeError:  # pragma: nocover
+                pass
+
+    class Subclass(Class):
+        @privatemethod
+        def private_method(self):
+            super().private_method()  # pragma: nocover
+
+    class SubclassOverridesWithProtectedMethod(Class):
+        @protectedmethod
+        def private_method(self):
+            return "Subclass.private_method -> " + super().private_method()  # pragma: nocover
+
     def test_call_private_method_directly(self):
         """Test the accessing a private method throws an exception."""
         self.assertRaises(AccessException, self.Class().private_method)
@@ -72,47 +113,15 @@ class PrivateMethodTestsMixin:
         self.assertRaises(AccessException, self.Subclass().public_method)
         self.assertRaises(AccessException, self.Subclass().private_method)
 
-
-class PrivateMethodTest(PrivateMethodTestsMixin, unittest.TestCase):
-    """Unit tests for the @privatemethod decorator."""
-
-    # pylint: disable=missing-docstring
-
-    class Class:
-        @privatemethod
-        def private_method(self):  # pylint: disable=no-self-use
-            return "Class.private_method"
-
-        def public_method(self):
-            return "Class.public_method -> " + self.private_method()
-
-        def public_method_using_list_comprehension(self):
-            return ["Class.public_method -> " + self.private_method() for _ in range(1)][0]
-
-        def public_method_using_nested_lambdas(self):
-            # pylint: disable=unnecessary-lambda
-            inner_lambda_function = lambda: self.private_method()
-            outer_lambda_function = lambda: "Class.public_method -> " + inner_lambda_function()
-            return outer_lambda_function()
-
-        def public_method_using_try_except(self):
-            try:
-                return "Class.public_method -> " + self.private_method()
-            except AttributeError:  # pragma: nocover
-                pass
-
-    class Subclass(Class):
-        @privatemethod
-        def private_method(self):
-            super().private_method()  # pragma: nocover
-
-    class SubclassOverridesWithProtectedMethod(Class):
-        @protectedmethod
-        def private_method(self):
-            return "Subclass.private_method -> " + super().private_method()  # pragma: nocover
+    def test_call_private_method_via_private_method(self):
+        """Test that a private method can be called via another private method."""
+        self.assertEqual(
+            "Class.public_method_calling_private_method_via_private_method -> "
+            "Class.private_method_calling_private_method -> Class.private_method",
+            self.Class().public_method_calling_private_method_via_private_method())
 
 
-class StaticPrivateMethodTest(PrivateMethodTestsMixin, unittest.TestCase):
+class StaticPrivateMethodTests(PrivateMethodTests):
     """Unit tests for the combined @staticmethod @privatemethod decorator."""
 
     # pylint: disable=missing-docstring
@@ -123,9 +132,17 @@ class StaticPrivateMethodTest(PrivateMethodTestsMixin, unittest.TestCase):
         def private_method():
             return "Class.private_method"
 
+        @privatemethod
+        def private_method_calling_private_method(self):
+            return "Class.private_method_calling_private_method -> " + self.private_method()
+
         def public_method(self):
             return "Class.public_method -> " + self.private_method()
 
+        def public_method_calling_private_method_via_private_method(self):
+            return "Class.public_method_calling_private_method_via_private_method -> " + \
+                   self.private_method_calling_private_method()
+
         def public_method_using_list_comprehension(self):
             return ["Class.public_method -> " + self.private_method() for _ in range(1)][0]