Commits

uniqx committed 4b6c5ef

pyld as package rather in a single file; split functionality into separate objects; added some nosetests

Comments (0)

Files changed (7)

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
-import multiprocessing
-
-
-#
-# 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)
-
-
+
+
+
+__author__ = 'uniqx <uniqx@fused.at>'
+__version__ = '0.1'
+__website__ = 'https://bitbucket.org/uniqx/pyld'
+__license__ = 'AGPL 3'
+
+__all__ = [
+    'PyldProcess','PyldFile',
+]
+
+
+#! /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 ast
+
+from multiprocessing import Process
+
+class PyldFile():
+
+  def __init__(self,file_name,global_vars={}):
+
+    self.local_vars = {}
+    self.global_vars = global_vars
+    self.file_name = file_name
+
+    self._read_and_compile_code_objects(True)
+
+  def _read_and_compile_code_objects(self, compile_init=False):
+    """Reads the script file at ``file_name`` and builds code objetcs
+    for the relevant parts of the script. Is ``compile_init`` set to
+    True the code object for __init_pyld__ will also be compiled.
+    """
+
+    src = self.__read_text_file_to_string(self.file_name)
+
+    for block in ast.parse(src).body:
+
+      if 'test' in dir(block):
+        dump = ast.dump(block.test)
+        if compile_init and '__pyld_init__' in dump:
+          self.__init_co = compile(ast.Module(block.body),
+            os.path.basename(self.file_name),'exec')
+        elif '__pyld_reload__' in dump:
+          self.__reload_co = compile(ast.Module(block.body),
+            os.path.basename(self.file_name),'exec')
+        elif '__pyld_unload__' in dump:
+          self.__unload_co = compile(ast.Module(block.body),
+            os.path.basename(self.file_name),'exec')
+        elif '__pyld_tick__' in dump:
+          self.__tick_co = compile(ast.Module(block.body),
+            os.path.basename(self.file_name),'exec')
+
+  def __read_text_file_to_string(self,path):
+    """This function reads the entire file located at ``path`` and returns
+    it as string.
+    """
+
+    ret = ''
+
+    with open(path,'r') as f:
+      ret = '\n'.join([ line for line in f ])
+    f.closed
+
+    return ret
+
+
+  def _execute_init(self):
+    exec(self.__init_co,self.global_vars,self.local_vars)
+
+  def _execute_reload(self):
+    exec(self.__reload_co,self.global_vars,self.local_vars)
+
+  def _execute_unload(self):
+    exec(self.__unload_co,self.global_vars,self.local_vars)
+
+  def _execute_tick(self):
+    exec(self.__tick_co,self.global_vars,self.local_vars)
+
+
+
+
+
+
+class PyldProcess(Process):
+
+  def __init__(self, file_name_list):
+    """PyldProcess will execute all files from ``file_name_list``.
+    """
+
+    Process.__init__(self)
+    self.files = {}
+
+    for file_name in file_name_list:
+
+      self.files[file_name] = PyldFile(file_name)
+
+
+
+  def run(self):
+    while True:
+      #print self.x
+      time.sleep(1)
+
+  def i(self):
+    self.x += 1
+
+
+#! /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
+
+from multiprocessing import Process
+
+
+#
+# classes
+#
+
+class PyldProcess(Process):
+
+  def __init__(self, file_name_list):
+
+    Process.__init__(self)
+
+    for file_name in file_name_list:
+      
+    self.x = 0
+    pass
+
+  def run(self):
+    while True:
+      print self.x
+      time.sleep(1)
+
+  def i(self):
+    self.x += 1
+
+  def set_code_objects(reload_co,unload_co,tick_co):
+    self.reload_co = reload_co
+    self.unload_co = unload_co
+    slef.tick_co = tock_co
+
+#
+# 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)
+
+

tests/pyldFile.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/>.
+#
+
+from nose.tools import assert_not_equal, assert_equal
+
+from pyld.process import PyldFile
+
+def test_new_instance():
+
+  f = PyldFile('examples/simple/test-1-pyld.py')
+  assert_not_equal(f,None)
+
+def test_exec_init():
+
+  f = PyldFile('examples/simple/test-1-pyld.py')
+  f._execute_init()
+
+def test_exec_basic_life_cicle():
+
+  f = PyldFile('examples/simple/test-1-pyld.py')
+
+  f._execute_init()
+  assert_equal(42,f.global_vars['the_global_number'])
+  assert_equal(0,f.local_vars['test_1_var'])
+
+  f._execute_reload()
+
+  for i in range(50):
+    f._execute_tick()
+  assert_equal(50,f.local_vars['test_1_var'])
+
+  f._execute_unload()
+
+def test_reloading():
+
+  f = PyldFile('examples/simple/test-1-pyld.py')
+
+  f._execute_init()
+  assert_equal(42,f.global_vars['the_global_number'])
+  assert_equal(0,f.local_vars['test_1_var'])
+
+  f._execute_reload()
+
+  for i in range(50):
+    f._execute_tick()
+  assert_equal(50,f.local_vars['test_1_var'])
+
+  f._execute_unload()
+
+  # reload the file
+  f._read_and_compile_code_objects()
+
+  f._execute_reload()
+
+  assert_equal(0,f.local_vars['test_1_var'])
+
+

tests/pyldProcess.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/>.
+#
+
+from nose.tools import assert_not_equal, assert_equal
+
+from pyld.process import PyldProcess
+
+def test_new_instance():
+
+  p = PyldProcess([
+      'examples/simple/test-1-pyld.py',
+      'examples/simple/test-1-pyld.py',
+    ])
+  assert_not_equal(p,None)
+
+
+
+
+
+
+
+

tests/run_all_tests.sh

+#! /bin/bash
+
+# cd into pylds project directory
+SOURCE="${BASH_SOURCE[0]}"
+while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done
+DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
+cd $DIR/..
+
+
+nosetests --version
+
+if [ "$?" -eq 0 ] ; then
+
+  nosetests tests/pyldFile.py tests/pyldProcess.py
+
+else
+
+  echo 'Please install nosetests ...'
+
+fi