TL;DR
Python lacks other languages’ abilities to create custom conditional block managers without dörty hacks.
The Problem
PEP-377: Allow __enter__()
methods to skip the statement body was proposed and declined a long time ago in a galaxy far away in 2009. Since then, many things changed, including some roots of the Python design philosophy.
Thus, I want to restart the discussion on the original post.
The biggest benefit this PEP could give is the ability to make custom conditional statements.
Use Case
This is helpful when wrappers with conditional body execution are used. These wrappers could have on-enter action(s), on-exit action(s), and on-skip action(s) – similar behaviour to the test methods in unittest
package.
Example
Suppose we have the following structure to occur multiple times across the codebase:
if (some_condition()):
print("Step 1: Executing code...")
try:
conditional_body()
except:
print("Step 1: Oops, something got wrong")
raise
else:
print("Step 1: Done")
else:
print("Step 1: Skipped")
The internal try-except-else
block could be extracted to the context manager level, but the outer if-else
could not unless you use the forbidden technique.
On Groovy
This is possible in many other languages, so here’s an implementation on Groovy:
// This can actually be a simple function instead of class
class ConditionalStep implements Serializable
{
def String name
def boolean condition
def call(Closure body)
{
if (condition)
{
println "$name..."
try
{
// If necessary, can provide access to this "ContextManager" via body.setDelegate(this)
body.call()
}
catch (e)
{
println "$name: FAILED"
throw e
}
println "$name: OK"
}
else
println "$name: SKIPPED"
}
@Override
def String toString()
{ "${this.class.simpleName}(name='$name', condition=$condition)" }
}
// or function which internally creates class
def conditionalStep(Map args, Closure body)
{ new ConditionalStep(args).call(body) }
new ConditionalStep(name: "Step 1", condition: a < b).call
{
conditionalBody()
}
// or, if defined as function and not class:
conditionalStep(name: "Step 1", condition: a < b)
{
conditionalBody()
}
Current Solutions
ContextManager (desired) way
Currently possible only via call stack manipulation.
with CondiditionalStep(name="Step 1", condition=a < b):
conditional_body()
For-loop way
The second option works better but does support returning the context manager as part of with ... as
syntax, and looks weirder.
for _ in ConditionalStep(name="Step 1", condition=a < b):
conditional_body()
ContextManager If-else way
with ConditionalStep(name="Step 1", condition=a < b) as step:
if step.ok:
conditional_body()
Lambdas or functions as arguments way
This works basically on pure Python but looks very ugly and can mess up the local variables namespace.
def step_body():
conditional_body_part_one()
conditional_body_part_two()
# ...
ConditionalStep(name="Step 1", condition=a < b).run(step_body)
I believe there are other use cases and/or implementations for this functionality, so feel free to discuss.