prompt-toolkit / ptpython

A better Python REPL
BSD 3-Clause "New" or "Revised" License
5.23k stars 281 forks source link

How do I save it to a file? #138

Open Ryanb58 opened 8 years ago

Ryanb58 commented 8 years ago

Say I am writing something as I go in ptpython.. how can I go about saving the code I typed in and ran here to a file for later use?

Is this possible with ptpython?

Duality4Y commented 8 years ago

i am curious too, what i have been doing is taking the sources out of

~/.ptpython/history
Ryanb58 commented 8 years ago

@Duality4Y I like what you have been doing but it isn't very optimal. Plus then you have to manually remove the + symbol from each line.. :/

I believe adding a shortcut to the menu that would allow us to save the code typed in during the current session to a file would meet my specific expectations. What do you think?

Duality4Y commented 8 years ago

yes, that would be pretty nice and handy.

mandarg commented 8 years ago

By the way, this is an easy way to remove all the additional stuff from the history and get a quick summary (I'm sure there are tons of better ones).

cat ~/.ptpython/history | sed 's/^\+//' | sed 's/^\#.*//' | tr -s '\n'
jonathanslenders commented 8 years ago

Hi all, thanks a lot for this feature request! It's definitely something useful. And right now, there is indeed no easy way to do this. (Except for looking into ~/.ptpython/history.)

I'll have a look at what options we have and maybe add a save button somewhere. (Last few weeks I've been really busy, so development here is slowing down a little for the moment.)

edit: Maybe I'll make the whole session input available as a string variable in the session (Similar to IPython's "In" and "Out" variables.) Then you can easily save it by typing open(filename).write(input_data). Or make a save function from that in your Python startup file. I'll think about it.

kiddten commented 7 years ago

Any news? Thoughts?

harsha-surukam commented 7 years ago

@jonathanslenders This would be a handy feature to have. Let me know if you want me to code this up. I like your approach of storing the session input in a string variable.

ProbonoBonobo commented 6 years ago

Here's a quick/dirty function that chunks the history file into a vector of features:

import os
import re
from itertools import grouper
def load_ptpython_history(fp=os.path.sep.join([os.getenv('HOME'),".ptpython/history"])):
       delimiter = re.compile(r'\n*\n#\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.\
       with open(fp, "r") as f:
       raw = f.read().replace("\n+", "\n") # nix the continuation operator (a '+' sign)
           return grouper(re.split(delimiter, raw)[1:], 7)

 defs = list(load_ptpython_history())

Demo usage:

In [85]: defs[0]
Out[85]: ('2017', '10', '01', '13', '59', '21', 'globals().keys()')
In [86]: eval(defs[0][-1])
Out[86]: dict_keys(['__name__', '__doc__', '__package__', '__loader__',

This tries to solve the opposite problem, of "well heck, now that my session is saved to a log, how can I deserialize the previous session's contents?" It's more of a proof-of-concept, and I hope to do more with it later. In addition to fetching the previous session's contents it also suggests a possible workaround for the fact that inspect.getsourcelines can't typically locate the source code of functions/objects defined within the same session (a perennial frustration of mine unrelated to ptpython). I'm very open to ideas where to go with it, but my dream is eventually to commit each row (at least those worth saving) to a namespaced, version-controlled database that checks for things like unscoped variables, global dependencies, etc., probably just by blocking an attempt to write if the function doesn't exec in an empty environment. Usage might look something like

>>> from ptpython import VCM
>>> vcm = VCM(ns='math')
math
>>> vcm.knows('square')
True
>>> vcm.exec('square')
{  'ns'  : 'math', 
   'fname' : 'square',
   'v' : '0.3',
   'deps' : [],
   'src' : ['def square(x):', '    return x*x+1'],
   'params' : ['x'],
   'tests' : [(4, 16), (-1, 1)]
}
>>> square(5)
26 # oh no
>>> vcm.rollback('square')
{  'ns' : 'math', 
   'v' : '0.2',
   'deps' : [],
   'src' : ['def square(x):', '    return x*x'],
   'fname'  : 'square',
   'params' : ['x'],
   'tests'   : [(4, 16), (-1, 1)]
}
>>> vcm.run_tests('square')
{'v' : 0.0.2,
 'src' : ['def square(x):', '   return x*x'],
 'ok' : True,
 'passed' : True,
 'source' : 'def square(x):\n   return x*x',
 'verbose' : ['Testing function square with source:\n'
             '\n'
             'def square(x):\n'
             '   return x*x\n'
             '\n',
             '=========== TEST #0/2: OK ==========   \n'
             "  Now evaluating the expression '4'.\n"
             '    Args: square(4)\n'
             '    Expecting: 16\n'
             '    Got: 16\n'
             '    Passed: True\n'
             '\n',
             '=========== TEST #1/2: OK ==========   \n'
             "  Now evaluating the expression '-1'.\n"
             '    Args: square(-1)\n'
             '    Expecting: 1\n'
             '    Got: 1\n'
             '    Passed: True\n'
             '\n',
             '\n\nPassed 2 out of 2 tests in 0.0001s.']}
>>> def cube(x):
           return x**3
>>> vcm.add('cube')
ok
>>> vcm.commit('cube', m="maybe a commit message")
...

@jonathanslenders thanks for this wonderful tool. I'm happy to contribute these changes to the repo if you see a place for them here -- or would releasing as an optional extension to ptpython be best? Use of the tool I'm imagining would require having Postgres or mySQL installed locally, but since you're already writing to the history log on each input, I don't imagine this really needs to share state with ptpython itself... could just hang back and periodically check the log file for changes in a separate Python process.

danieldjewell commented 3 years ago

I'm curious -- can IPython's %notebook abc.ipynb and nbconvert --to python abc.ipynb like functionality be used here? (or maybe as an inspiration?)

One of the major differences between ptpython and ipython in terms of history saving is the format: ipython saves history into an sqlite3 database for each session (which has the advantage of being able to keep several simultaneous sessions separate) -- as of now, ptpython mingles the history from two different sessions into the history log file. Not saying that sqlite3 is the only way to keep this separate but it does have some advantages.

I'm very impressed so far with how much more responsive ptpython is compared to ipython (especially for completion) ...

ProbonoBonobo commented 3 years ago

I was happy to get a bump notification for this. I've made lots of tweaks to ptpython since my last post, enough that my local fork is a bit of a mess. I did manage to implement a session-based history serializer at one point, but I seem to recall there was a big update a few years ago which borked my crude hacks and I had to revert to master.

I've tinkered with the code quite a bit over the years, and have a slightly better understanding of the challenges. Some of the things that make ptpython awesome, like its support for iPython macros and shell commands, mean that not every valid REPL entry is valid Python. There's also, of course, magic methods like quit() which need to be erased before that file can be evaluated. My biggest fear is the disturbingly high possibility that calling eval/exec on some previous REPL session's transcript might cause horrific side effects, like filesystem modification, owing to the fact that I use ptpython as a replacement shell a lot. I think that this can probably be prevented by restoring previous sessions from globals(), but I'm not sure that I could ever be 100% certain that a restoring from a previous state wouldn't cause nasty side effects.

One option I've considered, though, and could be interesting, would be to use cloudpickle to serialize/deserialize the session environment. IIRC I did encounter a strange bug when a callable object that references globals() is reinitialized in another ptpython process, and sort of stopped there, but it's totally possible that my elaborate config.py is to blame for that. I'd be interested to try this again though.