Skip to content

Commit

Permalink
Fill mid-scope empty lines with spaces (fixes #2)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergei-mironov committed Sep 12, 2023
1 parent b8f99e2 commit ab9d662
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 36 deletions.
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions python/bin/litrepl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from litrepl import *
from litrepl import __version__
from os import chdir
from subprocess import check_output, DEVNULL, CalledProcessError
from argparse import ArgumentParser

LOCSHELP='(N|$|N..N)[,(...)] where N is either: number,$,ROW:COL'

Expand Down
70 changes: 52 additions & 18 deletions python/litrepl/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import fcntl

from copy import deepcopy
from typing import List, Optional, Tuple, Set, Dict, Callable
from typing import List, Optional, Tuple, Set, Dict, Callable, Any
from re import search, match as re_match
from select import select
from os import environ, system
Expand All @@ -26,20 +26,22 @@
processCont)

DEBUG:bool=False
LitreplArgs=Any

def pdebug(*args,**kwargs):
if DEBUG:
print(*args, file=sys.stderr, **kwargs, flush=True)

def pipenames(a)->FileNames:
def pipenames(a:LitreplArgs)->FileNames:
""" Return file names of in.pipe, out.pip and log """
auxdir=a.auxdir if a.auxdir is not None else \
join(gettempdir(),f"litrepl_{getuid()}_"+
sha256(getcwd().encode('utf-8')).hexdigest()[:6])
return FileNames(auxdir, join(auxdir,"_in.pipe"), join(auxdir,"_out.pipe"),
join(auxdir,"_pid.txt"))

def fork_python(a, name):
def fork_python(a:LitreplArgs, name:str):
""" Forks an instance of Python interpreter `name` """
assert 'python' in name
wd,inp,outp,pid=astuple(pipenames(a))
system((f'{name} -uic "import os; import sys; sys.ps1=\'\'; sys.ps2=\'\';'
Expand All @@ -53,7 +55,8 @@ def fork_python(a, name):
'_=signal.signal(signal.SIGINT,_handler)\n')
exit(0)

def fork_ipython(a, name):
def fork_ipython(a:LitreplArgs, name:str):
""" Forks an instance of IPython interpreter `name` """
assert 'ipython' in name
wd,inp,outp,pid=astuple(pipenames(a))
cfg=join(wd,'litrepl_ipython_config.py')
Expand Down Expand Up @@ -86,7 +89,7 @@ def fork_ipython(a, name):
)
exit(0)

def start_(a, fork_handler:Callable[...,None])->None:
def start_(a:LitreplArgs, fork_handler:Callable[...,None])->None:
""" Starts the background Python interpreter. Kill an existing interpreter if
any. Creates files `_inp.pipe`, `_out.pipe`, `_pid.txt`."""
wd,inp,outp,pid=astuple(pipenames(a))
Expand All @@ -105,7 +108,7 @@ def start_(a, fork_handler:Callable[...,None])->None:
if not isfile(pid):
raise ValueError(f"Couldn't see '{pid}'. Did the fork fail?")

def start(a):
def start(a:LitreplArgs):
if 'ipython' in a.interpreter:
start_(a, partial(fork_ipython,a=a,name=a.interpreter))
elif 'python' in a.interpreter:
Expand All @@ -118,12 +121,12 @@ def start(a):
else:
raise ValueError(f"Unsupported interpreter '{a.interpreter}'")

def running(a)->bool:
def running(a:LitreplArgs)->bool:
""" Checks if the background session was run or not. """
wd,inp,outp,pid=astuple(pipenames(a))
return 0==system(f"test -f '{pid}' && test -p '{inp}' && test -p '{outp}'")

def stop(a):
def stop(a:LitreplArgs)->None:
""" Stops the background Python session. """
wd,inp,outp,pid=astuple(pipenames(a))
system(f'kill "$(cat {pid})" >/dev/null 2>&1')
Expand Down Expand Up @@ -261,31 +264,62 @@ def cursor_within(pos, posA, posB)->bool:
else:
return False

def unindent(col:int,lines:str)->str:
def unindent(col:int, lines:str)->str:
def _rmspaces(l):
return l[col:] if l.startswith(' '*col) else l
return '\n'.join(map(_rmspaces,lines.split('\n')))

def indent(col,lines:str)->str:
def indent(col:int, lines:str)->str:
return '\n'.join([' '*col+l for l in lines.split('\n')])

def escape(text,pat):
def escape(text, pat:str):
""" Escapes every letter of a pattern with (\) """
epat=''.join(['\\'+c for c in pat])
return text.replace(pat,epat)

def eval_code(a, code:str, runr:Optional[RunResult]=None) -> str:
LEADSPACES=re.compile('^([ \t]*)')

