Source

pyld / pyld.py

Full commit
uniqx 8897364 


uniqx e7886b6 

uniqx 8897364 
uniqx e7886b6 



uniqx 8897364 
uniqx e7886b6 



uniqx 8897364 
uniqx e7886b6 

uniqx 8897364 


uniqx 83fdc4b 
uniqx 8897364 
uniqx 079dfe7 
uniqx 8897364 
uniqx 83fdc4b 
uniqx 8897364 
uniqx 190878f 
uniqx 8897364 








uniqx e442c70 
uniqx 83fdc4b 
uniqx 8897364 

uniqx e7886b6 





uniqx 8897364 

uniqx e7886b6 


uniqx 8897364 

uniqx e7886b6 

uniqx 83fdc4b 

uniqx e7886b6 

uniqx 83fdc4b 

uniqx e7886b6 


uniqx 8897364 




uniqx 83fdc4b 









uniqx e7886b6 

uniqx e442c70 


uniqx cc5cd1b 





uniqx e7886b6 
uniqx cc5cd1b 



uniqx e7886b6 
uniqx e442c70 





uniqx 079dfe7 
uniqx cc5cd1b 
uniqx e7886b6 
uniqx e442c70 

uniqx e7886b6 
uniqx e442c70 


uniqx e7886b6 
uniqx cc5cd1b 
uniqx e442c70 

uniqx e7886b6 
uniqx e442c70 

uniqx cc5cd1b 
uniqx e442c70 
uniqx 190878f 
uniqx 5bbb696 
uniqx cc5cd1b 


uniqx 079dfe7 
uniqx cc5cd1b 






uniqx 83fdc4b 
uniqx e7886b6 
uniqx b077557 






uniqx 5bbb696 
uniqx b077557 

uniqx 190878f 
uniqx b077557 



uniqx 58902d8 
uniqx e7886b6 
uniqx 58902d8 





uniqx e7886b6 
uniqx 58902d8 










uniqx 5bbb696 

uniqx 58902d8 

uniqx e7886b6 
uniqx 079dfe7 
uniqx e442c70 
uniqx 079dfe7 


uniqx e442c70 
uniqx 079dfe7 
uniqx e442c70 
uniqx 83fdc4b 
uniqx 079dfe7 



uniqx 190878f 


uniqx 079dfe7 
uniqx 8897364 

uniqx 5bbb696 


uniqx 8897364 
uniqx e442c70 
uniqx 8897364 
uniqx 83fdc4b 

uniqx 190878f 
uniqx 8897364 
uniqx 83fdc4b 





uniqx 8897364 

uniqx 5bbb696 


uniqx 8897364 






uniqx 83fdc4b 
uniqx 8897364 




uniqx cc5cd1b 
uniqx 83fdc4b 
uniqx 8897364 
uniqx cc5cd1b 
uniqx 83fdc4b 
uniqx cc5cd1b 
uniqx 83fdc4b 

uniqx 8897364 
uniqx 190878f 
uniqx 8897364 



uniqx 83fdc4b 
uniqx 190878f 
uniqx 83fdc4b 

uniqx 8897364 

uniqx 83fdc4b 


uniqx 8897364 


uniqx 5bbb696 


uniqx 8897364 
uniqx 190878f 

uniqx 8897364 


uniqx 190878f 


uniqx 8897364 
uniqx c9f5dc3 
uniqx 190878f 
uniqx 8897364 
uniqx c9f5dc3 

uniqx 8897364 
uniqx 190878f 
uniqx 8897364 
uniqx 5bbb696 



uniqx c9f5dc3 

uniqx 8897364 
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 uniqx
#  Metalab, 1010 Vienna, Austria
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os
import re
import sys
import ast
import time
import tarfile


#
# main application
#

def __print_help_msg():
  print '''
NAME
       pyld - Python Live Daemon

SYNOPSIS
       pyld [-i INTERVAL] [-n] [-r RECORD] FILE...

DESCRIPTION
       pyld is a live coding tool for python. It is designed to run as a
       daemon so you may use your favourite editor for coding. pyld
       executes a python script and monitors it constantly. When a change
       to the script file is detected pyld will attempt to monkey-patch
       the running script. Also a copy of the script will be added to a
       version history archive if changes are detected.

       FILE
              The file(s) which should be executed and monitored for
              changes. If a file does not exist, pyld will still look for
              the file and execute/monitor it when it is created.

      -i INTERVAL, --interval INTERVAL
              The interval for scanning the files in seconds.
              e.g. 0.1 will result in 10 scans per second. default: 0.33

      -n, --no-record
              Instructs pyld to do not record your live session to a tar
              file. If -r is set, the -n option will be ignored.

      -r RECORD, --record RECORD
              Records your live session to a tar file. Defaults to create
              a archive with a with the name of the current folder and a
              time stamp. If -r is set, the -n option will be ignored.
'''

def __print_error_msg(err_msg):
  print '\n', err_msg, '\n for more help see: pyld -h'

def __crop_name(p):
  '''crops the file name from the given path (without file extension)'''
  return re.split('\.',os.path.split(p)[-1])[-2]

