Source

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
# 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

  # 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'):
    try:
      __name__ = '__pyld_'+target+'__'
      exec(handle['code_object_'+target])
      __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')


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




if __name__ == '__main__':


  # handles for the monitored files
  observe_handles = []

  # scan interval for file changes
  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})

      # parse option arguments
      else:
        remaining_opt_param_count -= 1
        if last_opt_param == '-i':
          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()
  running = True
  while running:

    # calculate delta time since since last tick
    d = time.time() - last_tick

    # check files
    if d >= interval:

      for handle in observe_handles:
        __check_file(handle)

      last_tick = time.time()

    # sleep ...
    time.sleep(0)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.