Repository: fniessink/access-modifiers

Issue:
Add flag to turn off the access modifiers' checks in production

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a696ab..2e0ae89 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]
+
+### Added
+
+- Added `access_modifiers.disable()` to disable access checks, e.g. in production. This method should be called before any access modifier decorators are compiled, so somewhere at the start of your program. Closes [#3](https://github.com/fniessink/access-modifiers/issues/3). 
+
 ## [0.2.1] - [2019-08-26]
 
 ### Fixed
diff --git a/README.md b/README.md
index e2b4fd9..f0fc54b 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,23 @@ print(c.static_private_method())  # Raises an exception
 
 Combining protected methods with static methods is not supported. Combining access modifiers with class methods is not supported (yet).
 
+## Performance
+
+The access modifier decorators work by looking at the code that is calling the decorator to decide whether it is allowed to call the method. To do so, the decorators use implementation details of CPython, like sys._getframe() and the names of code objects such as lambdas and modules. These checks are done on each method call. Consequently, there is a considerable performance impact. Therefore it's recommended to use the access modifiers during testing and turn them off in production using the `access_modifiers.disable()` method. Note that you need to call this method before any of the access modifier decorators are evaluated, i.e.:
+
+```python
+from access_modifiers import disable, privatemethod
+
+disable()  # This will disable the access checks
+
+class Class:
+    @privatemethod
+    def private_method(self) -> str:
+        return "private_method"
+
+disable()  # Calling disable here will not work, Class.private_method has already been wrapped
+```
+
 ## Installation
 
 The package is available from the Python Package Index, install with `pip install access-modifiers`.
@@ -84,8 +101,4 @@ To run the unittests and measure the coverage (which should always be at 100%):
 
 To run Pylint (which should score a 10) and Mypy (which shouldn't complain): `ci/quality.sh`.
 
-## Implementation notes
-
-Both the `privatemethod` and the `protectedmethod` decorator work by looking at the code that is calling the decorator to decide whether it is allowed to call the method. To do so, the decorators use implementation details of CPython, like `sys._getframe()` and the names of code objects such as lambdas and modules. 
-
 The implementation is driven by (unit) tests and has 100% unit test statement and branch coverage. Please look at the tests to see which usage scenario's are currently covered.
diff --git a/access_modifiers/access_modifiers.py b/access_modifiers/access_modifiers.py
index 77ec865..cf99620 100644
--- a/access_modifiers/access_modifiers.py
+++ b/access_modifiers/access_modifiers.py
@@ -12,8 +12,25 @@ class AccessException(Exception):
 ReturnType = TypeVar('ReturnType')
 
 
+_CHECK_ACCESS = True
+
+
+def disable() -> None:
+    """Disable all access checks. Needs to be invoked before the decorators are evaluated."""
+    global _CHECK_ACCESS  # pylint: disable=global-statement
+    _CHECK_ACCESS = False
+
+
+def enable() -> None:
+    """Enable all access checks. For testing purposes."""
+    global _CHECK_ACCESS  # pylint: disable=global-statement
+    _CHECK_ACCESS = True
+
+
 def privatemethod(method: Callable[..., ReturnType]) -> Callable[..., ReturnType]:
     """Decorator that creates a private method."""
+    if not _CHECK_ACCESS:
+        return method
     method_class_qualname = getframe(1).f_locals.get("__qualname__")
     @wraps(method)
     def private_method_wrapper(*args, **kwargs) -> ReturnType:
@@ -40,6 +57,8 @@ def privatemethod(method: Callable[..., ReturnType]) -> Callable[..., ReturnType
 
 def protectedmethod(method: Callable[..., ReturnType]) -> Callable[..., ReturnType]:
     """Decorator that creates a protected method."""
+    if not _CHECK_ACCESS:
+        return method
     @wraps(method)
     def protected_method_wrapper(*args, **kwargs) -> ReturnType:
         """Wrap the original method to make it protected."""


Test patch:
diff --git a/access_modifiers/tests/test_private.py b/access_modifiers/tests/test_private.py
index 97ac772..747d525 100644
--- a/access_modifiers/tests/test_private.py
+++ b/access_modifiers/tests/test_private.py
@@ -2,7 +2,7 @@
 
 import unittest
 
-from ..access_modifiers import AccessException, privatemethod, protectedmethod
+from ..access_modifiers import AccessException, disable, enable, privatemethod, protectedmethod
 
 
 class PrivateMethodTests(unittest.TestCase):
@@ -52,16 +52,29 @@ class PrivateMethodTests(unittest.TestCase):
             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."""
+        """Test that accessing a private method throws an exception."""
         self.assertRaises(AccessException, self.Class().private_method)
-        self.assertRaises(AccessException, self.Class.private_method, self.Class())
+
+    def test_call_private_method_directly_without_access_checks(self):
+        """Test that accessing a private method without access checks works."""
+        try:
+            disable()
+
+            class Class:
+                @privatemethod
+                def private_method(self):  # pylint: disable=no-self-use
+                    return "Class.private_method"
+
+            self.assertEqual("Class.private_method", Class().private_method())
+        finally:
+            enable()
 
     def test_call_private_method_via_public_method(self):
-        """Test the accessing a private method via a public method is allowed."""
+        """Test that accessing a private method via a public method is allowed."""
         self.assertEqual("Class.public_method -> Class.private_method", self.Class().public_method())
 
     def test_call_private_method_via_public_method_from_subclass(self):
-        """Test the accessing a private method via an overridden public method is allowed."""
+        """Test that accessing a private method via an overridden public method is allowed."""
 
         class Subclass(self.Class):
             def public_method(self):
@@ -80,7 +93,7 @@ class PrivateMethodTests(unittest.TestCase):
         self.assertRaises(AccessException, Subclass().public_method)
 
     def test_call_private_method_via_public_method_in_subclass_using_super(self):
-        """Test the accessing a private method via a public method in a subclass is not allowed,
+        """Test that accessing a private method via a public method in a subclass is not allowed,
         not even when using super()."""
 
         class Subclass(self.Class):
@@ -168,3 +181,18 @@ class StaticPrivateMethodTests(PrivateMethodTests):
         @protectedmethod
         def private_method(self):
             return "Subclass.private_method -> " + super().private_method()  # pragma: nocover
+
+    def test_call_private_method_directly_without_access_checks(self):
+        """Test that accessing a private method without access checks works."""
+        try:
+            disable()
+
+            class Class:  # pylint: disable=too-few-public-methods
+                @staticmethod
+                @privatemethod
+                def private_method():
+                    return "Class.private_method"
+
+            self.assertEqual("Class.private_method", Class().private_method())
+        finally:
+            enable()
diff --git a/access_modifiers/tests/test_protected.py b/access_modifiers/tests/test_protected.py
index 414e06d..2ec5a78 100644
--- a/access_modifiers/tests/test_protected.py
+++ b/access_modifiers/tests/test_protected.py
@@ -2,7 +2,7 @@
 
 import unittest
 
-from ..access_modifiers import AccessException, protectedmethod, privatemethod
+from ..access_modifiers import AccessException, enable, disable, protectedmethod, privatemethod
 
 
 class ProtectedMethodTest(unittest.TestCase):
@@ -21,14 +21,27 @@ class ProtectedMethodTest(unittest.TestCase):
     def test_protected_method(self):
         """Test that accessing a protected method throws an exception."""
         self.assertRaises(AccessException, self.Class().protected_method)
-        self.assertRaises(AccessException, self.Class.protected_method, self.Class())
+
+    def test_call_protected_method_directly_without_access_checks(self):
+        """Test that accessing a protected method without access checks works."""
+        try:
+            disable()
+
+            class Class:  # pylint: disable=too-few-public-methods
+                @protectedmethod
+                def protected_method(self):  # pylint: disable=no-self-use
+                    return "Class.protected_method"
+
+            self.assertEqual("Class.protected_method", Class().protected_method())
+        finally:
+            enable()
 
     def test_call_protected_method_via_public_method(self):
-        """Test the accessing a protected method via a public method is allowed."""
+        """Test that accessing a protected method via a public method is allowed."""
         self.assertEqual("Class.public_method -> Class.protected_method", self.Class().public_method())
 
     def test_call_protected_method_via_protected_method(self):
-        """Test the accessing a protected method via a another protected method is allowed."""
+        """Test that accessing a protected method via a another protected method is allowed."""
 
         class Subclass(self.Class):
             @protectedmethod