Added documentation for the technique used for the automagic doc
This commit is contained in:
parent
a066887744
commit
cb809cbb8d
|
|
@ -0,0 +1,178 @@
|
|||
# Automagic help for attributes
|
||||
|
||||
## Teminology
|
||||
|
||||
In Python data members of a class are **attributes** and function members are **methods**.
|
||||
|
||||
By convention **private** attributes has names starting with underscore. Nobody enforces it, is just a convention.
|
||||
|
||||
## Motivation
|
||||
|
||||
The output classes of KiPlot are now designed as some sort of *plug-ins*. Each class defines which options are available in a clean and simple way: *Every public attribute is a valid option.*
|
||||
|
||||
To prevent configurations files from messing with internal machinery all the other attributes are declared *private*.
|
||||
|
||||
This makes things simple, you just declare public attributes and they automatically become avaliable as options.
|
||||
|
||||
The point is: *Can I also make their documentation simple and elegant?*.
|
||||
|
||||
## Docstrings
|
||||
|
||||
Python docstrings are a very nice feature, you just add a special string after the declaration of something and it becomes its documentation.
|
||||
|
||||
The cool thing is that is part of the language, not just something an external parser collects. You can access this documentation from the code:
|
||||
|
||||
```
|
||||
def func():
|
||||
""" func doc """
|
||||
pass
|
||||
|
||||
print(func.__doc__)
|
||||
```
|
||||
|
||||
Can it solve my problem? **Nope**
|
||||
|
||||
Why? simple, as the above example shows the documentation is stored in an attribute. So I can't store documentation inside an attribute of an attribute ;-)
|
||||
|
||||
Docstrings applies to stuff that has attributes, as another attribute. It means it applies to classes, objects, modules, functions, etc. If we try to access to the docstring of a variable we will be accessing to the docstring of the object contained in the variable:
|
||||
|
||||
```
|
||||
a = True
|
||||
print(a.__doc__)
|
||||
```
|
||||
|
||||
Prints the help for the **bool** class. The variable `a` contains a `bool` object.
|
||||
|
||||
## Defining another attribute
|
||||
|
||||
The solution can be defining another attribute with a name associated to the attribute we want to document. To avoid access from configuration files the attribute must be private. So we want to define something like *_help_NAME*. How?
|
||||
|
||||
### Decorators
|
||||
|
||||
This is another nice Python feature, you can wrap stuff with function calls. The problem could be solved if you could wrap the attribute declaration (well its first assignment) with a call to a function that takes the help text and creates the help attribute.
|
||||
|
||||
But **nope** again, decorators applies to classes, functions, etc. not to attributes.
|
||||
|
||||
### Macros
|
||||
|
||||
In C language we use macros to solve these situations, something like this:
|
||||
|
||||
```
|
||||
DO_DOC(int, a, 25, "Doc for a")
|
||||
```
|
||||
|
||||
With a propper macro that expands to:
|
||||
|
||||
```
|
||||
int a = 25;
|
||||
char *_help_a = "Doc for a";
|
||||
```
|
||||
|
||||
What about Python? The above example is solved before compiling the C code, using a preprocessor. So you could do the same with Python. This is complex because now the scripts executed are the post-processed ones.
|
||||
|
||||
Python code is "compiled" by the interpreter, not by external stages. If the idea is to keep distributing the source using a preprocessor isn't very "pythonic".
|
||||
|
||||
A couple of projects offer another interesting option: *syntatic macros*. The idea is to hook the Python parser to get the parsed code, expand the macros and then let Python compile the modified code.
|
||||
|
||||
The two projects I found are **macropy** and **mcpy**.
|
||||
|
||||
### macropy
|
||||
|
||||
The parsed code is represented in an *Abstract Syntax Tree* (aka AST). The **macropy** module hooks the Python machinery used to import modules. So you can declare functions that takes an AST, modifies it and returns the modified AST for execution.
|
||||
|
||||
Things are a little bit complex because:
|
||||
- Only works for imported stuff, a small example needs to be imported.
|
||||
- Macros are stored in separated module.
|
||||
|
||||
For these reasons the simplest example involves three modules. A solution for the problem we have can be found [here](https://github.com/INTI-CMNB/kiplot/tree/new_parser/experiments/__doc__/macropy)
|
||||
|
||||
The example (*application.py*) is:
|
||||
```
|
||||
from mymacros import macros, document
|
||||
|
||||
with document:
|
||||
# comment for a
|
||||
a = "5.1"
|
||||
""" docu a """
|
||||
b = False
|
||||
""" docu b """
|
||||
c = 3
|
||||
""" docu c """
|
||||
|
||||
|
||||
class d(object):
|
||||
def __init__(self):
|
||||
with document:
|
||||
self.at1 = 4.5
|
||||
""" documenting d.at1 """
|
||||
|
||||
|
||||
print("a = "+str(a)+" # "+_help_a)
|
||||
print("b = "+str(b)+" # "+_help_b)
|
||||
print("c = "+str(c)+" # "+_help_c)
|
||||
e = d()
|
||||
print("e.at1 = "+str(e.at1)+" # "+e._help_at1)
|
||||
```
|
||||
|
||||
And its output:
|
||||
|
||||
```
|
||||
a = 5.1 # [string='5.1'] docu a
|
||||
b = False # [boolean=false] docu b
|
||||
c = 3 # [number=3] docu c
|
||||
e.at1 = 4.5 # [number=4.5] documenting d.at1
|
||||
```
|
||||
Showing that we can simulate docstrings creating a companion variable/attribute
|
||||
|
||||
### mcpy
|
||||
|
||||
This is a simplified version of **macropy**. The module is smaller and what you need to add is also smaller. The implementation using it can be found [here](https://github.com/INTI-CMNB/kiplot/tree/new_parser/experiments/__doc__/mcpy)
|
||||
|
||||
A `diff` between both implementations:
|
||||
|
||||
```
|
||||
¤ diff -u . ../mcpy/
|
||||
diff -u ./mymacros.py ../mcpy/mymacros.py
|
||||
--- ./mymacros.py 2020-06-23 09:22:21.934505936 -0300
|
||||
+++ ../mcpy/mymacros.py 2020-06-23 10:45:12.593827618 -0300
|
||||
@@ -1,10 +1,6 @@
|
||||
-from macropy.core.macros import Macros
|
||||
from ast import (Assign, Name, Attribute, Expr, Num, Str, NameConstant, Load, Store)
|
||||
|
||||
-macros = Macros()
|
||||
|
||||
-
|
||||
-@macros.block
|
||||
def document(tree, **kw):
|
||||
""" This macro takes literal strings and converts them into:
|
||||
_help_ID = type_hint+STRING
|
||||
diff -u ./try_mymacros.py ../mcpy/try_mymacros.py
|
||||
--- ./try_mymacros.py 2020-06-23 10:44:08.773919281 -0300
|
||||
+++ ../mcpy/try_mymacros.py 2020-06-23 10:44:17.413906631 -0300
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/usr/bin/python3
|
||||
-import macropy.activate # noqa: F401
|
||||
+import mcpy.activate # noqa: F401
|
||||
import application # noqa: F401
|
||||
```
|
||||
|
||||
As you can see the final *application.py* is the same. The module implementing the macros is simpler because you don't need to mark it calling `Macros()` and you don't need to decorate the macros with `@macros.block` or similar.
|
||||
|
||||
### Important details
|
||||
|
||||
The use of these modules has some important drawbacks:
|
||||
|
||||
1. Error messages can become messy. Sometimes you'll get cryptic messages referencing to syntax errors on an *unknown* file.
|
||||
2. The mechanism interferes with the Python cache mechanism. Recent versions of **macropy** has support for [CPython](https://github.com/python/cpython) (the official Python implementation) and [PyPy](https://www.pypy.org/). I'm not sure what's the real impact. If your code is speed sensitive you should consult the mitigation mechanisms offered by **macropy**.
|
||||
3. Both packages are available from [PyPI](https://pypi.org/project/mcpy/), but neither has a Debian package. I created Debian packages for both: [macropy](https://github.com/set-soft/macropy) and [mcpy](https://github.com/set-soft/mcpy).
|
||||
4. Syntactical macros are really powerful, but hard to write. **macropy** has some helpers to cover some cases, but you have to understand something about ASTs.
|
||||
5. As this looks like "magic" tools like [`flake8`](https://flake8.pycqa.org/en/latest/) won't understand what's going on and you'll need to explain. Here are some examples:
|
||||
- The import to make it work (and others):
|
||||
```
|
||||
import macropy.activate # noqa: F401
|
||||
```
|
||||
- Variable coming from nowhere:
|
||||
```
|
||||
print("a = "+str(a)+" # "+_help_a) # noqa: F821
|
||||
```
|
||||
|
||||
Loading…
Reference in New Issue