Abstracting code from question blocks to modules - docassemble

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:
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?
Variables can be passed as set out below.
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"
---

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.

I would avoid moving code into a module that doesn't have a clear interface. It's totally appropriate to have code blocks in the YAML that contain interview logic. I keep modules for reusable, abstracted code.

Related

Declaring variables inside a function python

For some reason I keep getting error in my code stating that my variables have not been declared. This only happens when I try to declare them in a function and not outside.
example
x, y = 105,107
print (x,y)
the above line of code works and gives me the output 105 107
but when I do this below
def fun1():
x, y = 105,107
print (x,y)
I get NameError: name 'x' is not defined
need help to understand what's happening.
One of the main utilities of functions is exactly the way they allow one
to isolate variables - no worries about clashing names for the code
in various functions - once a function works properly, it is done.
But if one needs to expose variables that populated inside functions to
the outside world, it is possible with the keyword "global". Notice that this
is in general considered bad practice, and even for small scripts,
there are often better solutions. But, common sense should always be the rule:
def fun1():
global x, y
x, y = 105, 107
fun1()
print(x, y)
Note that your code had another incorrect assumption: code
inside function bodies is not executed unless the function is called -
so, in the example in your question, even if you had declared
the variables as global, the print call would still
raise the same error, since you are not executing the line
that defines these variables by calling the function.
Now, you've learned about "globals" - next step is forget it
exists and learn how to work with variables properly encapsulated
inside functions, and when you get to some intermediate/advanced
level, then you will be able to judge when "globals" might actually
do more good than harm (which is almost never).

TypeError: makeMove() missing 1 required positional argument: 'self'

I am getting a weird problem. I call the same method against instances of a class and they work fine. But somewhere along the line of running the programs I lose the object reference and now it shows me trying to make a call against my class instead. Here is what I see in the debugger for a good call:
<Move.Move object at 0x0000000003F362E8> (object reference)
but later I will just get a
<Move.Move class> (class reference)
How am I at times losing my object reference and instead getting a class reference.
Famous last words, I have checked my code against my change files and don't see any changes in this area.
Any thoughts or suggestions are appreciated.
Code as requested. Hope it helps.
for move in moves:
if move.targetStack is not None:
t = t+1
bestMove = move #Just need to have one to compare to
if t==0:
break
try:
for move in moves:
if move.targetStack is None:
continue
elif move.highCard.rank > bestMove.highCard.rank:
P.S. This raises AttributeError instead. But has same cause of referencing the Move class and not an instance. It works for about 6 full cycles before breaking.
Found my problem. It was a capitalization issue. In of my cases it was assignin Move (the name of the class) to the variable bestMove. Sometimes I miss type safe languages!

Pythonic way of checking if variable exists and if so, comparing it to another

I often find myself in a situation where i need to check if i assigned something to a variable and then if so, compare it's value to something else.
Sure, I know how to do it, but I wonder if there is a more elegant way of handling this.
EXAMPLE:
def function():
with suppress(SomeException):
variable = 10
if variable and variable > 5:
# do things
That will give me an UnboundLocalError exception (in case of SomeException happening during variable assignment). Again, I know how to code it, so it will work, it's the style of coding that troubles me. I want to avoid creating unnecessary local variable.
You say you know how to do it, but the code you posted will throw an exception if the assignment doesn't happen.
There really is a simple way to do this: just don't write code that can leave a variable uninitialised.
def function():
variable = None
if SomeCondition():
variable = 10
if variable is not None:
# do things
Note that this doesn't create any unneeded local variables, the variable variable would be created whether or not you assign to it, but if you don't assign then it is uninitialised and testing whether it is initialised means having to throw and catch an exception, and that is slow. Simply initialising it means you have a simple easy test. Stop trying to optimise things that don't need optimising.

Call nested Python 3 nested function from parent function

