Repository: vyperlang/vyper

Issue:
Allow docblocks on functions
### What's your issue about?

```
Traceback (most recent call last):
  File "vyper-bin", line 56, in <module>
    print(optimizer.optimize(parse_to_lll(code)))
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 991, in parse_to_lll
    return parse_tree_to_lll(code, kode)
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 464, in parse_tree_to_lll
    o, otherfuncs, _globals, sigs, external_contracts, origcode, _custom_units, defaultfunc, runtime_only
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 417, in parse_other_functions
    sub.append(parse_func(_def, _globals, {**{'self': sigs}, **external_contracts}, origcode, _custom_units))  # noqa E999
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 554, in parse_func
    ['seq'] + clampers + [parse_body(c, context) for c in code.body] + ['stop']
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 554, in <listcomp>
    ['seq'] + clampers + [parse_body(c, context) for c in code.body] + ['stop']
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 572, in parse_body
    return parse_stmt(code, context)
  File "/home/jacques/projects/vyper/vyper/parser/parser.py", line 760, in parse_stmt
    return Stmt(stmt, context).lll_node
  File "/home/jacques/projects/vyper/vyper/parser/stmt.py", line 68, in __init__
    self.lll_node = self.stmt_table[stmt_type]()
  File "/home/jacques/projects/vyper/vyper/parser/stmt.py", line 75, in expr
    return Stmt(self.stmt.value, self.context).lll_node
  File "/home/jacques/projects/vyper/vyper/parser/stmt.py", line 72, in __init__
    raise StructureException("Unsupported statement type: %s" % type(stmt), stmt)
vyper.exceptions.StructureException: line 5: Unsupported statement type: <class '_ast.Str'>
 ````


#### Cute Animal Picture
![](https://cdn4.littlethings.com/app/uploads/2017/05/cute-dog-big-ears-600x600.jpg)



Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,6 @@
     license = f.read()
 
 
-
 setup(
     name='vyper',
     # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility.
@@ -23,17 +22,17 @@
     license=license,
     packages=find_packages(exclude=('tests', 'docs')),
     python_requires='>=3.6',
-    install_requires=['py-evm>=0.2.0a16,<1.0.0'],
+    install_requires=['py-evm==0.2.0a18'],
     setup_requires=['pytest-runner'],
     tests_require=[
-        'pytest',
-        'pytest-cov',
-        'eth-tester==0.1.0b24',
-        'py-evm==0.2.0a16',
+       'pytest',
+       'pytest-cov',
+       'eth-tester==0.1.0b26',
+       'py-evm==0.2.0a18',
     ],
     scripts=[
-        'bin/vyper',
-        'bin/vyper-serve',
-        'bin/vyper-run',
+       'bin/vyper',
+       'bin/vyper-serve',
+       'bin/vyper-run',
     ]
 )
diff --git a/vyper/parser/parser.py b/vyper/parser/parser.py
--- a/vyper/parser/parser.py
+++ b/vyper/parser/parser.py
@@ -572,7 +572,8 @@ def parse_body(code, context):
         return parse_stmt(code, context)
     o = []
     for stmt in code:
-        o.append(parse_stmt(stmt, context))
+        lll = parse_stmt(stmt, context)
+        o.append(lll)
     return LLLnode.from_list(['seq'] + o, pos=getpos(code[0]) if code else None)
 
 
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -61,11 +61,13 @@ def __init__(self, stmt, context):
             ast.Break: self.parse_break,
             ast.Continue: self.parse_continue,
             ast.Return: self.parse_return,
-            ast.Delete: self.parse_delete
+            ast.Delete: self.parse_delete,
+            ast.Str: self.parse_docblock  # docblock
         }
         stmt_type = self.stmt.__class__
         if stmt_type in self.stmt_table:
-            self.lll_node = self.stmt_table[stmt_type]()
+            lll_node = self.stmt_table[stmt_type]()
+            self.lll_node = lll_node
         elif isinstance(stmt, ast.Name) and stmt.id == "throw":
             self.lll_node = LLLnode.from_list(['assert', 0], typ=None, pos=getpos(stmt))
         else:
@@ -571,3 +573,8 @@ def get_target(self, target):
         if not target.mutable:
             raise ConstancyViolationException("Cannot modify function argument: %s" % target.annotation)
         return target
+
+    def parse_docblock(self):
+        if '"""' not in self.context.origcode.splitlines()[self.stmt.lineno - 1]:
+            raise InvalidLiteralException('Only valid """ docblocks allowed', self.stmt)
+        return LLLnode.from_list('pass', typ=None, pos=getpos(self.stmt))


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -20,6 +20,7 @@
 )
 from web3.contract import (
     ConciseContract,
+    ConciseMethod
 )
 from vyper.parser.parser_utils import (
     LLLnode
@@ -30,6 +31,35 @@
     optimizer,
 )
 
+
+class VyperMethod(ConciseMethod):
+    ALLOWED_MODIFIERS = {'call', 'estimateGas', 'transact', 'buildTransaction'}
+
+    def __call__(self, *args, **kwargs):
+        return self.__prepared_function(*args, **kwargs)
+
+    def __prepared_function(self, *args, **kwargs):
+        if not kwargs:
+            modifier, modifier_dict = 'call', {}
+            fn_abi = [x for x in self._function.contract_abi if x['name'] == self._function.function_identifier].pop()
+            modifier_dict.update({'gas': fn_abi['gas'] * 1000})  # To make tests faster just supply some high gas value.
+        elif len(kwargs) == 1:
+            modifier, modifier_dict = kwargs.popitem()
+            if modifier not in self.ALLOWED_MODIFIERS:
+                raise TypeError(
+                    "The only allowed keyword arguments are: %s" % self.ALLOWED_MODIFIERS)
+        else:
+            raise TypeError("Use up to one keyword argument, one of: %s" % self.ALLOWED_MODIFIERS)
+
+        return getattr(self._function(*args), modifier)(modifier_dict)
+
+
+class VyperContract(ConciseContract):
+
+    def __init__(self, classic_contract, method_class=VyperMethod):
+        super().__init__(classic_contract, method_class)
+
+
 ############
 # PATCHING #
 ############
@@ -164,7 +194,7 @@ def lll_compiler(lll, *args, **kwargs):
         }
         tx = w3.eth.sendTransaction(deploy_transaction)
         address = w3.eth.getTransactionReceipt(tx)['contractAddress']
-        contract = w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=ConciseContract)
+        contract = w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract)
         return contract
     return lll_compiler
 
@@ -182,11 +212,11 @@ def _get_contract(w3, source_code, *args, **kwargs):
         'from': w3.eth.accounts[0],
         'data': contract._encode_constructor_data(args, kwargs),
         'value': value,
-        'gasPrice': gasPrice
+        'gasPrice': gasPrice,
     }
     tx = w3.eth.sendTransaction(deploy_transaction)
     address = w3.eth.getTransactionReceipt(tx)['contractAddress']
-    contract = w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=ConciseContract)
+    contract = w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract)
     # Filter logs.
     contract._logfilter = w3.eth.filter({
         'fromBlock': w3.eth.blockNumber - 1,
diff --git a/tests/parser/syntax/test_invalids.py b/tests/parser/syntax/test_invalids.py
--- a/tests/parser/syntax/test_invalids.py
+++ b/tests/parser/syntax/test_invalids.py
@@ -2,8 +2,11 @@
 from pytest import raises
 
 from vyper import compiler
-from vyper.exceptions import TypeMismatchException, StructureException
-
+from vyper.exceptions import (
+    TypeMismatchException,
+    StructureException,
+    InvalidLiteralException
+)
 
 # These functions register test cases
 # for pytest functions at the end
@@ -239,14 +242,27 @@ def foo():
     return 3
 """, TypeMismatchException)
 
-# We disabled these keywords
-# throws AttributeError in this case
 must_fail("""
 @public
 def foo():
     suicide(msg.sender)
     """, StructureException)
 
+must_succeed('''
+@public
+def sum(a: int128, b: int128) -> int128:
+    """
+    Sum two signed integers.
+    """
+    return a + b
+''')
+
+must_fail('''
+@public
+def a():
+    "Test"
+''', InvalidLiteralException)
+
 
 @pytest.mark.parametrize('bad_code,exception_type', fail_list)
 def test_compilation_fails_with_exception(bad_code, exception_type):