def __crop_ext(p):
  '''crops the file extension from the given path'''
  return re.split('\.',p)[-1]



def __check_file(handle):
  '''checks if a file has changed and calls __refresh_file if necessary.

  :param dict handle: meta data for a monitored python script
  '''

  if os.path.exists(handle['path']):
    #print handle['path'], 'file exists'

    mtime = os.path.getmtime(handle['path'])
    if handle['mtime'] < mtime:
      __refresh_file(handle,mtime)
  else:
    print handle['path'], 'does not exits'
    handle['init'] = False

def __refresh_file(handle,mtime):
  '''refresh script file and execute it.

  :param dict handle: meta data for a monitored python script
  :param number mtime: new modification time
  '''

  # unload if already initialized
  if handle['init']:
    __exec_code_object(handle,'unload')

  # (re)read the file
  __read_and_compile_script(handle)

  # initialize if not yet initialized
  if not handle['init']:
    __exec_code_object(handle,'init')
    handle['init'] = True

  # exec reload
  __exec_code_object(handle,'reload')

  # update modification time
  handle['mtime'] = mtime

  #print handle

  # archive
  if arch_path != '':
    try:
      tar = tarfile.open(arch_path, 'a')
      tar.add(name=handle['path'],arcname=__crop_name(handle['path'])+'.'+str(int(time.time()))+'.'+__crop_ext(handle['path']))
    finally:
      try:
        tar.close()
      except:
        pass


def __exec_code_object(handle,target):
  '''executes a prepared code object that is stored inside the handle already.
  if no code object for the given target exists, calling this function will
  have no effect, but won't fail either.

  :param str target: valid target are: init, reload, unload
  '''

  if target in ('init','reload','unload','tick'):
    try:
      __name__ = '__pyld_'+target+'__'
      exec(handle['code_object_'+target],global_vars,handle['local_vars'])
      __name__ = '__main__'
    except KeyError:
      pass


def __read_and_compile_script(handle):
  '''reads a script file, parses it and creates code objects for
  of the relevant part from a script.

  :param dict handle: meta data for a monitored python script
  '''

  src = __read_text_file(handle['path'])

  for block in ast.parse(src).body:

    if 'test' in dir(block):
      dump = ast.dump(block.test)
      if '__pyld_init__' in dump:
        handle['code_object_init']   = compile(ast.Module(block.body),os.path.split(handle['path'])[-1],'exec')
      elif '__pyld_reload__' in dump:
        handle['code_object_reload'] = compile(ast.Module(block.body),os.path.split(handle['path'])[-1],'exec')
      elif '__pyld_unload__' in dump:
        handle['code_object_unload'] = compile(ast.Module(block.body),os.path.split(handle['path'])[-1],'exec')
      elif '__pyld_tick__' in dump:
        handle['code_object_tick'] = compile(ast.Module(block.body),os.path.split(handle['path'])[-1],'exec')


def __read_text_file(path):
  '''reads a text file and returns it as a string'''

  ret = ''

  with open(path,'r') as f:
    for line in f:
      ret += line
  f.closed

  return ret



# global vars shared by all files that are observed by pyld
global_vars = {}


if __name__ == '__main__':

  ##
  ## local variables
  ##

  # handles for the monitored files
  observe_handles = []

  # scan interval for file changes
  scan_interval = 0.33

  # path for the archive where the changesets are stored
  arch_path = '' #os.path.split(os.getcwd())[-1] + '.' + str(int(time.time())) + '.tar'
  # opt out recording flag
  record_opt_out = False




  ##
  ## parse arguments
  ##

  remaining_opt_param_count = 0
  last_opt_param = ''
  for arg in sys.argv:

    if arg != 'pyld.py':

      # parse options and files
      if remaining_opt_param_count == 0:
        if arg[0] == '-':
          if arg == '--help' or arg == '-h':
            __print_help_msg()
            quit(0)
          elif arg == '-i' or arg == '--interval':
            last_opt_param = '-i'
            remaining_opt_param_count = 1
          elif arg == '-n':
            record_opt_out = True
          elif arg == '-r' or '--record':
            last_opt_param = '-r'
            remaining_opt_param_count = 1
        else:
          observe_handles.append({'path':arg,'mtime':0.0,'init':False,'local_vars':{}})

      # parse option arguments
      else:
        remaining_opt_param_count -= 1
        if last_opt_param == '-i':
          scan_interval = float(arg)
        if last_opt_param == '-r':
          arch_path = arg


  if arch_path == '' and not record_opt_out:
    arch_path = os.path.split(os.getcwd())[-1] + '.' + str(int(time.time())) + '.tar'




  ##
  ## main loop
  ##

  last_tick = time.time() # tick time
  last_scan = last_tick # scan time
  running = True
  while running:

    # calculate delta time since last tick
    tick_delta = time.time() - last_tick
    scan_delta = time.time() - last_scan

    # check files
    if scan_delta >= scan_interval:

      for handle in observe_handles:
        __check_file(handle)

      last_scan = time.time()

    # exec ticks
    for handle in observe_handles:
      __exec_code_object(handle,'tick')

    # sleep ...
    time.sleep(0)