Repository: Parquery/icontract

Issue:
KeyError in contract caused by staticmethod and class invariant
```Python
from icontract import invariant


@invariant(lambda self: all(" " not in part for part in self.parts))
class ID:
    def __init__(self, identifier: str) -> None:
        self.parts = identifier.split(self.separator())

    @staticmethod
    def separator() -> str:
        return "."


ID("A")
```

```Console
$ python test.py
Traceback (most recent call last):
  File "/tmp/venv/lib/python3.9/site-packages/icontract/_checkers.py", line 576, in wrapper
    instance = _find_self(param_names=param_names, args=args, kwargs=kwargs)
  File "/tmp/venv/lib/python3.9/site-packages/icontract/_checkers.py", line 499, in _find_self
    return kwargs["self"]
KeyError: 'self'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tmp/test.py", line 14, in <module>
    ID("A")
  File "/tmp/venv/lib/python3.9/site-packages/icontract/_checkers.py", line 561, in wrapper
    result = func(*args, **kwargs)
  File "/tmp/test.py", line 7, in __init__
    self.parts = identifier.split(self.separator())
  File "/tmp/venv/lib/python3.9/site-packages/icontract/_checkers.py", line 578, in wrapper
    raise KeyError(("The parameter 'self' could not be found in the call to function {!r}: "
KeyError: "The parameter 'self' could not be found in the call to function <function ID.separator at 0x7f26061a1550>: the param names were [], the args were (<__main__.ID object at 0x7f260636beb0>,) and kwargs were {}"
```

### Versions

```
icontract  2.4.1
asttokens  2.0.4
Python     3.9.1
```

Comments:

Patch:
diff --git a/README.rst b/README.rst
index d5660c1..08b5f1f 100644
--- a/README.rst
+++ b/README.rst
@@ -29,8 +29,9 @@ It also gives a base for a flourishing of a wider ecosystem:
   `Hypothesis <https://hypothesis.readthedocs.io/en/latest/>`_ strategies based on the contracts,
 
   * together with IDE integrations such as
-    `icontract-hypothesis-vim <https://github.com/mristin/icontract-hypothesis-vim>`_ and
-    `icontract-hypothesis-pycharm <https://github.com/mristin/icontract-hypothesis-pycharm>`_,
+    `icontract-hypothesis-vim <https://github.com/mristin/icontract-hypothesis-vim>`_,
+    `icontract-hypothesis-pycharm <https://github.com/mristin/icontract-hypothesis-pycharm>`_, and
+    `icontract-hypothesis-vscode <https://github.com/mristin/icontract-hypothesis-vscode>`_,
 * An ongoing integration with `CrossHair <https://github.com/pschanely/CrossHair>`_, and
 * An ongoing integration with `FastAPI <https://github.com/tiangolo/fastapi/issues/1996>`_.
 
@@ -925,7 +926,7 @@ The following scripts were run:
 * `benchmarks/against_others/compare_postcondition.py <https://github.com/Parquery/icontract/tree/master/benchmarks/against_others/compare_postcondition.py>`_
 
 The benchmarks were executed on Intel(R) Xeon(R) E-2276M  CPU @ 2.80GHz.
-We used Python 3.8.5, icontract 2.3.5, deal 4.2.0 and dpcontracts 0.6.0.
+We used Python 3.8.5, icontract 2.4.1, deal 4.1.0 and dpcontracts 0.6.0.
 
 The following tables summarize the results.
 
@@ -934,10 +935,10 @@ Benchmarking invariant at __init__:
 =========================  ============  ==============  =======================
 Case                         Total time    Time per run    Relative time per run
 =========================  ============  ==============  =======================
-`ClassWithIcontract`             1.74 s         1.74 μs                     100%
-`ClassWithDpcontracts`           0.55 s         0.55 μs                      32%
-`ClassWithDeal`                  3.26 s         3.26 μs                     187%
-`ClassWithInlineContract`        0.33 s         0.33 μs                      19%
+`ClassWithIcontract`             1.36 s         1.36 μs                     100%
+`ClassWithDpcontracts`           0.46 s         0.46 μs                      34%
+`ClassWithDeal`                  2.65 s         2.65 μs                     195%
+`ClassWithInlineContract`        0.27 s         0.27 μs                      20%
 =========================  ============  ==============  =======================
 
 Benchmarking invariant at a function:
@@ -945,10 +946,10 @@ Benchmarking invariant at a function:
 =========================  ============  ==============  =======================
 Case                         Total time    Time per run    Relative time per run
 =========================  ============  ==============  =======================
-`ClassWithIcontract`             2.48 s         2.48 μs                     100%
-`ClassWithDpcontracts`           0.56 s         0.56 μs                      22%
-`ClassWithDeal`                  9.76 s         9.76 μs                     393%
-`ClassWithInlineContract`        0.28 s         0.28 μs                      11%
+`ClassWithIcontract`             1.94 s         1.94 μs                     100%
+`ClassWithDpcontracts`           0.46 s         0.46 μs                      24%
+`ClassWithDeal`                  7.14 s         7.14 μs                     368%
+`ClassWithInlineContract`        0.23 s         0.23 μs                      12%
 =========================  ============  ==============  =======================
 
 Benchmarking precondition:
