2015-06-29

Things I don't like about python: bools indexing into a list

Things I don't like about python

Bools indexing into a list

This is valid code, and does what you expect:

list_ = [ 'A', 'B', 'C' ]
print list_[False]  # 'A'
print list_[True]   # 'B'

... which is neat, but it upsets me a little bit. You index into things with integers, not floats, not strings, and certainly not bools. This smells more like Javascript than Python. If I want to use a bool to index values out I can do that with a dict.

2015-06-23

A Python object to detect when an imported module has had the source code changed

Detecting changes to loaded modules

I'm reworking the web server for my irrigation project. I've already got "kr", which restarts a program whenever it detects the source files have been updated, but first I need the existing web service to close down when there is a change to the source code. For this I implemented the code below, which defines a class. Calling an instance of the class will check the loaded modules (with custom filtering available) for changes to their source code. If a change is found, a callback is made. You can shut down your web server with the callback. The only tricky part is getting the event loop to periodically call the check function. It worked fine for my architecture that doesn't block indefinitely. Later I'll probably add an option for having a background thread do the monitoring.

The file is available on github here.


#!python

#Copyright 2015 Mark Santesson
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.


# Test with: kr.py *.py -c watch_modules.py


import os
import re
import sys


class ImportedModulesTimestampChecker(object):
    '''
       A class to help detect when one of the source files for a
    running process has changed.
       The returned object should be called periodically to let it check
    the timestamps of all files. There is, as of yet, no multi-threaded
    version.
       Typical use of this module would be to have it track the files
    being actively developed and to call a function resulting in program
    exit (perhaps sys.exit) when a change is detected. This works well
    when combined with "kr" to relaunch the program.

    '''
    def __init__(self, on_difference_fn, filename_filter_fn=None):
        '''
        Takes two parameters:
          on_difference_fn: a function taking the name of the file which
                      changed, which is called when the change is
                      detected.
          filename_filter_fn: a function taking a module's filename and
                      which should return True if the file's timestamp
                      should be observed. If the filename filter is a
                      string, then any filenames containing that string
                      will be observed. If not present, then all modules
                      will be tracked.
        '''
        self._onDifferenceFn = on_difference_fn

        if isinstance(filename_filter_fn, basestring):
            filename_filter_fn = lambda x: filename_filter_fn in x
        self._filenameFilterFn = filename_filter_fn or (lambda x:True)

        self._timestamps = {}

    def __call__(self):
        '''
        Call this periodically to do a check of the timestamps.
        '''
        # Get the timestamps on all matching files in this module's
        # directory. If any have changed, quit.
        all_files = [ x.__file__ for x in sys.modules.values()
                      if isinstance(x,type(sys))
                         and hasattr(x, '__file__')
                         and self._filenameFilterFn(x.__file__)
                    ]
        for module_name in sorted(all_files):
            module_name = re.sub(r'\.pyc$', r'.py', module_name)
            try:
                ts = os.stat(module_name).st_mtime
            except WindowsError:    # TODO: What is the error on Linux?
                logging.exception(module_name)
                pass
            else:
                if ts > self._timestamps.setdefault(module_name, ts):
                    self._onDifferenceFn(module_name)
                    self._timestamps[module_name] = ts


def main():
    import os.path
    import SocketServer
    import logging
    # From an example in the documentation for SocketServer.
    class TinyHandler(SocketServer.StreamRequestHandler):
        def handle(self):
            # Get one line.
            self.data = self.rfile.readline().strip()
            logging.info('Received: %r', self.data)
            out = self.data.lower()
            self.wfile.write(out)
            logging.info('Sending : %r', out)

    class TinySocketServer(SocketServer.TCPServer):
        def __init__(self):
            self._address = ("localhost", 9999)
            SocketServer.TCPServer.__init__( self
                                           , self._address
                                           , TinyHandler )
            self.timeout = 0.5
            self.quit = False
            self._timestampChecker = ImportedModulesTimestampChecker\
                    ( self.on_module_modification
                    , self.module_name_filter
                    )

        @staticmethod
        def module_name_filter(module_name):
            this_mod_name = os.path.basename(__file__).split('.')[0]
            return this_mod_name in module_name

        def on_module_modification(self, module_name):
            logging.info('Module %s was modified, exiting.', module_name)
            self.quit = True

        def run(self):
            logging.info('Listening at: %s', self._address)
            while not self.quit:
                self._timestampChecker()
                self.handle_request()
            logging.info('Quitting.')

    logging.getLogger().setLevel(20)
    tws = TinySocketServer()
    tws.run()

if __name__ == "__main__":
    main()