mitsuhiko / mitsuhiko/python-logical-call-context
Accounting for possible integration with the frame stack in 3.7+
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 9
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
While I realise logical context management is something that needs to be solved for the async framework case in at least 3.5+ (and perhaps even 2.7 if we can find a way to manage it), a possibility I've been discussing with @njsmith and @1st1 is the idea of solving this problem in the general case for Python 3.7+, such that code like:
def rounded_decimals(iterable, precision):
with decimal.localcontext() as ctx:
ctx.prec = precision
for value in iterable:
yield +value
doesn't break code like the following the way it does today.
def surpising_state_changes(iterable):
# Original decimal context applies here
for value in rounded_decimals(iterable, precision=10):
# Oops, the decimal context precision change applies here
if value > 10: break
# Worse, the decimal context here depends on:
# - whether or not the generator was exhausted
# (if it was, precision will have been reverted)
# - whether or not the interpreter uses a reference counting GC
# (if it does, precision will have been reverted even if we broke out of the loop)
Similarly, it would be desirable to support deterministic frame-linked cleanup in cases like:
def currently_nondeterministic_cleanup(fname):
# File cleanup here is non-deterministic if the generator isn't run to completion
# And you're not using a refcounted implementation like CPython
# However, you won't get a ResourceWarning about it: http://bugs.python.org/issue28629
with open(fname) as f:
yield from fname
The reason I think this is relevant to your API design sketch here is that it means we probably need to set out two different sets of design goals:
-
for 3.5+ (and hopefully 2.7), make good logical context management possible if you're diligent about creating and destroying logical contexts at the appropriate time. This will likely only happen consistently if you're using an asynchronous framework that manages them for you (e.g. asyncio, tulip, curio, Twisted, Tornado)
-
for 3.7+, aim to make good logical context management straightforward by implicitly linking it to execution frame lifecycles if you don't otherwise manage it explicitly, as the differences in frame lifecycle between synchronous and asynchronous code are one of the main causes of resource management and shared state manipulation problems
To better explain the latter point, one of the key differences between synchronous code and asynchronous code in Python is that:
-
In synchronous code, the Python level frame stack aligns 100% with the C level function call stack, so you know not only that thread local state will outlive the execution lifecycle of the current frame, but also that any changes you make to thread local state will be invisible to other code as long as you revert them before the frame finishes execution. Thus the "change system state" + "revert state change" model supported by context managers is sufficient for both resource management and thread local state management.
-
In asynchronous code (i.e. generators, coroutines and callback chains), there are two different Python level frame stacks - the traditional synchronous one that tracks "this is what is executing in this thread right now" (i.e. the one
sys._getframe()andinspect.currentframe()grant access to), and the logical asynchronous one, which currently isn't exposed for introspection in any consistent manner.
Thus, in asynchronous code, both of those core assumptions about the relationship between frame local state and thread local state are broken, and they're broken in essentially the same way that assumptions about safely accessing process global state from a synchronous function call are broken by multi-processing and multi-threading:
- you can't be sure every invocation of the frame will see the same thread local state (the multi-processing problem)
- if you suspend frame execution in any way, other code may see your modifications to the thread local state even if you revert those changes before returning your final result (the multi-threading problem)
Python 3.5+ provides at least partial visibility into the logical call stack, in that given a currently running coroutine or generator, you can trace the stack down via the gi_yieldfrom and cr_await attributes, however there's currently no way to do the reverse lookup at runtime:
- running frames don't have a reference back to the object that originated them (whether that's a function, coroutine, or generator-iterator): http://bugs.python.org/issue12857
- there aren't any
gi_backorcr_backattributes to trace the logical call stack backwards foryield fromandawait
Given a robust logical context management system as a foundation, I think the above problems are solvable given some additional changes to the frame management requirements defined at the language level, but I also think there are going to be limits to how easy to use a logical context management system can be without those language level changes.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No repository files or tests are named. Start by examining the proposed Python 3.5+/3.7 frame and logical-context behavior, including gi_yieldfrom, cr_await, sys._getframe(), and inspect.currentframe(); the issue needs an agreed design and concrete acceptance criteria before implementation can be considered done.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100