@@ -956,10 +957,10 @@ Benchmarking precondition:
 ===============================  ============  ==============  =======================
 Case                               Total time    Time per run    Relative time per run
 ===============================  ============  ==============  =======================
-`function_with_icontract`              0.03 s         3.17 μs                     100%
-`function_with_dpcontracts`            0.65 s        64.62 μs                    2037%
-`function_with_deal`                   0.16 s        16.04 μs                     506%
-`function_with_inline_contract`        0.00 s         0.17 μs                       6%
+`function_with_icontract`              0.03 s         2.61 μs                     100%
+`function_with_dpcontracts`            0.51 s        50.52 μs                    1939%
+`function_with_deal`                   0.13 s        12.59 μs                     483%
+`function_with_inline_contract`        0.00 s         0.15 μs                       6%
 ===============================  ============  ==============  =======================
 
 Benchmarking postcondition:
@@ -967,11 +968,11 @@ Benchmarking postcondition:
 ===============================  ============  ==============  =======================
 Case                               Total time    Time per run    Relative time per run
 ===============================  ============  ==============  =======================
-`function_with_icontract`              0.03 s         3.01 μs                     100%
-`function_with_dpcontracts`            0.66 s        65.78 μs                    2187%
-`function_with_deal_post`              0.01 s         1.12 μs                      37%
-`function_with_deal_ensure`            0.02 s         1.62 μs                      54%
-`function_with_inline_contract`        0.00 s         0.18 μs                       6%
+`function_with_icontract`              0.03 s         2.63 μs                     100%
+`function_with_dpcontracts`            0.51 s        50.59 μs                    1921%
+`function_with_deal_post`              0.01 s         0.89 μs                      34%
+`function_with_deal_ensure`            0.01 s         1.23 μs                      47%
+`function_with_inline_contract`        0.00 s         0.14 μs                       5%
 ===============================  ============  ==============  =======================
 
 
diff --git a/icontract/_checkers.py b/icontract/_checkers.py
index aaa5b97..7fde7be 100644
--- a/icontract/_checkers.py
+++ b/icontract/_checkers.py
@@ -638,7 +638,9 @@ def add_invariant_checks(cls: type) -> None:
     names_properties = []  # type: List[Tuple[str, property]]
 
     # Filter out entries in the directory which are certainly not candidates for decoration.
-    for name, value in [(name, getattr(cls, name)) for name in dir(cls)]:
+    for name in dir(cls):
+        value = getattr(cls, name)
+
         # __new__ is a special class method (though not marked properly with @classmethod!).
         # We need to ignore __repr__ to prevent endless loops when generating error messages.
         # __getattribute__, __setattr__ and __delattr__ are too invasive and alter the state of the instance.
@@ -658,15 +660,21 @@ def add_invariant_checks(cls: type) -> None:
                 not isinstance(value, property):
             continue
 
-        # Ignore class methods
-        if getattr(value, "__self__", None) is cls:
-            continue
-
         # Ignore "protected"/"private" methods
         if name.startswith("_") and not (name.startswith("__") and name.endswith("__")):
             continue
 
         if inspect.isfunction(value) or isinstance(value, _SLOT_WRAPPER_TYPE):
+            # Ignore class methods
+            if getattr(value, "__self__", None) is cls:
+                continue
+
+            # Ignore static methods
+            # See https://stackoverflow.com/questions/14187973/python3-check-if-method-is-static
+            bound_value = inspect.getattr_static(cls, name, None)
+            if isinstance(bound_value, staticmethod):
+                continue
+
             names_funcs.append((name, value))
 
         elif isinstance(value, property):


Test patch:
diff --git a/tests/test_invariant.py b/tests/test_invariant.py
index d47cf44..6a0e1fd 100644
--- a/tests/test_invariant.py
+++ b/tests/test_invariant.py
@@ -62,6 +62,46 @@ class TestOK(unittest.TestCase):
         inst = SomeClass()
         self.assertEqual(100, inst.x)
 
+    def test_static_method(self) -> None:
+        # Adapted from https://github.com/Parquery/icontract/issues/186
+        @icontract.invariant(lambda self: A.some_static_method(self.x))
+        @icontract.invariant(lambda self: self.some_instance_method())
+        class A:
+            def __init__(self) -> None:
+                self.x = 10
+
+            def some_instance_method(self) -> bool:
+                # We need this instance method for easier debugging.
+                return self.x < 100
+
+            @staticmethod
+            def some_static_method(x: int) -> bool:
+                return x > 0
+
+        _ = A()
+
+    def test_inherited_static_method(self) -> None:
+        @icontract.invariant(lambda self: A.some_static_method(self.x))
+        @icontract.invariant(lambda self: self.some_instance_method())
+        class A:
+            def __init__(self) -> None:
+                self.x = 10
+
+            def some_instance_method(self) -> bool:
+                # We need this instance method for easier debugging.
+                return self.x < 100
+
+            @staticmethod
+            def some_static_method(x: int) -> bool:
+                return x > 0
+
+        # We need to test for inheritance.
+        # See https://stackoverflow.com/questions/14187973/#comment74562120_37147128
+        class B(A):
+            pass
+
+        _ = B()
+
     def test_protected_method_may_violate_inv(self) -> None:
         @icontract.invariant(lambda self: self.x > 0)
         class SomeClass: