1. uniqx
  2. pyld


pyld / pyld.py

#! /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
# 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
import multiprocessing

# main application

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

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

       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.

              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:
    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']:

  # (re)read the file

  # initialize if not yet initialized
  if not handle['init']:
    handle['init'] = True

  # exec reload

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

  #print handle

  # archive
  if arch_path != '':
      tar = tarfile.open(arch_path, 'a')

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'):
      __name__ = '__pyld_'+target+'__'
      __name__ = '__main__'
    except KeyError:

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

  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':
          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

      # parse option arguments
        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:

      last_scan = time.time()

    # exec ticks
    for handle in observe_handles:

    # sleep ...