def fillspaces(code:str, suffix:str)->str:
""" Replace empty lines of multi-line code snippet with lines filled with
previous line's leading spaces, followed by suffix (e.g. a Python comment)."""
def _leadspaces(line:str)->str:
m=re_match(LEADSPACES,line)
return str(m.group(1)) if m else ''
lines=code.split('\n')
if len(lines)<=0:
return code
acc=[lines[0]]
nempty=0
spaces=_leadspaces(lines[0])
for line in lines[1:]:
if len(line)==0:
nempty+=1
else:
spaces2=_leadspaces(line)
if nempty>0:
acc.extend(['' if len(spaces2)<len(spaces) else spaces+suffix]*nempty)
nempty=0
acc.append(line)
spaces=spaces2
acc.extend(['']*nempty)
return '\n'.join(acc)

def eval_code(a:LitreplArgs, code:str, runr:Optional[RunResult]=None) -> str:
""" Start or complete the code snippet evaluation process.
`RunResult` may contain the existing runner's context. Alternatively, the
reference to the context could be encoded in the code section itself.
The function returns either the evaluation result or the running context
encoded in the result for later reference.
"""
fns=pipenames(a)
if runr is None:
code2,runr=rresultLoad(code)
else:
code2=code
if runr is None:
rr,runr=processAdapt(fns,code2,a.timeout_initial)
rr,runr=processAdapt(fns,fillspaces(code, '# spaces'),a.timeout_initial)
else:
rr=processCont(fns,runr,a.timeout_continue)
return rresultSave(rr.text,runr) if rr.timeout else rr.text

def eval_section_(a, tree, secrec:SecRec)->None:
def eval_section_(a:LitreplArgs, tree, secrec:SecRec)->None:
""" Evaluate sections as specify by the `secrec` request. """
fns=pipenames(a)
nsecs=secrec.nsecs
Expand Down
18 changes: 9 additions & 9 deletions python/litrepl/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ def pusererror(fname,err)->None:
with open(fname,"w") as f:
f.write(err)

def processAsync(fns:FileNames, lines:str)->RunResult:
""" Send `lines` to the interpreter and fork the response reader """
def processAsync(fns:FileNames, code:str)->RunResult:
""" Send `code` to the interpreter and fork the response reader """
wd,inp,outp,pidf=astuple(fns)
codehash=abs(hash(lines))
codehash=abs(hash(code))
fname=join(wd,f"litrepl_eval_{codehash}.txt")
pdebug(f"Interacting via {fname}")
pattern=PATTERN
Expand All @@ -147,7 +147,7 @@ def processAsync(fns:FileNames, lines:str)->RunResult:
def _handler(signum,frame):
pass
signal(SIGINT,_handler)
interact(fdr,fdw,lines,fo,pattern)
interact(fdr,fdw,code,fo,pattern)
except BlockingIOError:
pusererror(fname,"ERROR: litrepl.py couldn't lock the sessions pipes\n")
finally:
Expand Down Expand Up @@ -235,12 +235,12 @@ def processCont(fns:FileNames, r:RunResult, timeout:float=1.0)->ReadResult:
os.close(fdr)

def processAdapt(fns:FileNames,
lines:str,
code:str,
timeout:float=1.0)->Tuple[ReadResult,RunResult]:
""" Push `lines` to the interpreter and wait for `timeout` seconds for
immediate answer. In case of delay, return intermediate answer with
the continuation."""
runr=processAsync(fns,lines)
""" Push `code` to the interpreter and wait for `timeout` seconds for
the immediate answer. In case of delay, return intermediate answer and
the continuation context."""
runr=processAsync(fns,code)
rr=processCont(fns,runr,timeout=timeout)
return rr,runr

Expand Down
13 changes: 7 additions & 6 deletions python/litrepl/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ class PrepInfo:
@dataclass
class SecRec:
""" Request for section evaluation """
nsecs:Set[NSec]
pending:Dict[NSec,RunResult]
nsecs:Set[NSec] # Sections to evaluate
pending:Dict[NSec,RunResult] # Contexts of already running sections

@dataclass
class FileNames:
wd:str
inp:str
outp:str
pidf:str
""" Interpreter filenames """
wd:str # Working directory
inp:str # Input pipe
outp:str # Output pipe
pidf:str # File containing PID
20 changes: 20 additions & 0 deletions sh/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,25 @@ EOF
runlitrepl stop
)} #}}}

test_eval_with_empty_lines() {( #{{{
mktest "_test_eval_code"
runlitrepl start
cat >source.py <<"EOF"
def hello():
var = 33 # EMPTY LINE BELOW
print(f"Hello, {var}!")
hello()
EOF
cat source.py | runlitrepl eval-code >out.txt
diff -u out.txt - <<"EOF"
Hello, 33!
EOF
runlitrepl stop
)} #}}}

interpreters() {
echo "$(which python)"
echo "$(which ipython)"
Expand All @@ -399,6 +418,7 @@ tests() {
echo test_eval_tex
echo test_async
echo test_eval_code
echo test_eval_with_empty_lines
}

runlitrepl() {
Expand Down

0 comments on commit ab9d662

Please sign in to comment.