For Python 3. I want to call a nested function from a top-level function. NOT access a variable in a nested function but call a nested function (what I'd normally refer to as a subroutine) from a "parent" function.
Answers on SO and elsewhere describe how to use the global and nonlocal keywords to enable variables in nested functions to be accessed by "parent" functions. But I haven't been able to translate that technique to Python 3 nested functions.
What I'm hoping to achieve, largely for outer-to-inner readability, is:
def topLevelFunction(listOfStrings):
# Top-level function's code here.
desiredValue = nestedFunction(dataToModify)
return(desiredResult)
# This nested function's source code is visibly contained within its parent.
def nestedFunction(oneListEntry):
# Modify data passed to function.
return(fixedData)
This structure of course produces UnboundLocalError: local variable 'nestedFunction' referenced before assignment.
I've circumvented that with:
def topLevelFunction(listofStrings):
def nestedFunction(oneListEntry):
# nestedFunction's code goes here.
return(fixedData)
# topLevelFunction's code goes here.
# Only in this "upside down" structure can top-level function call nestedFunction?
return(desiredResult)
Part of the problem seems to be that the nonlocal / global keywords that enable me to reference variables outside of nested functions' scope haven't enabled me to do the same thing for nested functions themselves(?) Or if they do, the syntax is unique? If that's the case, thanks for a pointer to that specific syntax.
I've also made nestedFunction a stand-alone function at the same level / scope as topLevelFunction. But at least from a readability perspective both circumventions (I won't call them fixes) seem to require me to write "upside down" code where things that are used later in the program flow must be "higher" in the source code?
Perhaps I'm too accustomed to compiled languages that don't require this? Or must I instead create a Python 3 class?

How to modify immutable objects passed as **arguments in functions with Python3 the elegant way?

I am not sure what the problem is here, so I don't really know how I should call the subject for that question. Please offer a better subject if you know.
The code below is a extrem simplified example of the original one. But it reproduce the problem very nice. After the call of test() foo should be sieben.
I think I didn't know some special things about scopes of variables in Python. This might be a very good problem to learn more about that. But I don't know on which Python topic I should focus here to find a solution for my own.
#!/usr/bin/env python3
def test(handlerFunction, **handlerArgs):
handlerFunction(**handlerArgs)
def myhandler(dat):
print('dat={}'.format(dat))
dat = 'sieben'
print('dat={}'.format(dat))
foo = 'foo'
test(myhandler, dat=foo)
print('foo={}'.format(foo))
Of course I could make foo a global variable. But that is not the goal. The goal is to carry this variable inside and through sub-functions of different levels and bring the result back. In the original code I use some more complexe data structures with **handlerArgs.
A solution could be to use a list() as an mutable object holding the immutable one. But is this really elegant or pythonic?
#!/usr/bin/env python3
def test(handlerFunction, **handlerArgs):
handlerFunction(**handlerArgs)
def myhandler(dat):
print('dat={}'.format(dat))
# MODIFIED LINE
dat[0] = 'sieben'
print('dat={}'.format(dat))
# MODIFIED LINE
foo = ['foo']
test(myhandler, dat=foo)
print('foo={}'.format(foo))
The ** syntax has nothing to do with this. dat is local to myhandler, and assigning it doesn't change the global variable with the same name. If you want to change the module variable from inside the function, declare the variable as global at the beginning of the function body:
def myhandler(): # you don't need to pass dat as argument
global dat
print('dat={}'.format(dat))
dat = 'sieben'
print('dat={}'.format(dat))
Here's a relevant portion from the docs:
If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.
If the global statement occurs within a block, all uses of the name specified in the statement refer to the binding of that name in the top-level namespace. Names are resolved in the top-level namespace by searching the global namespace, i.e. the namespace of the module containing the code block, and the builtins namespace, the namespace of the module builtins. The global namespace is searched first. If the name is not found there, the builtins namespace is searched. The global statement must precede all uses of the name.
After your edit the question reads as: "how do I mutate an immutable object?"
Well, I think you've guessed it: you don't. Using a mutable object in this manner seems reasonable to me.

Resources