Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make python's compile strip docstrings but not asserts

Tags:

python

Python's -O switch strips asserts from the code it compiles.

Python's -OO switch does this and also strips docstrings.

Is there any way to make Python strip docstrings but not asserts?

In particular, is this possible from the command-line or from using the built-in compile function?

like image 270
xorsyst Avatar asked Jul 09 '18 14:07

xorsyst


People also ask

Should I use docstrings in Python?

Using docstrings, Python developers can provide a simple explanation of what a function or class is used for. Without such documentation, it would be very difficult—if not impossible—to learn new Python modules. Docstrings can also be used to generate API's (Application Programming Interfaces).

What are the three types of docstrings?

Let us know the most commonly used docstring formats out there in the wild, which are namely- Google, NumPy, and Sphinx docstring formats.

Do all functions need docstrings?

Every function you create ought to have a docstring. They're in triple-quoted strings and allow for multi-line text.

Can docstrings include escape sequences?

Since docstrings are Python strings, escape sequences such as \n will be parsed as if the corresponding character—for example a newline—occurred at the position of the escape sequence in the source code.


1 Answers

It's bit hacky, but you could generate Abstract Syntax Trees (asts) for your code, remove anything that looks like a docstring, and then pass the changed asts to compile.

Given this module:

$  cat func.py 
"""
This is  module-level docstring.
"""

def f(x):
    """
    This is a doc string
    """
    # This is a comment
    return 2 * x

First, generate the ast from the module source code.

>>> import ast
>>> with open('func.py') as f:
...     src = f.read()
... 
>>> tree = ast.parse(src)

Dumping the ast shows the docstrings are present (comments are not included in asts)

>>> ast.dump(tree)
"Module(body=[Expr(value=Str(s='\\nThis is  module-level docstring.\\n')), FunctionDef(name='f', args=arguments(args=[arg(arg='x', annotation=None)], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Expr(value=Str(s='\\n    This is a doc string\\n    ')), Return(value=BinOp(left=Num(n=2), op=Mult(), right=Name(id='x', ctx=Load())))], decorator_list=[], returns=None)])"

Now the hacky part: define a visitor that will visit each node in the ast, removing docstrings. A naive implmentation could just remove any expressions that are just strings that are not part of an assignment.

>>> class Transformer(ast.NodeTransformer):
...
...     def visit_Expr(self, node):
...         if isinstance(node.value, ast.Str):                      
...             return None
...         return node

This might be problematic if the code contains multi-line strings (though I haven't tested this).

A safer implementation might remove the first node from any module, function or class definitions if the node is an expression node and its value is a string node (if the string were being bound to a name the node would be an assignment node, not an expression).

class Transformer(ast.NodeTransformer):

    def visit_Module(self, node):
        self.generic_visit(node)
        return self._visit_docstring_parent(node)

    def visit_FunctionDef(self, node):
        self.generic_visit(node)
        return self._visit_docstring_parent(node)

    def visit_ClassDef(self, node):
        self.generic_visit(node)
        return self._visit_docstring_parent(node)

    def _visit_docstring_parent(self, node):
        # Common docstring removal code.
        # Assumes docstrings will always be first node in
        # module/class/function body.
        new_body = []
        for i, child_node in enumerate(node.body):
            if i == 0 and isinstance(child_node, ast.Expr) and isinstance(child_node.value, ast.Str):
                pass
            else:
                new_body.append(child_node)
        node.body = new_body
        return node

>>> # Transformer performs an in-place transformation.
>>> Transformer().visit(tree)

Observe the docstrings are not longer present in the new ast:

>>> ast.dump(tree)
"Module(body=[FunctionDef(name='f', args=arguments(args=[arg(arg='x', annotation=None)], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Return(value=BinOp(left=Num(n=2), op=Mult(), right=Name(id='x', ctx=Load())))], decorator_list=[], returns=None)])"

The new ast can be compiled to a code object and executed:

>>> ast.fix_missing_locations(new_tree)
>>> code_obj = compile(new_tree, '<string>', mode='exec')

>>> exec(code_obj, globals(), locals())
>>> globals()['f']
<function f at 0x7face8bc2158>
>>> globals()['f'](5)
10
like image 166
snakecharmerb Avatar answered Oct 13 '22 01:10

snakecharmerb