Challenge du matin :
with DoNotRun():
print("Is never printed")
Oui je sais que if False: existe, mais j’aimerai vraiment un with pas un if ici.
Challenge du matin :
with DoNotRun():
print("Is never printed")
Oui je sais que if False: existe, mais j’aimerai vraiment un with pas un if ici.
Le mieux que j’ai réussi à faire, et je ne suis pas fier :
class DoNotRunException(Exception):
pass
class DoNotRun:
def __enter__(self):
raise DoNotRunException
def __exit__(self, exc_type, exc_value, traceback):
pass
with suppress(DoNotRunException), DoNotRun():
print("This won't run.")
C’est proche, mais c’est moche.
C’est marrant, j’ai eu cette discussion à plusieurs occasions lors de la PyCon.
Pour le moment ma meilleure solution consiste à combiner deux gestionnaire de contexte :
class Catch:
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, exc_tb):
print('Catch.__exit__')
return True
class Raise:
def __enter__(self):
raise ValueError
def __exit__(self, exc_type, exc_value, exc_tb):
print('Raise.__exit__')
with Catch(), Raise():
print('Hello')
Mais je cherche encore à réduire en un seul.
J’ai cette solution de contournement qui n’est pas encore 100% satisfaisante :
class FakeIterable:
def __iter__(self):
raise ValueError
class Catch:
def __enter__(self):
return FakeIterable()
def __exit__(self, exc_type, exc_value, exc_tb):
if exc_type is ValueError:
return True
with Catch() as (a,):
print(a)
Oh oh oh amusant ton contournement !
En me documentant sur le sujet je suis aussi tombé sur contextlib.nested : 28.7. contextlib — Utilities for with-statement contexts — Documentation Python 2.7.18
Et on voit que même là c’est un cas qui n’était pas géré, d’ailleurs une des raisons de sa dépréciation puis suppression : cpython/Lib/contextlib.py at 2.7 · python/cpython · GitHub
Tu t’ennuies ? ![]()
Et pour revenir sur le sujet, mon but à la fin c’est de définir quelque chose comme :
def func(block):
print('Before block')
block(3, 5)
print('After block')
with BlockCall(func) as (x, y):
print(x + y)
qui afficherait
Before block
8
After block
Pourquoi faire ça plutôt que d’utiliser des fonctions/lambdas ? Pour voir si c’est possible !
Si certain·e·s connaissent, c’est pour reproduire le mécanisme des blocs en Ruby.
Et donc pour ça j’ai besoin d’un bloc qui ne s’exécute pas (et dont j’enregistrerais l’AST pour reconstruire une fonction derrière).
On voit que là, comme j’ai un as (a, b) je peux utiliser mon astuce sur le __iter__ mais ça ne fonctionnerait pas si le bloc n’a pas besoin d’arguments.
Avec 6 charactères de plus, ça marche :
class BlockCall:
def __init__(self, func):
self.func = func
def __enter__(self):
capture = []
def block(*args):
capture.extend(args)
self.iterator = self.func(block)
self.iterator.send(None)
return capture
def __exit__(self, exc_type, exc_value, traceback):
try:
self.iterator.send(None)
except StopIteration:
pass
else:
raise RunTimeError(f"{self.func} should have a single yield")
def func(block):
print("Before block")
yield block(3, 5)
print("After block")
with BlockCall(func) as (x, y):
print(x + y)
Ah oui en effet, c’est moi qui n’ai pas assez spécifié le problème.
L’idée c’est qu’un bloc soit réutilisable (donc appelable zéro ou plusieurs fois), de façon à avoir quelque chose comme :
>>> def range_each(start, stop, block):
... for i in range(start, stop):
... block(i)
...
... with Block.call(range_each, 0, 10) as (i,):
... print(i, '!')
...
... with Block.call(range_each, 0, 0) as (i,):
... print(i, '?')
...
0 !
1 !
2 !
3 !
4 !
5 !
6 !
7 !
8 !
9 !
Actuellement j’arrive donc à m’en sortir avec le code bien sale suivant (sur lequel il faut encore que je travaille, notamment pour être compatible avec l’interpréteur interactif) :
import ast
import inspect
import itertools
import textwrap
from functools import partial
class _Stop(Exception):
pass
class _BlockArguments:
def __iter__(self):
raise _Stop
class _Block:
def __init__(self, exit_callable=None):
self.exit_callable = exit_callable
def __enter__(self):
caller_frame_info = inspect.stack()[1]
for module_frame_info in inspect.stack()[1:]:
if module_frame_info.function == '<module>':
break
source = inspect.getsource(module_frame_info.frame)
source_lines = source.splitlines()
first_line = caller_frame_info.lineno
block_lines = []
(block_def,) = ast.parse(source_lines[first_line - 1].strip() + ' pass').body
assert isinstance(block_def, ast.With)
(block_item,) = block_def.items
block_args = block_item.optional_vars
assert isinstance(block_args, ast.Tuple)
self.block_arg_names = [name.id for name in block_args.elts]
for i in itertools.count(first_line):
try:
line = source_lines[i]
except IndexError:
break
code = textwrap.dedent('\n'.join(block_lines + [line]))
if code.startswith(' '):
break
block_lines.append(line)
block_source = textwrap.dedent('\n'.join(block_lines))
self._code = compile(block_source, '<block>', 'exec')
return _BlockArguments()
def __exit__(self, exc_type, exc_val, exc_tb):
ret = exc_type and issubclass(exc_type, _Stop)
if ret and self.exit_callable is not None:
self.exit_callable(self)
return ret
def __call__(self, *args, **kwargs):
loc = locals() | kwargs | dict(zip(self.block_arg_names, args))
exec(self._code, locals=loc)
class Block(_Block):
def __init__(self):
super().__init__()
@staticmethod
def call(func, /, *args, **kwargs):
return _Block(partial(func, *args, **kwargs))
def range_each(start, stop, block):
for i in range(start, stop):
block(i)
with Block.call(range_each, 0, 10) as (i,):
print(i, '!')
with Block.call(range_each, 0, 0) as (i,):
print(i, '?')
with (block1 := Block()) as ():
print('block1 - foo')
print('block1 - bar')
def get_block():
with (block := Block()) as ():
print('block2 - foo')
print('block2 - bar')
return block
block2 = get_block()
print('tests')
block2()
block1()
block2()