docassemble

Abstracting code from question blocks to modules


I have a working interview (yay!), and I'm working on refactoring it. I'm trying to figure out what might be a good way to move my code blocks into .py files. In particular, I'm worried about maintaining some of the fancy things docassemble does (the particular code block I'm thinking about is marked initial: True, for example).

Would the idea be to convert whatever your code blocks are doing into functions, and then assign the variables docassemble is looking for in a way that uses those functions? Presumably the results of those functions still need to be dealt with in code blocks?

Is the below roughly the right approach (code below)? I'm assuming that:

  1. Trying to run Bcode.func(A ...) as below, if A was undefined, would trigger the exception necessary to cause docassemble to search for the question needed to set A?
  2. Variables can be passed as set out below.
  3. returning from functions works as set out below.

Are any / all of these assumptions correct / incorrect?

So...

If this was my questions.yml file:

---
question: A?
yesno: A
---
# code setting value of B
---
code: |
    if A:
         answer = "A"
    elif B:
         answer = "B"
    else:
         answer = "C"
---

To abstract away the code, I assume I might do something like this?

questions.yml:

---
imports:
    - Bcode
---
question: A?
yesno: A
---
initial: True
code: |
    answer = Bcode.func(A, *args_to_set_value_of_B)
---

Bcode.py:

---
def func(a, *args_to_set_value_of_b):

    # code computing value of b

    if a:
        return "A"
    elif b:
        return "B"
    else:
        return "C"
---

Solution

  • If you do

    modules:
      - .Bcode
    

    Then docassemble will run (in effect):

    exec("from docassemble.yourpackage.Bcode import *", user_dict)
    

    where user_dict is the interview answers (the namespace of Python code in your YAML file). This will make the name func available in the namespace of your interview so that it can be used in code blocks, Mako templating, etc.

    Your initial block will raise an exception because Bcode is not a name in the interview answers.

    Your function func() will always raise an exception whenever a is true, because b is not defined in the namespace of func().

    Docassemble works by trapping NameError, IndexError, and AttributeError exceptions and then looking for a question or code block that will define whatever variable was undefined. NameError exceptions work with any type of variable, but IndexError and AttributeError exceptions only work on instances of DAObject or subclasses thereof.

    When you move code into module files, it is important to make sure that the code in the module file does not raise NameError exceptions, because those will cause confusion; docassemble will try to define the variable in the interview answer namespace, but this will never fix the problem inside the module because that name will be undefined inside the module no matter what. However, it is safe for code in a module file to raise IndexError and AttributeError errors on DAObject variables that have been passed from the interview namespace to the module namespace, because when docassemble defines those variables in the interview answer namespace, the definitions will be available inside the module as well.