libtpclient-py-0.3.2/0000755000175000017500000000000011164431420012613 5ustar timtimlibtpclient-py-0.3.2/setup.cfg0000644000175000017500000000007311164431420014434 0ustar timtim[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 libtpclient-py-0.3.2/PKG-INFO0000644000175000017500000000120511164431420013706 0ustar timtimMetadata-Version: 1.0 Name: libtpclient-py Version: 0.3.2 Summary: Client support library for Thousand Parsec Home-page: http://www.thousandparsec.net Author: Tim Ansell Author-email: tim@thousandparsec.net License: GPL Description: A library of code to support quick development of Clients for Thousand Parsec. Includes support for: * Classes of keeping a download cache of the universe (including automatic update) * Classes for parsing and calculating tpcl * Threading support Keywords: thousand parsec space client support empire building strategy game tpcl scheme Platform: UNKNOWN libtpclient-py-0.3.2/libtpclient_py.egg-info/0000755000175000017500000000000011164431420017326 5ustar timtimlibtpclient-py-0.3.2/libtpclient_py.egg-info/PKG-INFO0000644000175000017500000000120511164431420020421 0ustar timtimMetadata-Version: 1.0 Name: libtpclient-py Version: 0.3.2 Summary: Client support library for Thousand Parsec Home-page: http://www.thousandparsec.net Author: Tim Ansell Author-email: tim@thousandparsec.net License: GPL Description: A library of code to support quick development of Clients for Thousand Parsec. Includes support for: * Classes of keeping a download cache of the universe (including automatic update) * Classes for parsing and calculating tpcl * Threading support Keywords: thousand parsec space client support empire building strategy game tpcl scheme Platform: UNKNOWN libtpclient-py-0.3.2/libtpclient_py.egg-info/not-zip-safe0000644000175000017500000000000111164431064021560 0ustar timtim libtpclient-py-0.3.2/libtpclient_py.egg-info/SOURCES.txt0000644000175000017500000000213511164431420021213 0ustar timtimREADME setup.py libtpclient_py.egg-info/PKG-INFO libtpclient_py.egg-info/SOURCES.txt libtpclient_py.egg-info/dependency_links.txt libtpclient_py.egg-info/namespace_packages.txt libtpclient_py.egg-info/not-zip-safe libtpclient_py.egg-info/top_level.txt tp/__init__.py tp/client/ChangeDict.py tp/client/ChangeList.py tp/client/Log.py tp/client/SinglePlayer.py tp/client/__init__.py tp/client/cache.py tp/client/config.py tp/client/decorator.py tp/client/media.py tp/client/parser.py tp/client/strptime.py tp/client/subprocess.py tp/client/threadcheck.py tp/client/threads.py tp/client/version.py tp/client/pyscheme/__init__.py tp/client/pyscheme/all_tests.py tp/client/pyscheme/analyzer.py tp/client/pyscheme/builtins.py tp/client/pyscheme/environment.py tp/client/pyscheme/error.py tp/client/pyscheme/evaluator.py tp/client/pyscheme/expander.py tp/client/pyscheme/expressions.py tp/client/pyscheme/pair.py tp/client/pyscheme/parser.py tp/client/pyscheme/pogo.py tp/client/pyscheme/prompt.py tp/client/pyscheme/scheme.py tp/client/pyscheme/symbol.py tp/client/pyscheme/test_analyzer.py tp/client/pyscheme/test_scheme.pylibtpclient-py-0.3.2/libtpclient_py.egg-info/namespace_packages.txt0000644000175000017500000000000311164431420023652 0ustar timtimtp libtpclient-py-0.3.2/libtpclient_py.egg-info/dependency_links.txt0000644000175000017500000000000111164431420023374 0ustar timtim libtpclient-py-0.3.2/libtpclient_py.egg-info/top_level.txt0000644000175000017500000000000311164431420022051 0ustar timtimtp libtpclient-py-0.3.2/README0000644000175000017500000000033111164415250013473 0ustar timtim This library depends on libtpproto-py. This library contains all the common stuff an advanced client would want to do such as: * Download the whole universe efficently * Store/Restore the universe from disk * libtpclient-py-0.3.2/setup.py0000755000175000017500000000222711164415250014336 0ustar timtim#!/usr/bin/env python import sys try: del sys.argv[sys.argv.index('--ignore-deps')] print "Ignoring dependencies..." except ValueError: import pkg_resources pkg_resources.require('libtpproto-py') from tp.client import __version__ version = "%s.%s.%s" % __version__[0:3] from setuptools import setup setup( name ="libtpclient-py", version =version, license ="GPL", description ="Client support library for Thousand Parsec", long_description="""\ A library of code to support quick development of Clients for Thousand Parsec. Includes support for: * Classes of keeping a download cache of the universe (including automatic update) * Classes for parsing and calculating tpcl * Threading support """, author ="Tim Ansell", author_email="tim@thousandparsec.net", url ="http://www.thousandparsec.net", keywords ="thousand parsec space client support empire building strategy game tpcl scheme", namespace_packages = ['tp'], packages=[ \ 'tp', 'tp.client', 'tp.client.pyscheme', ], zip_safe=False, include_patckage_data = True, package_data = { 'tp.client': ['singleplayer/*.dtd', 'singleplayer/aiclients/*', 'singleplayer/servers/*'] } ) libtpclient-py-0.3.2/tp/0000755000175000017500000000000011164431420013236 5ustar timtimlibtpclient-py-0.3.2/tp/__init__.py0000644000175000017500000000062311164415250015353 0ustar timtimtry: try: import pkg_resources pkg_resources.declare_namespace(__name__) except ImportError: import pkgutil __path__ = pkgutil.extend_path(__path__, __name__) except Exception, e: import warnings warnings.warn(e, RuntimeWarning) try: import modulefinder for p in __path__: modulefinder.AddPackagePath(__name__, p) except Exception, e: import warnings warnings.warn(e, RuntimeWarning) libtpclient-py-0.3.2/tp/client/0000755000175000017500000000000011164431420014514 5ustar timtimlibtpclient-py-0.3.2/tp/client/__init__.py0000644000175000017500000000033411164415250016630 0ustar timtim from version import version as vi from version import installpath as ip __version__ = vi __installpath__ = ip import __builtin__ try: __builtin__._ except AttributeError: def _(s): return s __builtin__._ = _ libtpclient-py-0.3.2/tp/client/media.py0000644000175000017500000002437311164415250016161 0ustar timtim import os, os.path import shutil import stat import string import socket import httplib import urllib, urlparse import time import gzip #from cache import Cache from strptime import strptime """ Format for media repositories are the following, media-new.gz directory1/mediafile.png directory1/mediafile.mesh All media which is the same but in a different format must have the same basename. See the above example where the file "mediafile" is avaliable in both png and mesh format. The MEDIA is a compressed text file which describes all the media avaliable on the server and some metadata about it. It has the following format - directory1/mediafile.png 20060614T0923 1789345 md5-318424ccbd97c644d6baa594284fefe3 directory1/mediafile.mesh 20060614T0923 7623748 md5-1505b14a78c9e5858edd0f044c4e0062 URLs of media locations are escaped locally using the following system re.sub('[^a-zA-Z0-9_]', '-', url) This means that the url "http://media.thousandparsec.net:80/client" would be escape as "http---media.thousandparsec.net-80-client" The URL should always be in full format including the port. When media is downloaded locally (or distributed via packages) a meta file is created for each file. This file allows the client to check if the graphic is up to date. The meta file contains, - 20060614T0923 1789345 md5-318424ccbd97c644d6baa594284fefe3 Media can also be distributed via packages. This should be installed in a shared location such as /usr/share/games/tp, the MEDIA should be distributed with the data where the data is split may have to be merged together (say if the 3d and 2d data from a URL is distributed seperately). /usr/share/games/tp/http---media.thousandparsec.net-80-client/MEDIA /usr/share/games/tp/http---media.thousandparsec.net-80-client/mediafile.png /usr/share/games/tp/http---media.thousandparsec.net-80-client/directory1/mediafile.png.meta /usr/share/games/tp/ftp---someotherplace.net-21-pub/media-new.gz ... When a new media is avaliable it will be downloaded to the local users home directory. ~/.tp/media/http---media.thousandparsec.net-80-client/media-new.gz ~/.tp/media/http---media.thousandparsec.net-80-client/directory1/mediafile.png ~/.tp/media/http---media.thousandparsec.net-80-client/directory1/mediafile.png.meta ~/.tp/media/ftp---someotherplace.net-21-pub/media-new.gz If a media file is found in the distribution search locations that is the same or newer then the users version. The users version should be removed. """ # FIXME: This should be different for different types of file systems. # FIXME: This should searchdirs = [ os.path.join("usr", "share", "tp"), os.path.join("usr", "share", "games", "tp"), os.path.join("usr", "local", "share", "tp"), os.path.join("usr", "local", "share", "games", "tp"), os.path.join("Program Files", "Thousand Parsec", "Media"), os.path.join("media"), # FIXME: This should only be searched in the development version os.path.join("..", "media"), "/home/tim/oss/tp/media", ] import re def filesafe(url): """ Make a URL safe for the filesystem """ return re.sub('[^a-zA-Z0-9_]', '-', url) def totime(s): return "%02.0f%02.0f%02.0fT%02.0f%02.0f" % strptime(s, "%a, %d %b %Y %H:%M:%S %Z")[0:5] MEDIA="media-new.gz" class URLOpener(urllib.FancyURLopener): def http_error_default(self, file, socket, code, reason, message): raise IOError(code, reason) class CallbackLimiter(object): def __init__(self, realcallback): self.realcallback = realcallback self.last = 0 def __call__(self, i, chunksize, maxsize): if self.realcallback is None: return downloaded = chunksize*i if not (i == 0 or downloaded == maxsize): # Skip if we have had a call back in the last 5 seconds if self.last + 5 > time.time(): return self.realcallback(i, chunksize, maxsize) self.last = time.time() from threadcheck import thread_checker, thread_safe class Media(object): __metaclass__ = thread_checker def configdir(): dirs = [("APPDATA", "Thousand Parsec"), ("HOME", ".tp"), (".", "var")] for base, extra in dirs: if base in os.environ: base = os.environ[base] break elif base != ".": continue return os.path.join(base, extra) configdir = staticmethod(configdir) def hash(self, s): """\ Get the function needed to do a hash """ alog, hash = split(s, '-') if alog == 'md5': import md5 checksum = (md5.new, hash) elif alog == 'sha1': import sha checksum = (sha.new, hash) else: raise IOError("Unknown hash algorithm.") return hash @thread_safe def metainfo(self, file): """\ Return the info in the metafile. """ modtime, size, checksum = open(file+'.meta', 'r').read().strip().split(' ') return modtime, long(size), checksum @thread_safe def locate(self, file): """\ Locates a file with a given filename on the filesystem. """ # Search through the local filesystem and see if we can find the file foundhere = [] for location in self.locations: possible = os.path.join(location, file) if os.path.exists(possible): foundhere.append(possible) return foundhere @thread_safe def newest(self, file): """\ Returns the newest version of a file. """ location = None curtime = 0 for possible in self.locate(file): try: modtime, size, checksum = self.metainfo(possible) if modtime > curtime: location = possible curtime = modtime except (IOError, OSError), e: print e return location def remotetime(self, file): """\ Gets the remote time of a file. Needed to find out if we need to update media-new.gz """ self.connection.request("HEAD", self.url + file) headers = {} headers['last-modified'] = self.connection.getresponse().getheader('last-modified') return totime(headers['last-modified']) def media(self): """\ """ global MEDIA # Use the cached version if it's avaliable if hasattr(self, '_media'): return self._media file = self.newest(MEDIA) if file is None or not self.connection is None: if not file is None: modtime, size, checksum = self.metainfo(file) # Check if there is a remote version which is new... remotetime = self.remotetime(MEDIA) if remotetime > modtime: # Need to get a new version file = self.get(MEDIA) else: # Need to get a version file = self.get(MEDIA) media = {} for line in gzip.open(file).readlines(): file, timestamp, size, checksum = line.strip().split() media[file] = (timestamp, int(size), checksum) # Cache the data so we don't need to reload all the time self._media = media return media def local(self, media, newtime): """\ Finds a file which is local which has a modtime greater then the give time. """ foundhere = self.locate(media) while len(foundhere) > 0: possible = foundhere.pop(0) try: modtime, size, checksum = self.metainfo(possible) if modtime < newtime: continue except IOError: pass return possible return False def give(self, media): """\ Gets a file to be used. Will use a local version or will download one. """ mediagz = self.media() if not mediagz.has_key(media): raise IOError("No such media file exists!") modtime = mediagz[media][0] # See if we have a local version we can use location = self.local(media, modtime) if not location is False: return location # Looks like we will have to download a peice of media return self.get(media) def get(self, file, callback=None): """\ Gets a file from the remote server. """ global MEDIA # Where the file will be downloaded too local_location = os.path.join(self.locations[-1], file) # Where the file is on the remote server remote_location = urlparse.urljoin(self.url, file) # If the file already exists we better remove it if os.path.exists(local_location): os.unlink(local_location) dir = os.path.dirname(local_location) if not os.path.exists(dir): os.makedirs(dir) if 1: # try: # Download the file (trash, message) = self.getter.retrieve(remote_location, local_location, CallbackLimiter(callback)) if file != MEDIA: mediagz = self.media() # Check the checksum # Check the filesize # Create the metafile from the mediagz info open(local_location+".meta", 'w').write("%s %s %s" % mediagz[file]) else: # Have to generate our own data open(local_location + ".meta", 'w').write("%s %s %s" % (totime(message.getheader('last-modified')), 0, 'None')) return local_location # except IOError, e: # print e # return False def __init__(self, url, configdir=None): """\ Everything must be there, even if the port is the default. serverurl is the URL of the server to download the media from configdir is where the cache will be stored. mediatypes is the type of media which the client needs. """ self.url = url # Make the URL safe for filesystems safeurl = filesafe(url) # FIXME: Check the serverurl is valid, IE It's in the full form. if configdir == None: configdir = self.configdir() userdir = os.path.join(configdir, "media", safeurl) # # Make sure the user dir exists # if os.path.exists(userdir) and new: # shutil.rmtree(userdir) if not os.path.exists(userdir): os.makedirs(userdir) # Find all the locations which have media relavent to this URL. self.locations = [] global searchdirs for location in searchdirs: full = os.path.join(location, safeurl) if os.path.exists(full): self.locations.append(full) # Append the users "localdir" self.locations.append(userdir) @property def connection(self): if not hasattr(self, '_connection'): type, host, self.basepath, t, t, t = urlparse.urlparse(self.url) self._connection = getattr(httplib, "%sConnection" % type.upper())(host) return self._connection @property def getter(self): if not hasattr(self, '_getter'): self._getter = URLOpener() return self._getter def getpossible(self, media_types): """ Gets the Media description file from the http server. """ for file in self.media(): for possible in media_types: if file.endswith(possible): yield file if __name__ == "__main__": import sys media_cache = Media(sys.argv[1]) files = {} for file in media_cache.getpossible(['png', 'gif', 'jpg']): print file media_cache.give(file) libtpclient-py-0.3.2/tp/client/Log.py0000644000175000017500000000017111164415250015611 0ustar timtim"""\ A Log is a version of the Cache which can have changes which happen undone and previous versions can be viewed. """ libtpclient-py-0.3.2/tp/client/parser.py0000644000175000017500000001317411164415250016373 0ustar timtim import pyscheme as scheme def get(list, id): for lid, value in list: if lid == id: return value return None class DesignCalculator: def __init__(self, cache, design): self.cache = cache self.design = design self.__dirty = True def rank(self): if self.__dirty: ranks = {} for component_id, number in self.design.components: component = self.cache.components[component_id] for property_id, value in component.properties: property = self.cache.properties[property_id] if not ranks.has_key(property.rank): ranks[property.rank] = [] if not property_id in ranks[property.rank]: ranks[property.rank].append(property_id) self.__ranks = ranks return self.__ranks def change(self, component, amount): """\ change(component, amount) -> None Changes the current design by adding/subtracting the certain amount of a component. """ self.__dirty = True i = 0 while True: # FIXME: There should be a better way to do this. if i >= len(self.design.components): self.design.components.append([component.id, amount]) break if self.design.components[i][0] == component.id: if isinstance(self.design.components[i], tuple): self.design.components[i] = list(self.design.components[i]) self.design.components[i][1] += amount if self.design.components[i][1] < 0: del self.design.components[i] break i += 1 def calculate(self): """\ calculate() -> Interpretor, Properties Calculates all the properties on a design. Returns the Interpretor and the object with the Properties. """ i = scheme.make_interpreter() # Step 1 ------------------------------------- ranks = self.rank() print "The order I need to calculate stuff in is,", ranks # Step 2 ------------------------------------- # The object which will store the properties calculated class Properties(dict): pass properties = Properties() scheme.environment.defineVariable(scheme.symbol.Symbol('design'), properties, i.get_environment()) # Step 3 ------------------------------------- for rank in ranks.keys(): for property_id in ranks[rank]: property = self.cache.properties[property_id] # Where we will store the values as calculated bits = [] # Get all the components we contain for component_id, amount in self.design.components: # Create the component object component = self.cache.components[component_id] # Calculate the actual value for this design value = get(component.properties, property_id) if value: print "Now evaluating", value value = i.eval(scheme.parse("""(%s design)""" % value)) print "The value calculated for component %i was %r" % (component_id, value) for x in range(0, amount): bits.append(value) print "All the values calculated where", bits bits_scheme = "(list" for bit in bits: bits_scheme += " " + str(bit).replace('L', '') bits_scheme += ")" print "In scheme that is", bits_scheme total = i.eval(scheme.parse("""(let ((bits %s)) (%s design bits))""" % (bits_scheme, property.calculate))) value, display = scheme.pair.car(total), scheme.pair.cdr(total) print "In total I got '%i' which will be displayed as '%s'" % (value, display) properties[property.name] = (property_id, value, display) def t(properties, name=property.name): return properties[name][1] i.install_function('designtype.'+property.name, t) print "The final properties we have are", properties.items() return i, properties def check(self, i, properties): """\ check(Interperator, Properties) -> Valid, Feedback Checks the requirements of a design. Returns if the properties are valid and a string which has human readable feedback. """ total_okay = True total_feedback = [] # Step 2, calculate the requirements for the properties ranks = self.rank() for rank in ranks.keys(): for property_id in ranks[rank]: property = self.cache.properties[property_id] if property.requirements == '': print "Property with id (%i) doesn't have any requirements" % property_id continue print "Now checking the following requirement" print property.requirements result = i.eval(scheme.parse("""(%s design)""" % property.requirements)) print "Result was:", result okay, feedback = scheme.pair.car(result), scheme.pair.cdr(result) if okay != scheme.symbol.Symbol('#t'): total_okay = False if feedback != "": total_feedback.append(feedback) # Step 3, calculate the requirements for the components for component_id, amount in self.design.components: component = self.cache.components[component_id] if component.requirements == '': print "Component with id (%i) doesn't have any requirements" % property_id continue print "Now checking the following requirement" print component.requirements result = i.eval(scheme.parse("""(%s design)""" % component.requirements)) print "Result was:", result okay, feedback = scheme.pair.car(result), scheme.pair.cdr(result) if okay != scheme.symbol.Symbol('#t'): total_okay = False if feedback != "": total_feedback.append(feedback) return total_okay, "\n".join(total_feedback) def apply(self, properties, okay, feedback): """\ apply(Properties, Apply the results returned from calculate/check to the design object. """ self.design.properties = [(x[0], x[2]) for x in properties.values()] self.design.feedback = feedback self.design.used = (-1, 0)[okay] def update(self): if self.__dirty: i, p = self.calculate() okay, reason = self.check(i, p) self.apply(p, okay, reason) libtpclient-py-0.3.2/tp/client/subprocess.py0000644000175000017500000013063711164415250017273 0ustar timtim# subprocess - Subprocesses with accessible I/O streams # # For more information about this module, see PEP 324. # # This module should remain compatible with Python 2.2, see PEP 291. # # Copyright (c) 2003-2005 by Peter Astrand # # Licensed to PSF under a Contributor Agreement. # See http://www.python.org/2.4/license for licensing details. r"""subprocess - Subprocesses with accessible I/O streams This module allows you to spawn processes, connect to their input/output/error pipes, and obtain their return codes. This module intends to replace several other, older modules and functions, like: os.system os.spawn* os.popen* popen2.* commands.* Information about how the subprocess module can be used to replace these modules and functions can be found below. Using the subprocess module =========================== This module defines one class called Popen: class Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0): Arguments are: args should be a string, or a sequence of program arguments. The program to execute is normally the first item in the args sequence or string, but can be explicitly set by using the executable argument. On UNIX, with shell=False (default): In this case, the Popen class uses os.execvp() to execute the child program. args should normally be a sequence. A string will be treated as a sequence with the string as the only item (the program to execute). On UNIX, with shell=True: If args is a string, it specifies the command string to execute through the shell. If args is a sequence, the first item specifies the command string, and any additional items will be treated as additional shell arguments. On Windows: the Popen class uses CreateProcess() to execute the child program, which operates on strings. If args is a sequence, it will be converted to a string using the list2cmdline method. Please note that not all MS Windows applications interpret the command line the same way: The list2cmdline is designed for applications using the same rules as the MS C runtime. bufsize, if given, has the same meaning as the corresponding argument to the built-in open() function: 0 means unbuffered, 1 means line buffered, any other positive value means use a buffer of (approximately) that size. A negative bufsize means to use the system default, which usually means fully buffered. The default value for bufsize is 0 (unbuffered). stdin, stdout and stderr specify the executed programs' standard input, standard output and standard error file handles, respectively. Valid values are PIPE, an existing file descriptor (a positive integer), an existing file object, and None. PIPE indicates that a new pipe to the child should be created. With None, no redirection will occur; the child's file handles will be inherited from the parent. Additionally, stderr can be STDOUT, which indicates that the stderr data from the applications should be captured into the same file handle as for stdout. If preexec_fn is set to a callable object, this object will be called in the child process just before the child is executed. If close_fds is true, all file descriptors except 0, 1 and 2 will be closed before the child process is executed. if shell is true, the specified command will be executed through the shell. If cwd is not None, the current directory will be changed to cwd before the child is executed. If env is not None, it defines the environment variables for the new process. If universal_newlines is true, the file objects stdout and stderr are opened as a text files, but lines may be terminated by any of '\n', the Unix end-of-line convention, '\r', the Macintosh convention or '\r\n', the Windows convention. All of these external representations are seen as '\n' by the Python program. Note: This feature is only available if Python is built with universal newline support (the default). Also, the newlines attribute of the file objects stdout, stdin and stderr are not updated by the communicate() method. The startupinfo and creationflags, if given, will be passed to the underlying CreateProcess() function. They can specify things such as appearance of the main window and priority for the new process. (Windows only) This module also defines two shortcut functions: call(*popenargs, **kwargs): Run command with arguments. Wait for command to complete, then return the returncode attribute. The arguments are the same as for the Popen constructor. Example: retcode = call(["ls", "-l"]) check_call(*popenargs, **kwargs): Run command with arguments. Wait for command to complete. If the exit code was zero then return, otherwise raise CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute. The arguments are the same as for the Popen constructor. Example: check_call(["ls", "-l"]) Exceptions ---------- Exceptions raised in the child process, before the new program has started to execute, will be re-raised in the parent. Additionally, the exception object will have one extra attribute called 'child_traceback', which is a string containing traceback information from the childs point of view. The most common exception raised is OSError. This occurs, for example, when trying to execute a non-existent file. Applications should prepare for OSErrors. A ValueError will be raised if Popen is called with invalid arguments. check_call() will raise CalledProcessError, if the called process returns a non-zero return code. Security -------- Unlike some other popen functions, this implementation will never call /bin/sh implicitly. This means that all characters, including shell metacharacters, can safely be passed to child processes. Popen objects ============= Instances of the Popen class have the following methods: poll() Check if child process has terminated. Returns returncode attribute. wait() Wait for child process to terminate. Returns returncode attribute. communicate(input=None) Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child. communicate() returns a tuple (stdout, stderr). Note: The data read is buffered in memory, so do not use this method if the data size is large or unlimited. The following attributes are also available: stdin If the stdin argument is PIPE, this attribute is a file object that provides input to the child process. Otherwise, it is None. stdout If the stdout argument is PIPE, this attribute is a file object that provides output from the child process. Otherwise, it is None. stderr If the stderr argument is PIPE, this attribute is file object that provides error output from the child process. Otherwise, it is None. pid The process ID of the child process. returncode The child return code. A None value indicates that the process hasn't terminated yet. A negative value -N indicates that the child was terminated by signal N (UNIX only). Replacing older functions with the subprocess module ==================================================== In this section, "a ==> b" means that b can be used as a replacement for a. Note: All functions in this section fail (more or less) silently if the executed program cannot be found; this module raises an OSError exception. In the following examples, we assume that the subprocess module is imported with "from subprocess import *". Replacing /bin/sh shell backquote --------------------------------- output=`mycmd myarg` ==> output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0] Replacing shell pipe line ------------------------- output=`dmesg | grep hda` ==> p1 = Popen(["dmesg"], stdout=PIPE) p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) output = p2.communicate()[0] Replacing os.system() --------------------- sts = os.system("mycmd" + " myarg") ==> p = Popen("mycmd" + " myarg", shell=True) pid, sts = os.waitpid(p.pid, 0) Note: * Calling the program through the shell is usually not required. * It's easier to look at the returncode attribute than the exitstatus. A more real-world example would look like this: try: retcode = call("mycmd" + " myarg", shell=True) if retcode < 0: print >>sys.stderr, "Child was terminated by signal", -retcode else: print >>sys.stderr, "Child returned", retcode except OSError, e: print >>sys.stderr, "Execution failed:", e Replacing os.spawn* ------------------- P_NOWAIT example: pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg") ==> pid = Popen(["/bin/mycmd", "myarg"]).pid P_WAIT example: retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg") ==> retcode = call(["/bin/mycmd", "myarg"]) Vector example: os.spawnvp(os.P_NOWAIT, path, args) ==> Popen([path] + args[1:]) Environment example: os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env) ==> Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"}) Replacing os.popen* ------------------- pipe = os.popen(cmd, mode='r', bufsize) ==> pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout pipe = os.popen(cmd, mode='w', bufsize) ==> pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin (child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize) ==> p = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdin, child_stdout) = (p.stdin, p.stdout) (child_stdin, child_stdout, child_stderr) = os.popen3(cmd, mode, bufsize) ==> p = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) (child_stdin, child_stdout, child_stderr) = (p.stdin, p.stdout, p.stderr) (child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize) ==> p = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) (child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout) Replacing popen2.* ------------------ Note: If the cmd argument to popen2 functions is a string, the command is executed through /bin/sh. If it is a list, the command is directly executed. (child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode) ==> p = Popen(["somestring"], shell=True, bufsize=bufsize stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdout, child_stdin) = (p.stdout, p.stdin) (child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode) ==> p = Popen(["mycmd", "myarg"], bufsize=bufsize, stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdout, child_stdin) = (p.stdout, p.stdin) The popen2.Popen3 and popen2.Popen4 basically works as subprocess.Popen, except that: * subprocess.Popen raises an exception if the execution fails * the capturestderr argument is replaced with the stderr argument. * stdin=PIPE and stdout=PIPE must be specified. * popen2 closes all filedescriptors by default, but you have to specify close_fds=True with subprocess.Popen. """ import sys mswindows = (sys.platform == "win32") import os import types import traceback import gc import signal # Exception classes used by this module. class CalledProcessError(Exception): """This exception is raised when a process run by check_call() returns a non-zero exit status. The exit status will be stored in the returncode attribute.""" def __init__(self, returncode, cmd): self.returncode = returncode self.cmd = cmd def __str__(self): return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) if mswindows: import threading import msvcrt if 0: # <-- change this to use pywin32 instead of the _subprocess driver import pywintypes from win32api import GetStdHandle, STD_INPUT_HANDLE, \ STD_OUTPUT_HANDLE, STD_ERROR_HANDLE from win32api import GetCurrentProcess, DuplicateHandle, \ GetModuleFileName, GetVersion from win32con import DUPLICATE_SAME_ACCESS, SW_HIDE from win32pipe import CreatePipe from win32process import CreateProcess, STARTUPINFO, \ GetExitCodeProcess, STARTF_USESTDHANDLES, \ STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE from win32process import TerminateProcess from win32event import WaitForSingleObject, INFINITE, WAIT_OBJECT_0 else: from _subprocess import * class STARTUPINFO: dwFlags = 0 hStdInput = None hStdOutput = None hStdError = None wShowWindow = 0 class pywintypes: error = IOError else: import select import errno import fcntl import pickle __all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "CalledProcessError"] try: MAXFD = os.sysconf("SC_OPEN_MAX") except: MAXFD = 256 # True/False does not exist on 2.2.0 #try: # False #except NameError: # False = 0 # True = 1 _active = [] def _cleanup(): for inst in _active[:]: if inst._internal_poll(_deadstate=sys.maxint) >= 0: try: _active.remove(inst) except ValueError: # This can happen if two threads create a new Popen instance. # It's harmless that it was already removed, so ignore. pass PIPE = -1 STDOUT = -2 def call(*popenargs, **kwargs): """Run command with arguments. Wait for command to complete, then return the returncode attribute. The arguments are the same as for the Popen constructor. Example: retcode = call(["ls", "-l"]) """ return Popen(*popenargs, **kwargs).wait() def check_call(*popenargs, **kwargs): """Run command with arguments. Wait for command to complete. If the exit code was zero then return, otherwise raise CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute. The arguments are the same as for the Popen constructor. Example: check_call(["ls", "-l"]) """ retcode = call(*popenargs, **kwargs) cmd = kwargs.get("args") if cmd is None: cmd = popenargs[0] if retcode: raise CalledProcessError(retcode, cmd) return retcode def list2cmdline(seq): """ Translate a sequence of arguments into a command line string, using the same rules as the MS C runtime: 1) Arguments are delimited by white space, which is either a space or a tab. 2) A string surrounded by double quotation marks is interpreted as a single argument, regardless of white space or pipe characters contained within. A quoted string can be embedded in an argument. 3) A double quotation mark preceded by a backslash is interpreted as a literal double quotation mark. 4) Backslashes are interpreted literally, unless they immediately precede a double quotation mark. 5) If backslashes immediately precede a double quotation mark, every pair of backslashes is interpreted as a literal backslash. If the number of backslashes is odd, the last backslash escapes the next double quotation mark as described in rule 3. """ # See # http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp result = [] needquote = False for arg in seq: bs_buf = [] # Add a space to separate this argument from the others if result: result.append(' ') needquote = (" " in arg) or ("\t" in arg) or ("|" in arg) or not arg if needquote: result.append('"') for c in arg: if c == '\\': # Don't know if we need to double yet. bs_buf.append(c) elif c == '"': # Double backslashes. result.append('\\' * len(bs_buf)*2) bs_buf = [] result.append('\\"') else: # Normal char if bs_buf: result.extend(bs_buf) bs_buf = [] result.append(c) # Add remaining backslashes, if any. if bs_buf: result.extend(bs_buf) if needquote: result.extend(bs_buf) result.append('"') return ''.join(result) class Popen(object): def __init__(self, args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0): """Create new Popen instance.""" _cleanup() self._child_created = False if not isinstance(bufsize, (int, long)): raise TypeError("bufsize must be an integer") if mswindows: if preexec_fn is not None: raise ValueError("preexec_fn is not supported on Windows " "platforms") if close_fds and (stdin is not None or stdout is not None or stderr is not None): raise ValueError("close_fds is not supported on Windows " "platforms if you redirect stdin/stdout/stderr") else: # POSIX if startupinfo is not None: raise ValueError("startupinfo is only supported on Windows " "platforms") if creationflags != 0: raise ValueError("creationflags is only supported on Windows " "platforms") self.stdin = None self.stdout = None self.stderr = None self.pid = None self.returncode = None self.universal_newlines = universal_newlines # Input and output objects. The general principle is like # this: # # Parent Child # ------ ----- # p2cwrite ---stdin---> p2cread # c2pread <--stdout--- c2pwrite # errread <--stderr--- errwrite # # On POSIX, the child objects are file descriptors. On # Windows, these are Windows file handles. The parent objects # are file descriptors on both platforms. The parent objects # are None when not using PIPEs. The child objects are None # when not redirecting. (p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) = self._get_handles(stdin, stdout, stderr) self._execute_child(args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) # On Windows, you cannot just redirect one or two handles: You # either have to redirect all three or none. If the subprocess # user has only redirected one or two handles, we are # automatically creating PIPEs for the rest. We should close # these after the process is started. See bug #1124861. if mswindows: if stdin is None and p2cwrite is not None: os.close(p2cwrite) p2cwrite = None if stdout is None and c2pread is not None: os.close(c2pread) c2pread = None if stderr is None and errread is not None: os.close(errread) errread = None if p2cwrite is not None: self.stdin = os.fdopen(p2cwrite, 'wb', bufsize) if c2pread is not None: if universal_newlines: self.stdout = os.fdopen(c2pread, 'rU', bufsize) else: self.stdout = os.fdopen(c2pread, 'rb', bufsize) if errread is not None: if universal_newlines: self.stderr = os.fdopen(errread, 'rU', bufsize) else: self.stderr = os.fdopen(errread, 'rb', bufsize) def _translate_newlines(self, data): data = data.replace("\r\n", "\n") data = data.replace("\r", "\n") return data def __del__(self, sys=sys): if not self._child_created: # We didn't get to successfully create a child process. return # In case the child hasn't been waited on, check if it's done. self._internal_poll(_deadstate=sys.maxint) if self.returncode is None and _active is not None: # Child is still running, keep us alive until we can wait on it. _active.append(self) def communicate(self, input=None): """Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child. communicate() returns a tuple (stdout, stderr).""" # Optimization: If we are only using one pipe, or no pipe at # all, using select() or threads is unnecessary. if [self.stdin, self.stdout, self.stderr].count(None) >= 2: stdout = None stderr = None if self.stdin: if input: self.stdin.write(input) self.stdin.close() elif self.stdout: stdout = self.stdout.read() self.stdout.close() elif self.stderr: stderr = self.stderr.read() self.stderr.close() self.wait() return (stdout, stderr) return self._communicate(input) def poll(self): return self._internal_poll() if mswindows: # # Windows methods # def _get_handles(self, stdin, stdout, stderr): """Construct and return tupel with IO objects: p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite """ if stdin is None and stdout is None and stderr is None: return (None, None, None, None, None, None) p2cread, p2cwrite = None, None c2pread, c2pwrite = None, None errread, errwrite = None, None if stdin is None: p2cread = GetStdHandle(STD_INPUT_HANDLE) if p2cread is not None: pass elif stdin is None or stdin == PIPE: p2cread, p2cwrite = CreatePipe(None, 0) # Detach and turn into fd p2cwrite = p2cwrite.Detach() p2cwrite = msvcrt.open_osfhandle(p2cwrite, 0) elif isinstance(stdin, int): p2cread = msvcrt.get_osfhandle(stdin) else: # Assuming file-like object p2cread = msvcrt.get_osfhandle(stdin.fileno()) p2cread = self._make_inheritable(p2cread) if stdout is None: c2pwrite = GetStdHandle(STD_OUTPUT_HANDLE) if c2pwrite is not None: pass elif stdout is None or stdout == PIPE: c2pread, c2pwrite = CreatePipe(None, 0) # Detach and turn into fd c2pread = c2pread.Detach() c2pread = msvcrt.open_osfhandle(c2pread, 0) elif isinstance(stdout, int): c2pwrite = msvcrt.get_osfhandle(stdout) else: # Assuming file-like object c2pwrite = msvcrt.get_osfhandle(stdout.fileno()) c2pwrite = self._make_inheritable(c2pwrite) if stderr is None: errwrite = GetStdHandle(STD_ERROR_HANDLE) if errwrite is not None: pass elif stderr is None or stderr == PIPE: errread, errwrite = CreatePipe(None, 0) # Detach and turn into fd errread = errread.Detach() errread = msvcrt.open_osfhandle(errread, 0) elif stderr == STDOUT: errwrite = c2pwrite elif isinstance(stderr, int): errwrite = msvcrt.get_osfhandle(stderr) else: # Assuming file-like object errwrite = msvcrt.get_osfhandle(stderr.fileno()) errwrite = self._make_inheritable(errwrite) return (p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) def _make_inheritable(self, handle): """Return a duplicate of handle, which is inheritable""" return DuplicateHandle(GetCurrentProcess(), handle, GetCurrentProcess(), 0, 1, DUPLICATE_SAME_ACCESS) def _find_w9xpopen(self): """Find and return absolut path to w9xpopen.exe""" w9xpopen = os.path.join(os.path.dirname(GetModuleFileName(0)), "w9xpopen.exe") if not os.path.exists(w9xpopen): # Eeek - file-not-found - possibly an embedding # situation - see if we can locate it in sys.exec_prefix w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix), "w9xpopen.exe") if not os.path.exists(w9xpopen): raise RuntimeError("Cannot locate w9xpopen.exe, which is " "needed for Popen to work with your " "shell or platform.") return w9xpopen def _execute_child(self, args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite): """Execute program (MS Windows version)""" if not isinstance(args, types.StringTypes): args = list2cmdline(args) # Process startup details if startupinfo is None: startupinfo = STARTUPINFO() if None not in (p2cread, c2pwrite, errwrite): startupinfo.dwFlags |= STARTF_USESTDHANDLES startupinfo.hStdInput = p2cread startupinfo.hStdOutput = c2pwrite startupinfo.hStdError = errwrite if shell: startupinfo.dwFlags |= STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE comspec = os.environ.get("COMSPEC", "cmd.exe") args = comspec + " /c " + args if (GetVersion() >= 0x80000000L or os.path.basename(comspec).lower() == "command.com"): # Win9x, or using command.com on NT. We need to # use the w9xpopen intermediate program. For more # information, see KB Q150956 # (http://web.archive.org/web/20011105084002/http://support.microsoft.com/support/kb/articles/Q150/9/56.asp) w9xpopen = self._find_w9xpopen() args = '"%s" %s' % (w9xpopen, args) # Not passing CREATE_NEW_CONSOLE has been known to # cause random failures on win9x. Specifically a # dialog: "Your program accessed mem currently in # use at xxx" and a hopeful warning about the # stability of your system. Cost is Ctrl+C wont # kill children. creationflags |= CREATE_NEW_CONSOLE # Start the process try: hp, ht, pid, tid = CreateProcess(executable, args, # no special security None, None, int(not close_fds), creationflags, env, cwd, startupinfo) except pywintypes.error, e: # Translate pywintypes.error to WindowsError, which is # a subclass of OSError. FIXME: We should really # translate errno using _sys_errlist (or simliar), but # how can this be done from Python? raise WindowsError(*e.args) # Retain the process handle, but close the thread handle self._child_created = True self._handle = hp self.pid = pid ht.Close() # Child is launched. Close the parent's copy of those pipe # handles that only the child should have open. You need # to make sure that no handles to the write end of the # output pipe are maintained in this process or else the # pipe will not close when the child process exits and the # ReadFile will hang. if p2cread is not None: p2cread.Close() if c2pwrite is not None: c2pwrite.Close() if errwrite is not None: errwrite.Close() def _internal_poll(self, _deadstate=None): """Check if child process has terminated. Returns returncode attribute.""" if self.returncode is None: if WaitForSingleObject(self._handle, 0) == WAIT_OBJECT_0: self.returncode = GetExitCodeProcess(self._handle) return self.returncode def wait(self): """Wait for child process to terminate. Returns returncode attribute.""" if self.returncode is None: obj = WaitForSingleObject(self._handle, INFINITE) self.returncode = GetExitCodeProcess(self._handle) return self.returncode def _readerthread(self, fh, buffer): buffer.append(fh.read()) def _communicate(self, input): stdout = None # Return stderr = None # Return if self.stdout: stdout = [] stdout_thread = threading.Thread(target=self._readerthread, args=(self.stdout, stdout)) stdout_thread.setDaemon(True) stdout_thread.start() if self.stderr: stderr = [] stderr_thread = threading.Thread(target=self._readerthread, args=(self.stderr, stderr)) stderr_thread.setDaemon(True) stderr_thread.start() if self.stdin: if input is not None: self.stdin.write(input) self.stdin.close() if self.stdout: stdout_thread.join() if self.stderr: stderr_thread.join() # All data exchanged. Translate lists into strings. if stdout is not None: stdout = stdout[0] if stderr is not None: stderr = stderr[0] # Translate newlines, if requested. We cannot let the file # object do the translation: It is based on stdio, which is # impossible to combine with select (unless forcing no # buffering). if self.universal_newlines and hasattr(file, 'newlines'): if stdout: stdout = self._translate_newlines(stdout) if stderr: stderr = self._translate_newlines(stderr) self.wait() return (stdout, stderr) def send_signal(self, sig): """Send a signal to the process """ if sig == signal.SIGTERM: self.terminate() else: raise ValueError("Only SIGTERM is supported on Windows") def terminate(self): """Terminates the process """ TerminateProcess(self._handle, 1) kill = terminate else: # # POSIX methods # def _get_handles(self, stdin, stdout, stderr): """Construct and return tupel with IO objects: p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite """ p2cread, p2cwrite = None, None c2pread, c2pwrite = None, None errread, errwrite = None, None if stdin is None: pass elif stdin == PIPE: p2cread, p2cwrite = os.pipe() elif isinstance(stdin, int): p2cread = stdin else: # Assuming file-like object p2cread = stdin.fileno() if stdout is None: pass elif stdout == PIPE: c2pread, c2pwrite = os.pipe() elif isinstance(stdout, int): c2pwrite = stdout else: # Assuming file-like object c2pwrite = stdout.fileno() if stderr is None: pass elif stderr == PIPE: errread, errwrite = os.pipe() elif stderr == STDOUT: errwrite = c2pwrite elif isinstance(stderr, int): errwrite = stderr else: # Assuming file-like object errwrite = stderr.fileno() return (p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) def _set_cloexec_flag(self, fd): try: cloexec_flag = fcntl.FD_CLOEXEC except AttributeError: cloexec_flag = 1 old = fcntl.fcntl(fd, fcntl.F_GETFD) fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag) def _close_fds(self, but): os.closerange(3, but) os.closerange(but + 1, MAXFD) def _execute_child(self, args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite): """Execute program (POSIX version)""" if isinstance(args, types.StringTypes): args = [args] else: args = list(args) if shell: args = ["/bin/sh", "-c"] + args if executable is None: executable = args[0] # For transferring possible exec failure from child to parent # The first char specifies the exception type: 0 means # OSError, 1 means some other error. errpipe_read, errpipe_write = os.pipe() self._set_cloexec_flag(errpipe_write) gc_was_enabled = gc.isenabled() # Disable gc to avoid bug where gc -> file_dealloc -> # write to stderr -> hang. http://bugs.python.org/issue1336 gc.disable() try: self.pid = os.fork() except: if gc_was_enabled: gc.enable() raise self._child_created = True if self.pid == 0: # Child try: # Close parent's pipe ends if p2cwrite is not None: os.close(p2cwrite) if c2pread is not None: os.close(c2pread) if errread is not None: os.close(errread) os.close(errpipe_read) # Dup fds for child if p2cread is not None: os.dup2(p2cread, 0) if c2pwrite is not None: os.dup2(c2pwrite, 1) if errwrite is not None: os.dup2(errwrite, 2) # Close pipe fds. Make sure we don't close the same # fd more than once, or standard fds. if p2cread is not None and p2cread not in (0,): os.close(p2cread) if c2pwrite is not None and c2pwrite not in (p2cread, 1): os.close(c2pwrite) if errwrite is not None and errwrite not in (p2cread, c2pwrite, 2): os.close(errwrite) # Close all other fds, if asked for if close_fds: self._close_fds(but=errpipe_write) if cwd is not None: os.chdir(cwd) if preexec_fn: preexec_fn() if env is None: os.execvp(executable, args) else: os.execvpe(executable, args, env) except: exc_type, exc_value, tb = sys.exc_info() # Save the traceback and attach it to the exception object exc_lines = traceback.format_exception(exc_type, exc_value, tb) exc_value.child_traceback = ''.join(exc_lines) os.write(errpipe_write, pickle.dumps(exc_value)) # This exitcode won't be reported to applications, so it # really doesn't matter what we return. os._exit(255) # Parent if gc_was_enabled: gc.enable() os.close(errpipe_write) if p2cread is not None and p2cwrite is not None: os.close(p2cread) if c2pwrite is not None and c2pread is not None: os.close(c2pwrite) if errwrite is not None and errread is not None: os.close(errwrite) # Wait for exec to fail or succeed; possibly raising exception data = os.read(errpipe_read, 1048576) # Exceptions limited to 1 MB os.close(errpipe_read) if data != "": os.waitpid(self.pid, 0) child_exception = pickle.loads(data) raise child_exception def _handle_exitstatus(self, sts): if os.WIFSIGNALED(sts): self.returncode = -os.WTERMSIG(sts) elif os.WIFEXITED(sts): self.returncode = os.WEXITSTATUS(sts) else: # Should never happen raise RuntimeError("Unknown child exit status!") def _internal_poll(self, _deadstate=None): """Check if child process has terminated. Returns returncode attribute.""" if self.returncode is None: try: pid, sts = os.waitpid(self.pid, os.WNOHANG) if pid == self.pid: self._handle_exitstatus(sts) except os.error: if _deadstate is not None: self.returncode = _deadstate return self.returncode def wait(self): """Wait for child process to terminate. Returns returncode attribute.""" if self.returncode is None: pid, sts = os.waitpid(self.pid, 0) self._handle_exitstatus(sts) return self.returncode def _communicate(self, input): read_set = [] write_set = [] stdout = None # Return stderr = None # Return if self.stdin: # Flush stdio buffer. This might block, if the user has # been writing to .stdin in an uncontrolled fashion. self.stdin.flush() if input: write_set.append(self.stdin) else: self.stdin.close() if self.stdout: read_set.append(self.stdout) stdout = [] if self.stderr: read_set.append(self.stderr) stderr = [] input_offset = 0 while read_set or write_set: try: rlist, wlist, xlist = select.select(read_set, write_set, []) except select.error, e: if e.args[0] == errno.EINTR: continue raise if self.stdin in wlist: # When select has indicated that the file is writable, # we can write up to PIPE_BUF bytes without risk # blocking. POSIX defines PIPE_BUF >= 512 chunk = input[input_offset : input_offset + 512] bytes_written = os.write(self.stdin.fileno(), chunk) input_offset += bytes_written if input_offset >= len(input): self.stdin.close() write_set.remove(self.stdin) if self.stdout in rlist: data = os.read(self.stdout.fileno(), 1024) if data == "": self.stdout.close() read_set.remove(self.stdout) stdout.append(data) if self.stderr in rlist: data = os.read(self.stderr.fileno(), 1024) if data == "": self.stderr.close() read_set.remove(self.stderr) stderr.append(data) # All data exchanged. Translate lists into strings. if stdout is not None: stdout = ''.join(stdout) if stderr is not None: stderr = ''.join(stderr) # Translate newlines, if requested. We cannot let the file # object do the translation: It is based on stdio, which is # impossible to combine with select (unless forcing no # buffering). if self.universal_newlines and hasattr(file, 'newlines'): if stdout: stdout = self._translate_newlines(stdout) if stderr: stderr = self._translate_newlines(stderr) self.wait() return (stdout, stderr) def send_signal(self, sig): """Send a signal to the process """ os.kill(self.pid, sig) def terminate(self): """Terminate the process with SIGTERM """ self.send_signal(signal.SIGTERM) def kill(self): """Kill the process with SIGKILL """ self.send_signal(signal.SIGKILL) def _demo_posix(): # # Example 1: Simple redirection: Get process list # plist = Popen(["ps"], stdout=PIPE).communicate()[0] print "Process list:" print plist # # Example 2: Change uid before executing child # if os.getuid() == 0: p = Popen(["id"], preexec_fn=lambda: os.setuid(100)) p.wait() # # Example 3: Connecting several subprocesses # print "Looking for 'hda'..." p1 = Popen(["dmesg"], stdout=PIPE) p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) print repr(p2.communicate()[0]) # # Example 4: Catch execution error # print print "Trying a weird file..." try: print Popen(["/this/path/does/not/exist"]).communicate() except OSError, e: if e.errno == errno.ENOENT: print "The file didn't exist. I thought so..." print "Child traceback:" print e.child_traceback else: print "Error", e.errno else: print >>sys.stderr, "Gosh. No error." def _demo_windows(): # # Example 1: Connecting several subprocesses # print "Looking for 'PROMPT' in set output..." p1 = Popen("set", stdout=PIPE, shell=True) p2 = Popen('find "PROMPT"', stdin=p1.stdout, stdout=PIPE) print repr(p2.communicate()[0]) # # Example 2: Simple execution of program # print "Executing calc..." p = Popen("calc") p.wait() if __name__ == "__main__": if mswindows: _demo_windows() else: _demo_posix() libtpclient-py-0.3.2/tp/client/strptime.py0000644000175000017500000002757711164415250016762 0ustar timtim"""Strptime-related classes and functions. CLASSES: TimeRE -- Creates regexes for pattern matching a string of text containing time information FUNCTIONS: strptime -- Calculates the time struct represented by the passed-in string """ import time import calendar from re import compile as re_compile from re import IGNORECASE from re import escape as re_escape from datetime import date as datetime_date try: from thread import allocate_lock as _thread_allocate_lock except: from dummy_thread import allocate_lock as _thread_allocate_lock __author__ = "Brett Cannon" __email__ = "brett@python.org" __all__ = ['strptime'] class TimeRE(dict): """Handle conversion from format directives to regexes.""" def __init__(self): """Create keys/values. Order of execution is important for dependency reasons. """ base = super(TimeRE, self) base.__init__({ # The " \d" part of the regex is to make %c from ANSI C work 'd': r"(?P3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])", 'H': r"(?P2[0-3]|[0-1]\d|\d)", 'I': r"(?P1[0-2]|0[1-9]|[1-9])", 'j': r"(?P36[0-6]|3[0-5]\d|[1-2]\d\d|0[1-9]\d|00[1-9]|[1-9]\d|0[1-9]|[1-9])", 'm': r"(?P1[0-2]|0[1-9]|[1-9])", 'M': r"(?P[0-5]\d|\d)", 'S': r"(?P6[0-1]|[0-5]\d|\d)", 'U': r"(?P5[0-3]|[0-4]\d|\d)", 'w': r"(?P[0-6])", # W is set below by using 'U' 'y': r"(?P\d\d)", #XXX: Does 'Y' need to worry about having less or more than # 4 digits? 'Y': r"(?P\d\d\d\d)", 'A': self.__seqToRE(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], 'A'), 'a': self.__seqToRE(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], 'a'), 'B': self.__seqToRE(['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'], 'B'), 'b': self.__seqToRE(['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'], 'b'), 'p': self.__seqToRE(['am', 'pm'], 'p'), 'Z': self.__seqToRE(['utc', 'cst', 'gmt', 'cst'], 'Z'), '%': '%'}) base.__setitem__('W', base.__getitem__('U').replace('U', 'W')) base.__setitem__('c', self.pattern('%a %b %d %H:%M:%S %Y')) base.__setitem__('x', self.pattern('%m/%d/%y')) base.__setitem__('X', self.pattern('%H:%M:%S')) def __seqToRE(self, to_convert, directive): """Convert a list to a regex string for matching a directive. Want possible matching values to be from longest to shortest. This prevents the possibility of a match occuring for a value that also a substring of a larger value that should have matched (e.g., 'abc' matching when 'abcdef' should have been the match). """ to_convert = sorted(to_convert, key=len, reverse=True) for value in to_convert: if value != '': break else: return '' regex = '|'.join(re_escape(stuff) for stuff in to_convert) regex = '(?P<%s>%s' % (directive, regex) return '%s)' % regex def pattern(self, format): """Return regex pattern for the format string. Need to make sure that any characters that might be interpreted as regex syntax are escaped. """ processed_format = '' # The sub() call escapes all characters that might be misconstrued # as regex syntax. Cannot use re.escape since we have to deal with # format directives (%m, etc.). regex_chars = re_compile(r"([\\.^$*+?\(\){}\[\]|])") format = regex_chars.sub(r"\\\1", format) whitespace_replacement = re_compile('\s+') format = whitespace_replacement.sub('\s*', format) while '%' in format: directive_index = format.index('%')+1 processed_format = "%s%s%s" % (processed_format, format[:directive_index-1], self[format[directive_index]]) format = format[directive_index+1:] return "%s%s" % (processed_format, format) def compile(self, format): """Return a compiled re object for the format string.""" return re_compile(self.pattern(format), IGNORECASE) _cache_lock = _thread_allocate_lock() # DO NOT modify _TimeRE_cache or _regex_cache without acquiring the cache lock # first! _TimeRE_cache = TimeRE() _CACHE_MAX_SIZE = 5 # Max number of regexes stored in _regex_cache _regex_cache = {} def strptime(data_string, format="%a %b %d %H:%M:%S %Y"): """Return a time struct based on the input string and the format string.""" global _TimeRE_cache _cache_lock.acquire() try: time_re = _TimeRE_cache if len(_regex_cache) > _CACHE_MAX_SIZE: _regex_cache.clear() format_regex = _regex_cache.get(format) if not format_regex: format_regex = time_re.compile(format) _regex_cache[format] = format_regex finally: _cache_lock.release() found = format_regex.match(data_string) if not found: raise ValueError("time data did not match format: data=%s fmt=%s" % (data_string, format)) if len(data_string) != found.end(): raise ValueError("unconverted data remains: %s" % data_string[found.end():]) year = 1900 month = day = 1 hour = minute = second = 0 tz = -1 # Default to -1 to signify that values not known; not critical to have, # though week_of_year = -1 week_of_year_start = -1 # weekday and julian defaulted to -1 so as to signal need to calculate # values weekday = julian = -1 found_dict = found.groupdict() for group_key in found_dict.iterkeys(): # Directives not explicitly handled below: # c, x, X # handled by making out of other directives # U, W # worthless without day of the week if group_key == 'y': year = int(found_dict['y']) # Open Group specification for strptime() states that a %y #value in the range of [00, 68] is in the century 2000, while #[69,99] is in the century 1900 if year <= 68: year += 2000 else: year += 1900 elif group_key == 'Y': year = int(found_dict['Y']) elif group_key == 'm': month = int(found_dict['m']) elif group_key == 'B': month = ['', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'].index(found_dict['B'].lower()) elif group_key == 'b': month = ['', 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'].index(found_dict['b'].lower()) elif group_key == 'd': day = int(found_dict['d']) elif group_key == 'H': hour = int(found_dict['H']) elif group_key == 'I': hour = int(found_dict['I']) ampm = found_dict.get('p', '').lower() # If there was no AM/PM indicator, we'll treat this like AM if ampm in ('', 'am'): # We're in AM so the hour is correct unless we're # looking at 12 midnight. # 12 midnight == 12 AM == hour 0 if hour == 12: hour = 0 elif ampm == 'pm': # We're in PM so we need to add 12 to the hour unless # we're looking at 12 noon. # 12 noon == 12 PM == hour 12 if hour != 12: hour += 12 elif group_key == 'M': minute = int(found_dict['M']) elif group_key == 'S': second = int(found_dict['S']) elif group_key == 'A': weekday = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].index(found_dict['A'].lower()) elif group_key == 'a': weekday = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].index(found_dict['a'].lower()) elif group_key == 'w': weekday = int(found_dict['w']) if weekday == 0: weekday = 6 else: weekday -= 1 elif group_key == 'j': julian = int(found_dict['j']) elif group_key in ('U', 'W'): week_of_year = int(found_dict[group_key]) if group_key == 'U': # U starts week on Sunday week_of_year_start = 6 else: # W starts week on Monday week_of_year_start = 0 elif group_key == 'Z': # Since -1 is default value only need to worry about setting tz if # it can be something other than -1. found_zone = found_dict['Z'].lower() for value, tz_values in enumerate((frozenset(['utc', 'cst', 'gmt']), frozenset(['cst']))): if found_zone in tz_values: # Deal with bad locale setup where timezone names are the # same and yet time.daylight is true; too ambiguous to # be able to tell what timezone has daylight savings if (time.tzname[0] == time.tzname[1] and time.daylight and found_zone not in ("utc", "gmt")): break else: tz = value break # If we know the week of the year and what day of that week, we can figure # out the Julian day of the year # Calculations below assume 0 is a Monday if julian == -1 and week_of_year != -1 and weekday != -1: # Calculate how many days in week 0 first_weekday = datetime_date(year, 1, 1).weekday() preceeding_days = 7 - first_weekday if preceeding_days == 7: preceeding_days = 0 # Adjust for U directive so that calculations are not dependent on # directive used to figure out week of year if weekday == 6 and week_of_year_start == 6: week_of_year -= 1 # If a year starts and ends on a Monday but a week is specified to # start on a Sunday we need to up the week to counter-balance the fact # that with %W that first Monday starts week 1 while with %U that is # week 0 and thus shifts everything by a week if weekday == 0 and first_weekday == 0 and week_of_year_start == 6: week_of_year += 1 # If in week 0, then just figure out how many days from Jan 1 to day of # week specified, else calculate by multiplying week of year by 7, # adding in days in week 0, and the number of days from Monday to the # day of the week if week_of_year == 0: julian = 1 + weekday - first_weekday else: days_to_week = preceeding_days + (7 * (week_of_year - 1)) julian = 1 + days_to_week + weekday # Cannot pre-calculate datetime_date() since can change in Julian #calculation and thus could have different value for the day of the week #calculation if julian == -1: # Need to add 1 to result since first day of the year is 1, not 0. julian = datetime_date(year, month, day).toordinal() - \ datetime_date(year, 1, 1).toordinal() + 1 else: # Assume that if they bothered to include Julian day it will #be accurate datetime_result = datetime_date.fromordinal((julian - 1) + datetime_date(year, 1, 1).toordinal()) year = datetime_result.year month = datetime_result.month day = datetime_result.day if weekday == -1: weekday = datetime_date(year, month, day).weekday() return time.struct_time((year, month, day, hour, minute, second, weekday, julian, tz)) libtpclient-py-0.3.2/tp/client/config.py0000644000175000017500000000154611164415250016344 0ustar timtim"""\ This file contains function useful for storing and retriving config. """ import sys import os import os.path try: import cPickle as pickle except ImportError: import pickle def configpath(): """\ Figures out where to save the preferences. """ dirs = [("APPDATA", "Thousand Parsec"), ("HOME", ".tp"), (".", "var")] for base, extra in dirs: if base in os.environ: base = os.environ[base] elif base != ".": continue rc = os.path.join(base, extra) if not os.path.exists(rc): os.mkdir(rc) return rc def load_data(file): """\ Loads preference data from a file. """ try: f = open(os.path.join(configpath(), file), "r") data = pickle.load(f) except IOError: return None return data def save_data(file, data): """\ Saves preference data to a file. """ f = open(os.path.join(configpath(), file), "w") pickle.dump(data, f) libtpclient-py-0.3.2/tp/client/SinglePlayer.py0000644000175000017500000004440511164415250017476 0ustar timtim"""\ Single player system support module. @author: Aaron Mavrinac (ezod) @organization: Thousand Parsec @license: GPL-2 """ # Python imports import os import sys import time import socket import urllib # find an elementtree implementation ET = None errors = [] try: import elementtree.ElementTree as ET except ImportError, e: errors.append(e) try: import cElementTree as ET except ImportError, e: errors.append(e) try: import lxml.etree as ET except ImportError, e: errors.append(e) try: import xml.etree.ElementTree as ET except ImportError, e: errors.append(e) if ET is None: raise ImportError(str(errors)) # import from local 2.6 subprocess module from subprocess import Popen # local imports import version class _Server(dict): """\ Dictionary subclass for server descriptions. """ def __init__(self): for k in ['longname', 'version', 'description', 'commandstring', 'cwd']: self[k] = '' self['forced'] = [] self['parameter'] = {} self['ruleset'] = {} class _AIClient(dict): """\ Dictionary subclass for AI client descriptions. """ def __init__(self): for k in ['longname', 'version', 'description', 'commandstring', 'cwd']: self[k] = '' self['rules'] = [] self['forced'] = [] self['parameter'] = {} class _Ruleset(dict): """\ Dictionary subclass for ruleset descriptions. """ def __init__(self): for k in ['longname', 'version', 'description']: self[k] = '' self['forced'] = [] self['parameter'] = {} class _Parameter(dict): """\ Dictionary subclass for parameter descriptions. """ def __init__(self): for el in ['type', 'longname', 'description', 'default', 'commandstring']: self[el] = '' class LocalList(dict): """\ Local list of servers, rulesets, and AI clients. """ def __init__(self): """\ Constructor. """ self['server'] = {} self['aiclient'] = {} # look for installed single player XML files ins_sharepath = [] if sys.platform == 'win32': try: import _winreg tpsp = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "Software\\Thousand Parsec\\SinglePlayer") i = 0 while True: name, value, t = _winreg.EnumValue(tpsp, i) ins_sharepath.append(value) i += 1 except WindowsError: pass else: ins_sharepath = ['/usr/share/tp', '/usr/share/games/tp', '/usr/local/share/tp', '/opt/tp', os.path.join(version.installpath, 'tp/client/singleplayer'), ] self.build('installed', ins_sharepath) # look for inplace single player XML files if hasattr(version, 'version_git'): inp_sharepath = [] for repo in [os.path.join('..', r) for r in os.listdir('..')]: if os.path.isdir(repo): inp_sharepath.append(repo) self.build('inplace', inp_sharepath) def build(self, stype, sharepath): """\ Build the local list from single player XML files. @param stype: The type of component (installed or inplace). @type stype: C{string} @param sharepath: A list of paths to search for XML files. @type sharepath: C{list} of C{string} """ for dir in sharepath: dir = os.path.abspath(dir) if not os.path.isdir(dir): continue print "Searching in %s..." % dir for xmlfile in os.listdir(dir): xmlfile = os.path.join(dir, xmlfile) if not os.path.isfile(xmlfile): continue if not xmlfile.endswith('xml'): continue try: xmltree = ET.parse(xmlfile) # FIXME: catch actual parsing exceptions (ExpatError?) except: continue # ensure this is a tpconfig document if xmltree._root.tag != 'tpconfig': continue # ensure it is of the type we are looking for if (not xmltree._root.attrib.has_key('type') and stype != 'installed') \ or xmltree._root.attrib.has_key('type') and xmltree._root.attrib['type'] != stype: continue print "Including %s." % xmlfile self.absorb_xml(xmltree, dir) # verify existence of command paths referred to in local list for t in self.keys(): for s in self[t].keys(): exe = os.path.join(self[t][s]['cwd'], self[t][s]['commandstring'].split()[0]) if not (os.path.exists(exe) or os.path.exists(exe + '.exe')): print "Removing %s: command %s not found." % (self[t][s]['longname'], exe) del self[t][s] def absorb_xml(self, tree, dir, d = None): """\ Recursively import an XML element tree into the local list. When called externally, the tree passed in should be an entire XML file parsed from the root, and the d parameter should not be specified. This implementation reads documents using the tpconfig DTD, and the classdict classes are the major (i.e. with sub-elements) element types specified in that DTD. @param tree: The XML element tree to import. @type tree: L{ET.ElementTree} @param dir: The absolute path of the source XML file. @type dir: C{string} @param d: A dictionary subclass instance for this type of tree (optional). @type d: C{dict} """ if d is None: d = self classdict = { 'server' : _Server, 'aiclient' : _AIClient, 'ruleset' : _Ruleset, 'parameter' : _Parameter, } for k in d.keys(): if type(d[k]) is dict: for s in tree.findall(k): sname = s.attrib['name'] if not d[k].has_key(sname): d[k][sname] = classdict[k]() self.absorb_xml(s, dir, d[k][sname]) elif type(d[k]) is list: for e in tree.findall(k): d[k].append(e.text) elif d[k] == '': if tree.attrib.has_key(k): d[k] = tree.attrib[k] elif tree.find(k) is not None: d[k] = tree.find(k).text if k == 'cwd': d[k] = os.path.join(dir, d[k]) class DownloadList(dict): """\ Builds a list of potentially downloadable servers and AI clients. """ def __init__(self, urlxml = 'http://thousandparsec.net/tp/downloads.xml', urldlp = 'http://thousandparsec.net/tp/downloads.php'): self.urlxml = urlxml self.urldlp = urldlp self['server'] = {} self['ai'] = {} self.rulesets = [] self.got = self.get_list() def get_list(self): """\ Fetch and parse the XML list of available downloads from TP web. @return: True if successful, false otherwise. @rtype: C{bool} """ try: dlxml = urllib.urlopen(self.urlxml) if not dlxml.info()['content-type'] == 'application/xml': return False xmltree = ET.parse(dlxml) for category in xmltree.findall('products/category'): cname = category.attrib['name'] if not cname in self.keys(): continue self[cname] = {} for product in category.findall('product'): if product.attrib['visible'] == 'no': continue self[cname][product.attrib['name']] = [] for rules in product.findall('rules'): self[cname][product.attrib['name']].append(rules.text) if not rules.text in self.rulesets: self.rulesets.append(rules.text) # FIXME: Bare excepts are bad. except: return False return True def list_servers_with_ruleset(self, rname): """\ Returns a list of available servers supporting the specified ruleset. @param rname: The ruleset name. @type rname: C{string} @return: A list of servers supporting the ruleset. @rtype: C{list} of C{string} """ servers = [] for sname in self['server'].keys(): if rname in self['server'][sname]: servers.append(sname) return servers def list_aiclients_with_ruleset(self, rname): """\ Returns a list of available AI clients supporting the specified ruleset. @param rname: The ruleset name. @type rname: C{string} @return: A list of AI clients supporting the ruleset. @rtype: C{list} of C{string} """ aiclients = [] for ainame in self['ai'].keys(): if rname in self['ai'][ainame]: aiclients.append(ainame) return aiclients def linkurl(self, component = None): """\ Returns the download page URL, optionally for a specific component type. @param component: The component type (optional). @type component: C{string} @return: A download URL (for component type or all). @rtype: C{string} """ if component in self.keys(): return self.urldlp + '?category=' + component else: return self.urldlp class InitError(Exception): """\ Generic initialization error, thrown to allow cleanup in certain situations. """ pass class SinglePlayerGame: """\ The single-player game manager. This is the object which should be instantiated externally to create a single player game. """ def __init__(self): # build local list self.locallist = LocalList() # initialize internals self.active = False self.sname = '' self.rname = '' self.sparams = {} self.rparams = {} self.opponents = [] def __del__(self): if self.active: self.stop() @property def servers(self): """\ Returns a list of available servers. @return: A list of servers. @rtype: C{list} of C{string} """ return self.locallist['server'].keys() @property def aiclients(self): """\ Returns a list of available AI clients. @return: A list of AI clients. @rtype: C{list} of C{string} """ return self.locallist['aiclient'].keys() @property def rulesets(self): """\ Returns a list of available rulesets from all servers. @return: A list of rulesets. @rtype: C{list} of C{string} """ rulesets = [] for sname in self.locallist['server'].keys(): for rname in self.locallist['server'][sname]['ruleset'].keys(): if rname not in rulesets: rulesets.append(rname) rulesets.sort() return rulesets def server_info(self, sname = None): """\ Returns information about a server. @param sname: Server name (optional). @type sname: C{string} @return Information about current or specified server. @rtype: C{dict} """ if sname is None: sname = self.sname try: return self.locallist['server'][sname] except KeyError, e: return None def aiclient_info(self, ainame = None): """\ Returns information about an AI client. @param ainame: AI client name. @type ainame: C{string} @return: Information about specified AI client. @rtype: C{dict} """ try: return self.locallist['aiclient'][ainame] except KeyError, e: return None def ruleset_info(self, rname = None): """\ Returns information about a ruleset. @param rname Ruleset name (optional). @return Information about current or specified ruleset. """ if rname is None: rname = self.rname if self.sname: sname = self.sname else: for sname in self.locallist['server'].keys(): if self.locallist['server'][sname]['ruleset'].has_key(rname): break try: return self.locallist['server'][sname]['ruleset'][rname] except KeyError, e: return None def list_servers_with_ruleset(self, rname = None): """\ Returns a list of servers supporting the current or specified ruleset. @param rname: Ruleset name (optional). @type rname: C{string} @return: A list of servers. @rtype: C{list} of C{string} """ if rname is None: rname = self.rname servers = [] for sname in self.locallist['server'].keys(): if self.locallist['server'][sname]['ruleset'].has_key(rname): servers.append(sname) servers.sort() return servers def list_aiclients_with_ruleset(self, rname = None): """\ Returns a list of AI clients supporting the current or specified ruleset. @param rname: Ruleset name (optional). @type rname: C{string} @return: A list of AI clients. @rtype: C{list} of C{string} """ if rname is None: rname = self.rname aiclients = [] for ainame in self.locallist['aiclient'].keys(): if rname in self.locallist['aiclient'][ainame]['rules']: aiclients.append(ainame) aiclients.sort() return aiclients def list_sparams(self, sname = None): """\ Returns the parameter list for the current or specified server. @param sname: Server name (optional). @type sname: C{string} @return: The server parameter list. @rtype: C{dict} """ if sname is None: sname = self.sname return self.locallist['server'][sname]['parameter'] def list_aiparams(self, ainame): """\ Returns the parameter list for the specified AI client. @param ainame: AI client name. @type ainame: C{string} @return: The AI client parameter list. @rtype: C{dict} """ return self.locallist['aiclient'][ainame]['parameter'] def list_rparams(self, sname = None, rname = None): """\ Returns the parameter list for the current or specified ruleset. @param rname: Ruleset name (optional). @type rname: C{string} @return: The ruleset parameter list. @rtype: C{dict} """ if sname is None: sname = self.sname if rname is None: rname = self.rname return self.locallist['server'][sname]['ruleset'][rname]['parameter'] def add_opponent(self, name, user, parameters): """\ Adds an AI client opponent to the game (before starting). @param name: The name of the AI client. @type name: C{string} @param user: The desired username of the opponent. @type user: C{string} @param parameters: A dictionary of parameters in the form {'name', 'value'}. @type parameters: C{dict} @return: True if successful, false otherwise. @rtype: C{bool} """ for aiclient in self.opponents: if aiclient['user'] is user: return False aiclient = { 'name' : name, 'user' : user, 'parameters' : parameters, } self.opponents.append(aiclient) return True def start(self): """\ Starts the server and AI clients. @return: Port number (OK to connect) or False. @rtype: C{int} """ import atexit atexit.register(self.stop) if self.active: return # find a free port s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('localhost',0)) port = s.getsockname()[1] s.close() try: # start server server = self.locallist['server'][self.sname] ruleset = server['ruleset'][self.rname] # start server - create server command line servercmd = server['commandstring'] # start server - set working directory servercwd = os.path.normpath(server['cwd']) if servercwd == '': servercwd = None else: servercmd = os.path.join(servercwd, servercmd) # start server - add forced parameters to command line for forced in server['forced']: servercmd += ' ' + forced # start server - set ruleset and port servercmd = servercmd % { 'rname' : self.rname, 'port' : port, } # start server - add regular parameters to command line for pname in server['parameter'].keys(): value = server['parameter'][pname]['default'] if self.sparams.has_key(pname): value = self.sparams[pname] value = self._format_value(value, server['parameter'][pname]['type']) if value is None: continue servercmd += ' ' + server['parameter'][pname]['commandstring'] % value # start server - add forced ruleset parameters to command line for forced in ruleset['forced']: servercmd += ' ' + forced # start server - add regular ruleset parameters to command line for pname in ruleset['parameter'].keys(): value = ruleset['parameter'][pname]['default'] if self.rparams.has_key(pname): value = self.rparams[pname] value = self._format_value(value, ruleset['parameter'][pname]['type']) if value is None: continue elif value == '': servercmd += ' ' + ruleset['parameter'][pname]['commandstring'] else: servercmd += ' ' + ruleset['parameter'][pname]['commandstring'] % value # start server - call the control script # TODO: allow redirection of stdout and stderr try: self.sproc = Popen(servercmd.split(), cwd = servercwd) except OSError, e: raise InitError(e) print "Running server with cmd:", servercmd # wait for the server to initialize # FIXME: use admin protocol if available to check this (loop) time.sleep(5) # start AI clients for aiclient in self.opponents: # create ai client command line aicmd = self.locallist['aiclient'][aiclient['name']]['commandstring'] # set working directory aicwd = os.path.normpath(self.locallist['aiclient'][aiclient['name']]['cwd']) if aicwd == '': aicwd = None else: aicmd = os.path.join(aicwd, aicmd) # add forced parameters to command line for forced in self.locallist['aiclient'][aiclient['name']]['forced']: aicmd += ' ' + forced # set port, ruleset and username aicmd = aicmd % { 'port' : port, 'rname' : self.rname, 'user' : aiclient['user'], } # add regular parameters to command line for pname in self.locallist['aiclient'][aiclient['name']]['parameter'].keys(): value = self.locallist['aiclient'][aiclient['name']]['parameter'][pname]['default'] if aiclient['parameter'].has_key(pname): value = aiclient['parameter'][pname] value = self._format_value(value, self.locallist['aiclient'][aiclient['name']]['parameter'][pname]['type']) if value is None: continue aicmd += ' ' + self.locallist['aiclient'][aiclient['name']]['parameter'][pname]['commandstring'] % value print "Running AI (%s) with cmd: %s" % (aiclient['name'], aicmd) # call the control script # TODO: allow redirection stdout and stderr try: aiclient['proc'] = Popen(aicmd.split(), cwd = aicwd) except OSError, e: raise InitError(e) # set active flag self.active = True # ensure that processes stay alive time.sleep(2) if not self.sproc.poll() is None: raise InitError for aiclient in self.opponents: if not aiclient['proc'].poll() is None: raise InitError except InitError, e: print e self.stop() return False return port def stop(self): """\ Stops the server and AI clients. Should be called by the client when disconnecting/closing. """ if not self.active: return # stop server if self.sname != '': try: self.sproc.kill() except OSError, e: pass self.sname = '' self.rname = '' # stop AI clients for aiclient in self.opponents: try: aiclient['proc'].kill() except OSError, e: pass self.opponents = [] # reset active flag self.active = False def _format_value(self, value, ptype): """\ Internal: formats a parameter value based on type. @oaram value: The value to format. @type value: C{string} @param ptype: The target value type (I, S, F, B). @type ptype: C{string} @return: The formatted value or None. """ if value is None or str(value) == '': return None elif ptype == 'I': return int(value) elif ptype == 'S' or type == 'F': return str(value) elif ptype == 'B' and str(value) == 'True': return '' else: return None libtpclient-py-0.3.2/tp/client/pyscheme/0000755000175000017500000000000011164431420016331 5ustar timtimlibtpclient-py-0.3.2/tp/client/pyscheme/expander.py0000644000175000017500000002632311164415250020522 0ustar timtim"""Syntax expansion. This is not yet a strong foundation for a macro system, but it's a start. I have to finish understanding R. Kent Dybvig's "Writing Hygenic Macros in Scheme with Syntax-Case". At the moment, this system takes expression datums and returns expanded expression datums. I'll probably need to rework this, since hygenic macros appear work on intermediate "Syntax" objects. """ __license__ = "MIT License" import pair import expressions import pogo import symbol import parser class Expander: def __init__(self): self._handlers = {} self._special_forms = { 'quote' : self.t_expand_quote, 'set!' : self.t_expand_setbang, 'define' : self.t_expand_define, 'if' : self.t_expand_if, 'lambda' : self.t_expand_lambda, 'begin' : self.t_expand_begin, } def get_keyword_tag(self, expr): """Tries to return the keyword. If no keyword exists, returns None.""" if pair.isPair(expr) and symbol.isSymbol(pair.car(expr)): return str(pair.car(expr)).lower() return None def install_handler(self, keyword, handler): """Adds a new macro expansion handler.""" self._handlers[keyword.lower()] = handler def expand(self, expr): """Given an expression, tries to expand it and its subexpressions into core forms.""" return pogo.pogo(self.t_expand(expr, pogo.land)) def t_expand(self, expr, cont): """Trampolined expander.""" if not pair.isList(expr): return pogo.bounce(cont, expr) handler = self._handlers.get(self.get_keyword_tag(expr), None) if handler: ## expand, and recursively call expansion again. return pogo.bounce(self.t_expand, handler(expr), cont) elif self.is_special_form(expr): return pogo.bounce(self.t_expand_special_form, expr, cont) else: ## We recursively expand all the subexpressions by mapping ## t_expand() across the elements. return pogo.bounce(pair.c_listMap, self.t_expand, expr, cont) def is_special_form(self, expr): """Returns True if expr looks like a core special form.""" return self.get_keyword_tag(expr) in self._special_forms def t_expand_special_form(self, expr, cont): """The core special forms have a different set of expansion rules.""" return self._special_forms[self.get_keyword_tag(expr)](expr, cont) def t_expand_quote(self, expr, cont): if expressions.isQuasiquoted(expr): expandedExp = expressions.expandQuasiquotation(text) return pogo.bounce(self.t_expand, expandedExp, cont) else: return pogo.bounce(cont, expr) def t_expand_setbang(self, expr, cont): variable = expressions.assignmentVariable(expr) value = expressions.assignmentValue(expr) def c_expanded(expandedValue): return pogo.bounce( cont, expressions.makeAssignment(variable, expandedValue)) return pogo.bounce(self.t_expand, value, c_expanded) def t_expand_define(self, expr, cont): variable = expressions.definitionVariable(expr) value = expressions.definitionValue(expr) def c_expanded(expandedValue): return pogo.bounce( cont, expressions.makeDefinition(variable, expandedValue)) return pogo.bounce(self.t_expand, value, c_expanded) def t_expand_if(self, expr, cont): pred = expressions.ifPredicate(expr) consq = expressions.ifConsequent(expr) altern = expressions.ifAlternative(expr) def c_pred(pred_value): def c_conseq(consequent_value): def c_altern(alternative_value): return pogo.bounce(cont, expressions.makeIf(pred_value, consequent_value, alternative_value)) return pogo.bounce(self.t_expand, altern, c_altern) return pogo.bounce(self.t_expand, consq, c_conseq) return pogo.bounce(self.t_expand, pred, c_pred) def t_expand_lambda(self, expr, cont): parameters = expressions.lambdaParameters(expr) body = expressions.lambdaBody(expr) def c_expanded(expandedBody): return pogo.bounce(cont, expressions.makeLambda(parameters, expandedBody)) return pogo.bounce(self.t_expand, body, c_expanded) def t_expand_begin(self, expr, cont): def c_expanded(expanded_actions): return pogo.bounce(cont, expressions.sequenceToExp(expanded_actions)) return pair.c_listMap(self.t_expand, expressions.beginActions(expr), c_expanded) ###################################################################### ## For convenience, we keep a singleton instance at the module level. _instance = Expander() expand = _instance.expand install_handler = _instance.install_handler ###################################################################### def LET_handler(exp): return expressions.letToApplication(exp) def COND_handler(exp): return expressions.condToIf(exp) def and_clauses(exp): return pair.cdr(exp) def makeAnd(clauses): return pair.cons(symbol.Symbol("AND"), clauses) def AND_handler(exp): """We have to consider three cases: (AND) ===> #f (AND e1) ===> e1 (AND e1 e2 ...) ===> (IF e1 (AND e2 ...) #f) """ clauses = and_clauses(exp) if pair.length(clauses) == 0: return symbol.false elif pair.length(clauses) == 1: return pair.car(clauses) else: return expressions.makeIf(pair.car(clauses), makeAnd(pair.cdr(clauses)), symbol.false) def makeOr(clauses): return pair.cons(symbol.Symbol("OR"), clauses) def OR_handler(exp): """We have to consider three cases: (OR) ===> #t (OR e1) ===> e1 (OR e1 e2 ...) ===> (let ((temp-val e1)) (IF temp-val temp-val (OR e2 ...))) ===> ((lambda (temp-val) (IF temp-val temp-val (OR e2 ...))) e1) """ clauses = and_clauses(exp) if pair.length(clauses) == 0: return symbol.true elif pair.length(clauses) == 1: return pair.car(clauses) else: temporarySymbol = symbol.makeUniqueTemporary() lambdaVal = expressions.makeLambda( pair.list(temporarySymbol), pair.list(expressions.makeIf(temporarySymbol, temporarySymbol, makeOr(pair.cdr(clauses))))) return expressions.makeApplication( lambdaVal, pair.list(pair.car(clauses))) def install_core_handlers(expanderInstance): expanderInstance.install_handler('AND', AND_handler) expanderInstance.install_handler('OR', OR_handler) expanderInstance.install_handler('LET', LET_handler) expanderInstance.install_handler('COND', COND_handler) ###################################################################### try: import unittest class ExpanderTests(unittest.TestCase): def setUp(self): self.expander = Expander() install_core_handlers(self.expander) def ep(self, string): """Expand and parse. Typing shortcut.""" return self.expander.expand(parser.parse(string)) def p(self, string): """Parse. Typing shortcut.""" return parser.parse(string) def testExpansionOnEmptyCase(self): self.assertEquals(self.p('()'), self.ep('()')) def testANDExpansion(self): self.assertEquals(self.p("#f"), self.ep("(and)")) self.assertEquals(self.p("foo"), self.ep("(and foo)")) self.assertEquals(self.p("(if 3 4 #f)"), self.ep("(and 3 4)")) self.assertEquals(self.p("(if 3 (if 4 5 #f) #f)"), self.ep("(and 3 4 5)")) self.assertEquals(3, self.ep("(and (and (and 3)))")) def testOrExpansion(self): self.assertEquals(self.p("#t"), self.ep("(or)")) self.assertEquals(self.p("blah"), self.ep("(or blah)")) self.ep("(or foo bar)") self.ep("(or (and 3) (and 4) (and 5))") ## FIXME: I have to figure out how to test this, without ## knowing in advance the temporary symbols used in the ## expansion... def testQuote(self): self.assertEquals(self.p("(quote and)"), self.ep("'and")) def testSetBang(self): self.assertEquals(self.p("(set! x 42)"), self.ep("(set! x (AND 42))")) def testDefine(self): self.assertEquals(self.p("(define x 42)"), self.ep("(define x (AND 42))")) def testIf(self): self.assertEquals(self.p("(if foo bar bah!)"), self.ep("(if (AND foo) (AND bar) (AND bah!))")) def testLambda(self): self.assertEquals(self.p("(lambda (x) one two)"), self.ep("(lambda (x) (AND one) (AND two))")) def testBegin(self): self.assertEquals(self.p("(begin one two three)"), self.ep("(begin (AND one) (begin (AND two)) (AND three))")) def testLet(self): self.assertEquals(self.p("((lambda (foo bar) (+ foo bar)) boo hoo)"), self.ep("""(let ((foo boo) (bar hoo)) (+ foo bar))""")) def testCond(self): self.assertEquals(self.p(""" (define fib (lambda (x) (if (= x 0) 0 (if (= x 1) 1 (+ (fib (- x 1) (- x 2))))))) """), self.ep(""" (define (fib x) (cond ((= x 0) 0) ((= x 1) 1) (else (+ (fib (- x 1) (- x 2)))))) """)) ## FIXME: add more complex cases here if __name__ == '__main__': unittest.main() except ImportError: pass libtpclient-py-0.3.2/tp/client/pyscheme/__init__.py0000644000175000017500000000070611164415250020450 0ustar timtim"""Package to hold pyscheme together. We also keep a few utility functions in here. """ __license__ = "MIT License" import scheme import pair import parser import symbol def make_interpreter(): """Simple utility function to make an pyscheme.scheme.Interpreter. Defaults to AnalyzingInterpreter for now.""" return scheme.AnalyzingInterpreter() def parse(s): """Parses string 's' into list structure.""" return parser.parse(s) libtpclient-py-0.3.2/tp/client/pyscheme/builtins.py0000644000175000017500000001626211164415250020546 0ustar timtim"""Defines a base built-in environment, with all the primitives.""" __license__ = "MIT License" import types import math import sys import pogo from symbol import Symbol, true, false import expressions import environment import pair from parser import parse import evaluator from error import SchemeError import operator def schemeParse(string): """Given a string, returns the pair list parsing of that string.""" return parse(string) def allNumbers(numbers): for n in numbers: if type(n) not in (types.IntType, types.LongType, types.FloatType): return 0 return 1 def schemeAdd(*numbers): if len(numbers) == 0: return 0 if not allNumbers(numbers): raise SchemeError, "primitive '+' --- arguments must be numbers" return reduce(operator.add, numbers) def schemeSubtract(*numbers): if len(numbers) == 0: raise SchemeError, "primitive '-' --- expects 1 or more arguments" if not allNumbers(numbers): raise SchemeError, "primitive '-' --- arguments must be numbers" if len(numbers) == 1: return -numbers[0] return reduce(operator.sub, numbers) def schemeMultiply(*numbers): if len(numbers) == 0: return 1 if not allNumbers(numbers): raise SchemeError, "primitive '*' --- arguments must be numbers" return reduce(operator.mul, numbers) def schemeDivide(*numbers): if len(numbers) == 0: raise SchemeError, "primitive '/' --- expects 1 or more arguments" if not allNumbers(numbers): raise SchemeError, "primitive '/' --- arguments must be numbers" return reduce(operator.div, numbers) def schemeRemainder(a, b): if not allNumbers([a, b]): raise SchemeError, "primitive 'remainder' --- arguments must be numbers" return a % b def schemePower(a, b): if not allNumbers([a, b]): raise SchemeError, "primitive 'power' --- arguments must be numbers" return a ** b def schemeQuotient(a, b): if not allNumbers([a, b]): raise SchemeError, "primitive 'quotient' --- arguments must be numbers" return int(a / b) def schemeSqrt(a): if not allNumbers([a]): raise SchemeError, "primitive 'sqrt' --- argument must be a number" return math.sqrt(a) def schemeFloor(a): if not allNumbers([a]): raise SchemeError, "primitive 'floor' --- argument must be a number" return int(math.floor(a)) def schemeCeiling(a): if not allNumbers([a]): raise SchemeError, "primitive 'ceiling' --- argument must be a number" return int(math.ceil(a)) def schemeNumericalEq(a, b): """Here, we need to make sure that we're returning the metacircular values of true or false.""" return schemeBooleanize(a == b) def schemeLessThan(a, b): return schemeBooleanize(a < b) def schemeLessThanOrEquals(a, b): return schemeBooleanize(a <= b) def schemeGreaterThan(a, b): return schemeBooleanize(a > b) def schemeGreaterThanOrEquals(a, b): return schemeBooleanize(a >= b) def schemeEqQuestion(a, b): if type(a) in (types.IntType, types.FloatType, types.LongType): return schemeBooleanize(a == b) return schemeBooleanize(a is b) def schemeEqualQuestion(a, b): return schemeBooleanize(a == b) def schemeListQuestion(a): return schemeBooleanize(pair.isList(a)) def schemeNullQuestion(a): return schemeBooleanize(pair.isNull(a)) def schemeDisplay(thing): sys.stdout.write(expressions.toString(thing, quoteStrings=0)) return Symbol("ok") def schemeWrite(thing): sys.stdout.write(expressions.toString(thing, quoteStrings=1)) return Symbol("ok") def schemeNewline(): sys.stdout.write("\n") return Symbol("ok") def schemeStringToSymbol(mystr): return Symbol(mystr) def schemeSymbolToString(mysymbol): return str(mysymbol) def schemeNumberToString(mynumber): return str(mynumber) def schemeStringAppend(*strings): if len(strings) == 0: return "" return reduce(operator.add, strings) def schemeNot(x): if x == false: return true return false def schemeQuit(): sys.stdout.write("bye\n") sys.exit(0) def schemeBooleanize(x): """Translates what Python considers true and false to the Scheme symbols "true" or "false".""" if x: return true return false def schemePairQuestion(L): return schemeBooleanize(pair.isPair(L)) def schemeError(*args): raise SchemeError, ' '.join(map(expressions.toString, args)) def wrapPrimitiveWithDefaults(f): """Normal primitives don't know what to do with their continuation or environment arguments, so this wrapper provides a nice default.""" def newPrimitive(cont, env, args): return pogo.bounce(cont, (f(*args))) return newPrimitive ###################################################################### def installPythonFunction(name, function, env, wrapDefaults=True): """Installs a new Python function as a primitive into the given environment 'env'""" if wrapDefaults: wrappedProcedure = expressions.makePrimitiveProcedure( wrapPrimitiveWithDefaults(function)) else: wrappedProcedure = expressions.makePrimitiveProcedure(function) environment.defineVariable(Symbol(name), wrappedProcedure, env) def setupEnvironment(): """Sets up a new environment with a bunch of fairly standard Scheme built-in primitives.""" PRIMITIVE_PROCEDURES = [ ["car", pair.car], ["cdr", pair.cdr], ["cons", pair.cons], ["append", pair.append], ["list", pair.list], ["set-car!", pair.setCarBang], ["set-cdr!", pair.setCdrBang], ["+", schemeAdd], ["-", schemeSubtract], ["*", schemeMultiply], ["/", schemeDivide], ["remainder", schemeRemainder], ["quotient", schemeQuotient], ["sqrt", schemeSqrt], ["floor", schemeFloor], ["ceiling", schemeCeiling], ["=", schemeNumericalEq], ["<", schemeLessThan], ["<=", schemeLessThanOrEquals], [">", schemeGreaterThan], [">=", schemeGreaterThanOrEquals], ["eq?", schemeEqQuestion], ["equal?", schemeEqualQuestion], ["list?", schemeListQuestion], ["pair?", schemePairQuestion], ["null?", schemeNullQuestion], ["display", schemeDisplay], ["write", schemeWrite], ["newline", schemeNewline], ["not", schemeNot], ["string->symbol", schemeStringToSymbol], ["symbol->string", schemeSymbolToString], ["number->string", schemeNumberToString], ["string-append", schemeStringAppend], ["quit", schemeQuit], ["exit", schemeQuit], ["error", schemeError], ["parse", schemeParse], ["pow", schemePower], ] initial_environment = environment.extendEnvironment( pair.NIL, pair.NIL, environment.THE_EMPTY_ENVIRONMENT) for name, proc in PRIMITIVE_PROCEDURES: installPythonFunction(name, proc, initial_environment) ## Finally, put true and false in there. environment.defineVariable(Symbol("#t"), Symbol("#t"), initial_environment) environment.defineVariable(Symbol("#f"), Symbol("#f"), initial_environment) return initial_environment libtpclient-py-0.3.2/tp/client/pyscheme/test_analyzer.py0000644000175000017500000000765411164415250021606 0ustar timtimimport unittest import sys import analyzer import parser import test_scheme from environment import THE_EMPTY_ENVIRONMENT, extendEnvironment, defineVariable from symbol import Symbol import pair from error import SchemeError import scheme class AnalyzerEvaluatorMixin: def setUp(self): ## sets the recursion limit to something fairly small to test ## out the the tail recursion. self.old_recursion_limit = sys.getrecursionlimit() sys.setrecursionlimit(55) self.interp = scheme.AnalyzingInterpreter() def tearDown(self): sys.setrecursionlimit(self.old_recursion_limit) def pe(self, s): """Parse and evaluate.""" return self.interp.eval(parser.parse(s)) class AnalyzingBasicSchemeTestCase( AnalyzerEvaluatorMixin, test_scheme.BasicSchemeTests, unittest.TestCase): pass class AnalyzingExtendedSchemeTestCase( AnalyzerEvaluatorMixin, test_scheme.ExtendedSchemeTests, unittest.TestCase): pass class IndependentAnalyzerTests(unittest.TestCase): def setUp(self): ## We set up a VERY minimal environment here for some tests. ## We also set the recursion limit to something dreadful to see that ## call/cc is doing the right thing. ## ## Note: these tests directly work with analyzer.eval, and not ## through the nicer scheme.AnalyzingInterpreter interface. self.env = extendEnvironment(pair.list(Symbol('pi')), pair.list(3.1415926), THE_EMPTY_ENVIRONMENT) defineVariable(Symbol("#t"), Symbol("#t"), self.env) defineVariable(Symbol("#f"), Symbol("#f"), self.env) self.old_recursion_limit = sys.getrecursionlimit() sys.setrecursionlimit(100) def tearDown(self): self.env = None sys.setrecursionlimit(self.old_recursion_limit) def pe(self, s): """Parse and evaluate.""" return analyzer.eval(parser.parse(s), self.env) def testSelfEvaluating(self): self.assertEquals(42, self.pe("42")) self.assertEquals(pair.list(), self.pe("()")) def testVariable(self): self.assertEquals(3.1415926, self.pe("pi")) self.assertRaises(SchemeError, self.pe, "nonexistantvalue") def testQuotation(self): self.assertEquals(Symbol("foobar"), self.pe("'foobar")) self.assertEquals(Symbol("foobar"), self.pe("'foobar")) self.assertEquals(pair.list(1, 2, 3), self.pe("'(1 2 3)")) def testAssignment(self): self.assertEquals(Symbol("ok"), self.pe("(set! pi 'three-point-one-four)")) self.assertEquals(Symbol("three-point-one-four"), self.pe("pi")) self.assertRaises(SchemeError, self.pe, "(set! nonexistantvalue 42)") def testDefinition(self): self.assertEquals(Symbol("ok"), self.pe("(define name 'danny)")) self.assertEquals(Symbol("danny"), self.pe("name")) def testIf(self): self.assertEquals(Symbol("ok"), self.pe("(if #t 'ok 'not-ok)")) self.assertEquals(Symbol("ok"), self.pe("(if #f 'not-ok 'ok)")) def testLambda(self): ## Can't test much yet without application. self.pe("(lambda (x) 'hello 'world)") def testBegin(self): self.assertEquals(Symbol("hello"), self.pe("(begin 'hello)")) self.assertEquals(Symbol("world"), self.pe("(begin 'hello '(this is a test) 'world)")) self.assertRaises(SchemeError, self.pe, "(begin)") def testSimpleApplication(self): self.assertEquals(Symbol("hello"), self.pe("((lambda (x) x) 'hello)")) self.assertEquals(Symbol("hi"), self.pe("((lambda (x y) y) '(this is a test) 'hi)")) if __name__ == '__main__': unittest.main() libtpclient-py-0.3.2/tp/client/pyscheme/pair.py0000644000175000017500000001773111164415250017652 0ustar timtim"""Implementation of pairs for Python. Pairs are like linked lists in Python. For historical reasons, we use the following funny names to construct and destructure lists: cons(head, tail) --- constructs a ConsPair that consists of a head and a tail. car(pair) --- returns the head of a ConsPair. cdr(pair) --- returns the tail of a ConsPair. Scheme's lists are built up of pair chains terminated by NIL. The reason we use a separate factory function --- cons() --- instead of directly using the ConsPair is because we may want the freedom to change implementation later on. In SICP, in fact, there's a totally screwy implementation that uses lambdas entirely, with no real data structure. There are a few more convenience functions here to rapidly make these pair structures. For example, there's a list() function here that, given a set of arguments, produces a pair chain. """ __license__ = "MIT License" """This module is not 'from pair import *' safe! Particularly because we define a few functions here that have names that conflict with builtins. Let's enforce this restriction.""" __all__ = [] from sets import Set from symbol import Symbol from error import SchemeError import pogo """The NIL atom. There should be only one!""" NIL = [] class ConsPair(list): pass def isNull(x): return x is NIL def isPair(p): return type(p) is ConsPair def cons(head, rest): """Returns the concatentation of head with rest.""" return ConsPair([head, rest]) def car(p): """Returns the head of the pair.""" if not isPair(p): raise SchemeError, "CAR --- argument is not a pair." if p is NIL: raise SchemeError, "CAR --- cannot take CAR of empty list." else: return p[0] def cdr(p): """Returns the tail of the pair.""" if not isPair(p): raise SchemeError, "CDR --- argument is not a pair." if p is NIL: raise SchemeError, "CDR --- cannot take CDR of empty list." else: return p[1] def cddr(p): return cdr(cdr(p)) def cdddr(p): return cdr(cdr(cdr(p))) def cadr(p): return car(cdr(p)) def caddr(p): return car(cdr(cdr(p))) def cadddr(p): return car(cdr(cdr(cdr(p)))) def setCarBang(pair, element): """Sets the head of the pair to the element.""" if not isPair(pair): raise SchemeError, "SET-CAR! --- cannot set car of non-pair." pair[0] = element return Symbol("ok") def setCdrBang(pair, element): """Sets the tail of the pair to the element.""" if not isPair(pair): raise SchemeError, "SET-CAR! --- cannot set car of non-pair." pair[1] = element return Symbol("ok") ## Hmm... some of these functions really belong in builtins.py. def isList(p): """Returns True if p refers to a list-like structure. Note: loopy structures don't qualify as lists. """ seenPairIds = Set() while True: if id(p) in seenPairIds: return 0 seenPairIds.add(id(p)) if isNull(p): return 1 if not isPair(p): return 0 p = cdr(p) def isDottedPair(p): """Returns True if p refers to an improper list, where the cdr is not a pair.""" if not isPair(p): return 0 elif isPair(cdr(p)) or isNull(cdr(p)): return 0 else: return 1 def length(p): """Returns the length of p. Assumes that p is a list.""" if not isList(p): raise SchemeError, "LENGTH --- not a list" length = 0 while True: if isNull(p): return length length += 1 p = cdr(p) def reverse(p): """Reverses a list.""" if not isList(p): raise SchemeError, "REVERSE --- not a list" result = NIL while not isNull(p): result = cons(car(p), result) p = cdr(p) return result def listMap(f, p): """Maps a function f across p.""" if not isList(p): raise SchemeError, "MAP --- not a list" resultsRev = NIL while not isNull(p): resultsRev = cons(f(car(p)), resultsRev) p = cdr(p) return reverse(resultsRev) def c_listMap(c_f, p, cont, allow_improper_lists=False): """Maps a function f across p, but in a continuation-passed style. 'c_f' is a function that takes a 2-tuple (element, cont), the element and the continuation. 'cont' is the continutation we apply on the mapped list. If the optional keyword parameter 'allow_improper' is set to True, then we'll also allow mapping across improper lists. """ if isNull(p): return pogo.bounce(cont, NIL) if not isPair(p): if allow_improper_lists: return pogo.bounce(c_f, p, cont) else: raise SchemeError, "CMAP --- not a list" def c_head(head_val): def c_tail(tail_val): return pogo.bounce(cont, cons(head_val, tail_val)) return pogo.bounce(c_listMap, c_f, cdr(p), c_tail, allow_improper_lists) return pogo.bounce(c_f, car(p), c_head) def append(*lists): """Appends all lists together.""" appended_list = lists[-1] for i in xrange(len(lists)-2, -1, -1): next_list = lists[i] appended_list = pogo.pogo( c_appendTwo(next_list, appended_list, pogo.land)) return appended_list def c_appendTwo(front, back, cont): """Appends two lists together. Written in continuation passing style.""" if not isList(front): raise SchemeError, "MAP --- not a list" if not isList(back): raise SchemeError, "MAP --- not a list" if isNull(front): return pogo.bounce(cont, back) def c(appendedRest): return pogo.bounce(cont, cons(car(front), appendedRest)) return pogo.bounce(c_appendTwo, cdr(front), back, c) def toPythonList(pair): """Does a shallow conversion of a pair list chain to a Python list.""" if not isList(pair): raise SchemeError, "not a list" elements = [] while not isNull(pair): elements.append(car(pair)) pair = cdr(pair) return elements """Let's save the old version of Python's list function.""" list_in_underlying_python = list def list(*elements): """Does a shallow conversion of a Python list to a pair chain. Warning: this does have the same name as the builtin list() function in Python. """ result = list_in_underlying_python(elements) result.reverse() return reduce(lambda x, y: cons(y, x), [NIL] + result) ###################################################################### try: import unittest class PairTests(unittest.TestCase): def testNull(self): self.assert_(isList(NIL)) def testReversal(self): self.assertEquals(list(1, 2, 3, 4, 5), reverse(list(5, 4, 3, 2, 1))) def testMapping(self): self.assertEquals(list(2, 4, 6, 8), listMap(lambda x: x*2, list(1, 2, 3, 4))) def testAppendTwo(self): self.assertEquals(list(1, 2, 3, 4), pogo.pogo(c_appendTwo(list(1, 2), list(3, 4), pogo.land))) def testContinuationMapping(self): def c_square(x, cont): return pogo.bounce(cont, x**2) self.assertEquals(cons(4, 9), pogo.pogo(c_listMap(c_square, cons(2, 3), pogo.land, allow_improper_lists=True))) self.assertRaises(SchemeError, pogo.pogo, pogo.bounce(c_listMap, c_square, cons(2, 3), pogo.land)) self.assertEquals(list(1, 4, 9, 16), pogo.pogo(c_listMap(c_square, list(1, 2, 3, 4), pogo.land))) if __name__ == '__main__': unittest.main() except ImportError: pass libtpclient-py-0.3.2/tp/client/pyscheme/parser.py0000644000175000017500000002524411164415250020211 0ustar timtim"""parser.py --- A small scheme parser. Danny Yoo (dyoo@hkn.eecs.berkeley.edu) This parser might be useful if we wanted to grab information from the user using Schemeish lists. It might also be nice if one is planning to write a Scheme interpreter in Python. *cough* The main functions to use in this module are tokenize(): makes a list of tokens out of a string parse(s): parse string s, and return a either an atomic value (symbol or number) or a pair. If anything bad happens during the parsing, we'll raise a ParserError. Some special notes: this module should behave well even under unusual input: there should be no possibility of hitting the recursion limit, since we are using trampolined style. """ __license__ = "MIT License" import re from symbol import Symbol import pair import pogo """The End Of File token is a sentinal that terminates a list of tokens.""" EOF_TOKEN = (None, None) """Here are the patterns we pay attention when breaking down a string into tokens.""" PATTERNS = [ ('whitespace', re.compile(r'(\s+)')), ('comment', re.compile(r'(;[^\n]*)')), ('(', re.compile(r'(\()')), (')', re.compile(r'(\))')), ('number', re.compile(r'''( [+\-]? ## optional sign, (?: ## followed by some ## decimals \d+\.\d+ | \d+\. | \.\d+ | \d+ ) ) ''', re.VERBOSE)), ('symbol', re.compile(r'''([a-zA-Z\+\=\?\!\@\#\$\%\^\&\*\-\/\.\>\<] [\w\+\=\?\!\@\#\$\%\^\&\*\-\/\.\>\<]*)''', re.VERBOSE)), ('string', re.compile(r''' " (([^\"] | \\")*) " ''', re.VERBOSE)), ('\'', re.compile(r'(\')')), ('`', re.compile(r'(`)')), ## fixme: add UNQUOTE-SPLICING form as well (',', re.compile(r'(,)')), ] def tokenize(s): """Given a string 's', return a list of its tokens. A token can be one of the following types listed in the PATTERNS above, and each token is a 2-tuple: (type, content).""" tokens = [] while 1: should_continue = 0 for tokenType, regex in PATTERNS: match_obj = regex.match(s) if match_obj: should_continue = 1 tokens.append( (tokenType, match_obj.group(1)) ) s = s[match_obj.span()[1] :] if should_continue == 0: break tokens.append(EOF_TOKEN) return filter(lambda x: x[0] not in ('whitespace', 'comment'), tokens) """With the lexer done, now let's direct our attention to the parser.""" ###################################################################### class ParserError(Exception): """Our personalized exception class.""" pass def peek(tokens): """Take a quick glance at the first token in our tokens list.""" if len(tokens) == 0: raise ParserError, "While peeking: ran out of tokens." return tokens[0] def eat(tokens, tokenType): """Digest the first token in our tokens list, making sure that we're biting on the right tokenType of thing.""" if len(tokens) == 0: raise ParserError, "While trying to eat %s: ran out of tokens." % \ (repr(tokenType),) if tokens[0][0] != tokenType: raise ParserError, "While trying to eat %s: got token %s instead." % \ (repr(tokenType), repr(tokens[0])) return tokens.pop(0) ###################################################################### def identity_cont(val): return pogo.land(val) """Below is our parser for symbol-lists. We'll use a recursive descent parsing technique, since Scheme's syntax is so well suited for it. Actually, it's a bit more complicated than this. There is one major problem with a naive rec-descent approach: with unusual input, we can run into Python's recursion stack limit. To get around this, we first wrote the parser normally, and then attacked the recursion using CPS and trampolining style. The result is that the parser works, even with evil input, but it's a bit harder to read. """ def parseSingleExpression(tokens, cont=identity_cont): """Returns a single Expression, given a sequence of tokens. Raises a ParserException if our tokens haven't been exhausted.""" def c(expression): eat(tokens, None) return pogo.bounce(cont, expression) return parseExpression(tokens, c) def parseExpression(tokens, cont=identity_cont): """Returns an Expression, given a sequence of tokens. An expression is made up of one of the following things: o A quoted expression o An atom (like a number or symbol or string) o A list. This procedure tries to take care of all these potentials.""" look_ahead_type = peek(tokens)[0] def make_c(quoteType): def c(expr): return pogo.bounce(cont, pair.list(Symbol(quoteType), expr)) return c if look_ahead_type == '\'': eat(tokens, '\'') return parseExpression(tokens, make_c('quote')) if look_ahead_type == '`': eat(tokens, '`') return parseExpression(tokens, make_c('quasiquote')) elif look_ahead_type == ',': eat(tokens, ',') return parseExpression(tokens, make_c('unquote')) elif look_ahead_type == '(': return parseList(tokens, cont) elif look_ahead_type in ('number', 'symbol', 'string'): return parseAtom(tokens, cont) else: raise ParserError, "While parsing Expression: no alternatives." def parseAtom(tokens, cont): """Returns an Atom, given a sequence of tokens. An atom is either a number, a symbol, or a string.""" if peek(tokens)[0] == 'number': return pogo.bounce(cont, toNumber(eat(tokens, 'number')[1])) elif peek(tokens)[0] == 'symbol': return pogo.bounce(cont, Symbol(eat(tokens, 'symbol')[1])) elif peek(tokens)[0] == 'string': return pogo.bounce(cont, eat(tokens, 'string')[1]) else: raise ParserError, "While parsing Atom: no alternatives." def toNumber(s): """Tries to convert string 's' into a number.""" try: return int(s) except ValueError: return float(s) def parseList(tokens, cont): """Parses a parenthesized list expression.""" eat(tokens, "(") def c_expressionsEaten(val): eat(tokens, ")") return pogo.bounce(cont, val) return pogo.bounce(parseExpressionStar, tokens, c_expressionsEaten) def parseExpressionStar(tokens, cont): """Tries to eat as many expressions as it can see.""" START_EXPR_SET = ('\'', '`', ',', '(', 'number', 'symbol', 'string') if peek(tokens) == ('symbol', '.'): ## Handle dotted pairs eat(tokens, "symbol") return pogo.bounce(parseExpression, tokens, cont) elif peek(tokens)[0] in START_EXPR_SET: def c_first_eaten(firstVal): def c_rest_eaten(restVal): return pogo.bounce(cont, pair.cons(firstVal, restVal)) return pogo.bounce(parseExpressionStar, tokens, c_rest_eaten) return pogo.bounce(parseExpression, tokens, c_first_eaten) else: return pogo.bounce(cont, pair.NIL) def parse(s): """Parse a single string. This is just a convenience function.""" return pogo.pogo(parseSingleExpression(tokenize(s), identity_cont)) ###################################################################### try: import unittest class ParserTests(unittest.TestCase): def testAtomParsing(self): self.assertEquals(42, parse("42")) self.assertEquals("particle-man", parse('"particle-man"')) def testBrokenNegativeExampleFromBaypiggies(self): ## embarassing case that didn't work during the Baypiggies meeting ## on September 9, 2004. Doh! self.assertEquals(-42, parse("-42")) def testLists(self): self.assertEquals(pair.list(1, 2), parse("(1 2)")) self.assertEquals(pair.list(1, 2, pair.list(3), 4), parse("(1 2 (3) 4)")) self.assertEquals(pair.list(pair.list(pair.list())), parse("((()))")) def testQuotation(self): self.assertEquals(pair.list(Symbol("quote"), Symbol("atom-man")), parse("'atom-man")) self.assertEquals(pair.list(Symbol("quasiquote"), Symbol("istanbul")), parse("`istanbul")) self.assertEquals(pair.list(Symbol("unquote"), Symbol("constantinople")), parse(",constantinople")) def testDottedPair(self): cons = pair.cons ## shortcut self.assertEquals(cons(Symbol("alpha"), Symbol("beta")), parse("(alpha . beta)")) self.assertEquals(cons(Symbol("a"), cons(Symbol("b"), cons(cons(Symbol("c"), Symbol("d")), pair.NIL))), parse("(a b (c . d))")) def testQuotedList(self): self.assertEquals(pair.list(Symbol("quote"), pair.list(Symbol("foo"), Symbol("bar"))), parse("'(foo bar)")) def testEmptyList(self): self.assertEquals(pair.NIL, parse("()")) def testStressWithSuperNesting(self): ## An evil test to see if this raises bad errors. N = 1000 bigNestedList = pair.list() for ignored in xrange(N-1): bigNestedList = pair.list(bigNestedList) try: self.assertEquals(bigNestedList, parse( "(" * N + ")" * N)) except RuntimeError, e: self.fail(e) if __name__ == '__main__': unittest.main() except ImportError: pass libtpclient-py-0.3.2/tp/client/pyscheme/scheme.py0000644000175000017500000002757311164415250020170 0ustar timtim#!/usr/bin/env python """A translation of a Scheme interpreter in Python, modeling the metacircular evaluator in Ch. 4 of The Structure and Interpretation of Computer Programs. (http://www-mitpress.mit.edu/sicp/) AUTHOR: Danny Yoo (dyoo@hkn.eecs.berkeley.edu) SYNOPSIS: It's Scheme. *grin* At least, quite a bit of Scheme. Scheme is a variant of Lisp, and it's used in many introductory CS courses. It is pretty powerful despite its deceptive size. For more information on Scheme, visit the Scheme home page: http://www.swiss.ai.mit.edu/projects/scheme OLD BUGS: One major deficiency of this implementation had in the past was the lack of tail call elimination. However, I've gotten this to work by using trampolined style. This involved rewriting the interpreter into CPS form, and trampolining the resulting tail calls. The resulting interpreter looks quite a bit uglier, but at least it shouldn't stack overflow easily. BUGS: Not all of R5RS is implemented, and probably will never be until I finish reading and understanding it. MISCELLANEOUS: This source code is for the public domain; I'm writing it just for the fun of it. This is certainly not meant to be efficient or even useful. *grin* My future goals is to make this usable enough to run something like the explicit-register simulator and compiler in SICP Ch. 5. I'm learning more about the advanced features in scheme (continuations, hygenic macros), so I'll probably use this as a future testbed to make sure I understand those ideas. """ __license__ = "MIT License" import sys import optparse import symbol import parser import evaluator import builtins import analyzer import prompt import error import expressions import environment import pogo import pair import expander ###################################################################### class Interpreter: """Meant to be subclassed. Subclasses should define _eval and _env in their constructors. _eval should be a callable that takes an expression and environment, and returns a value. _env should be an environment. """ def __init__(self): self._eval = None self._env = None self._expander = expander.Expander() expander.install_core_handlers(self._expander) def eval(self, exp): """Evaluates an expression exp.""" return self.get_evaluator()(self.get_expander().expand(exp), self.get_environment()) def install_function(self, name, function): """Installs a Python function into the toplevel environment.""" builtins.installPythonFunction(name, function, self.get_environment()) def repl(self): """A thin method around the repl function.""" return repl(self) def get_evaluator(self): """Returns the evaluation function.""" return self._eval def get_environment(self): """Returns the toplevel environment frame.""" return self._env def get_expander(self): """Returns the syntax expander.""" return self._expander def install_special_builtins(self): """Adds some unusual builtin cases that involve the interpreter itself.""" for (name, function) in self.get_special_builtins(): builtins.installPythonFunction(name, function, self.get_environment(), wrapDefaults=False) def get_special_builtins(self): """Returns a list of 2-tuples: name of the special builtin, and the function value of the builtin.""" return () class RegularInterpreter(Interpreter): """This interpreter evaluates expressions in a way similar to the metacircular evaluator described by SICP. The only significant difference is the internal use of trampolining style plus CPS.""" def __init__(self): Interpreter.__init__(self) self._eval = evaluator.eval self._env = builtins.setupEnvironment() self.install_special_builtins() def get_special_builtins(self): return (('load', self.schemeLoad), ('call/cc', self.schemeCallcc), ('call-with-current-continuation', self.schemeCallcc), ('eval', self.schemeEval), ('apply', self.schemeApply), ('dir', self.schemeDir)) def schemeDir(self, cont, env, args): """A quick function to inspect the first frame.""" if len(args) != 0: raise TypeError, ("dir expected at most 0 arguments, got %s" % len(args)) names = environment.firstFrame(env).keys() names.sort() return pogo.bounce(cont, pair.list(*names)) def schemeLoad(self, cont, env, args): """Special primitive: implements LOAD.""" symbolicFilename = str(args[0]) try: f = open(symbolicFilename) try: text = "(begin \n%s\n 'ok)" % f.read() finally: f.close() expandedExp = self.get_expander().expand(parser.parse(text)) return evaluator.teval(expandedExp, env, cont) except IOError, e: raise error.SchemeError, "LOAD error -- %s" % str(e) except TypeError, e: raise error.SchemeError, "LOAD error -- argument must be a string" def schemeApply(self, cont, env, args): return evaluator.apply(args[0], args[1], env, cont) def schemeEval(self, cont, env, args): expandedExp = self.get_expander().expand(args[0]) return evaluator.teval(expandedExp, env, cont) def schemeCallcc(self, cont, env, args): lambdaBody = args[0] def c2(val): return pogo.bounce(cont, val) cont_procedure = expressions.makeContinuationProcedure(c2) return evaluator.apply(lambdaBody, pair.list(cont_procedure), env, cont) ###################################################################### class AnalyzingInterpreter(Interpreter): """This version of the interpreter will do syntax analysis on its expressions before it executes them. Should be a win on speed.""" def __init__(self): Interpreter.__init__(self) self._eval = analyzer.eval self._env = builtins.setupEnvironment() self.install_special_builtins() def get_special_builtins(self): return ( ('load', self.schemeLoad), ('dir', self.schemeDir), ('call-with-current-continuation', self.schemeCallcc), ('call/cc', self.schemeCallcc), ('apply', self.schemeApply), ('eval', self.schemeEval), ) def schemeDir(self, cont, env, args): """A quick function to inspect the first frame.""" if len(args) != 0: raise TypeError, ("dir expected at most 0 arguments, got %s" % len(args)) names = environment.firstFrame(env).keys() names.sort() return pogo.bounce(cont, pair.list(*names)) def schemeLoad(self, cont, env, args): """Special primitive: implements LOAD.""" symbolicFilename = str(args[0]) try: f = open(symbolicFilename) try: text = "(begin \n%s\n 'ok)" % f.read() finally: f.close() expandedExp = self.get_expander().expand(parser.parse(text)) analyzedExp = analyzer.analyze(expandedExp) return analyzer.texec(analyzedExp, env, cont) except IOError, e: raise error.SchemeError, "LOAD error -- %s" % str(e) except TypeError, e: raise error.SchemeError, "LOAD error -- argument must be a string" def schemeApply(self, cont, env, args): return analyzer.apply(args[0], args[1], env, cont) def schemeEval(self, cont, env, args): expandedExp = self.get_expander().expand(args[0]) analyzedExp = analyzer.analyze(expandedExp) return analyzer.texec(analyzedExp, env, cont) def schemeCallcc(self, cont, env, args): analyzedLambdaBody = args[0] def c2(val): return pogo.bounce(cont, val) cont_procedure = expressions.makeContinuationProcedure(c2) return analyzer.apply(analyzedLambdaBody, pair.list(cont_procedure), env, cont) ###################################################################### class MinimalInterpreter(Interpreter): """ This is just a really minimal interpreter that supports only the core forms. No builtins, no expander, nothing except the following: self evaluating expressions variable references QUOTE SET! DEFINE IF LAMBDA BEGIN function application Furthermore, each evaluation generates a whole new environment, so no state should be saved between calls to eval. This is a demonstration of a restricted Scheme. In the future, we may even want to modify EVAL so that it goes through a version of pogo.pogo that bounds the number of bounces allowed before getting an answer. So we can do some interesting things here... """ def __init__(self): Interpreter.__init__(self) def get_evaluator(self): return analyzer.eval def get_environment(self): return environment.extendEnvironment(pair.list(), pair.list(), environment.THE_EMPTY_ENVIRONMENT) def get_expander(self): class SillyExpander: def expand(self, expr): return expr return SillyExpander() ###################################################################### def repl(interp=AnalyzingInterpreter()): """A fairly standard 'read->eval->print' loop.""" print 'Welcome to PyScheme! Type: (QUIT) to quit.\n' def promptCallback(s): try: print expressions.toString(interp.eval(parser.parse(s))) except (error.SchemeError, parser.ParserError), e: print "ERROR (%s): %s" % (e.__class__, e) p = prompt.Prompt(name="PyScheme", callback=promptCallback, quit_str=None) try: p.promptLoop() except SystemExit: pass ## Get out smoothly. def isNonInteractive(args): """Returns True if we want to run in non-interactive mode.""" return len(args) >= 1 def runNonInteractive(interp, args): """Executes the interpreter on args[0].""" interp.eval(pair.list(symbol.Symbol("load"), args[0])) def chooseInterpreter(options): """Choose which interpreter we should use.""" if options.interpreter_type == 'regular': return RegularInterpreter() else: return AnalyzingInterpreter() def configureOptionParser(): optparser = optparse.OptionParser() optparser.add_option("--regular", help="invoke regular interpreter", action="store_const", const="regular", dest="interpreter_type", ) optparser.add_option("--analyzer", help="invoke analyzing interpreter [default]", action="store_const", const="analyzer", dest="interpreter_type", ) return optparser def main(): """Main driver.""" optparser = configureOptionParser() options, args = optparser.parse_args() interp = chooseInterpreter(options) if isNonInteractive(args): runNonInteractive(interp, args) else: repl(interp) if __name__ == '__main__': main() libtpclient-py-0.3.2/tp/client/pyscheme/all_tests.py0000644000175000017500000000121211164415250020674 0ustar timtim#!/usr/bin/env python """Harness to run a bunch of tests.""" import unittest def suite(): moduleNames = ['pair', 'parser', 'pogo', 'test_scheme', 'test_analyzer', 'expressions', 'expander', ] suite = unittest.TestSuite() loader = unittest.defaultTestLoader for n in moduleNames: testCase = loader.loadTestsFromModule(__import__(n)) suite.addTest(testCase) return suite if __name__ == '__main__': suite = suite() runner = unittest.TextTestRunner() runner.run(suite) libtpclient-py-0.3.2/tp/client/pyscheme/test_scheme.py0000644000175000017500000003157311164415250021222 0ustar timtim#!/usr/bin/env python """ Unit tests on PyScheme. """ import unittest import symbol from symbol import Symbol from error import SchemeError import pair import sys import parser import scheme class RegularInterpreterMixin: def setUp(self): ## sets the recursion limit to something fairly small to test ## out the the tail recursion. self.old_recursion_limit = sys.getrecursionlimit() sys.setrecursionlimit(50) self.interp = scheme.RegularInterpreter() def tearDown(self): sys.setrecursionlimit(self.old_recursion_limit) def pe(self, s): return self.interp.eval(parser.parse(s)) class BasicSchemeTests: """Defines a set of test cases that assumes the presence of a pe() method.""" def testSimpleStuff(self): self.assertEquals(42, self.pe("42")) def testBadTypes(self): self.assertRaises(SchemeError, self.pe, "(* (list 1 2 3 4 5) 3)") def testEq(self): self.assertEquals(symbol.true, self.pe(""" (begin (define x 3) (define y '(4)) (eq? (cdr (cons x y)) y))""")) self.assertEquals(symbol.true, self.pe("(eq? (cdr '(1)) (cdr '(1)))")) self.assertEquals(symbol.false, self.pe("(eq? '(1) '(1))")) def testDefinition(self): self.assertRaises(SchemeError, self.pe, "pi") ## lookup before defn self.assertEquals(Symbol("ok"), self.pe("(define pi 3.1415926)")) self.assertEquals(3.1415926, self.pe("pi")) def testQuotation(self): self.assertEquals(pair.list(Symbol("foo"), Symbol("bar")), self.pe("'(foo bar)")) def testQuasiquotation(self): self.pe("(define one 1)") self.pe("(define two 2)") self.pe("(define three 3)") self.assertEquals(pair.list(Symbol("one"), Symbol("two"), 3), self.pe("`(one two ,three)")) self.assertEquals(pair.list(Symbol("one"), Symbol("two"), (pair.list (Symbol("unquote"), Symbol("three")))), self.pe("'(one two ,three)")) ## this evil case occurs in R5RS self.assertEquals(parser.parse("(a `(b ,(+ 1 2) ,(foo 4 d) e) f)"), self.pe("`(a `(b ,(+ 1 2) ,(foo ,(+ 1 3) d) e) f)")) def testVarArgs(self): self.pe("(define (mylist . args) args)") self.assertEquals(parser.parse("(1 2 3)"), self.pe("(mylist 1 2 3)")) self.pe("""(define (list2 a b . c) (list a b c))""") self.assertEquals(pair.list(1, 2, pair.list()), self.pe("(list2 1 2)")) def testMinusOne(self): self.assertEquals(-1, self.pe("(- 1)")) def testApply(self): self.assertEquals(15, self.pe("(apply + '(1 2 3 4 5))")) self.pe("(define (compose f g) (lambda (x) (f (g x))))") self.pe("(define (square x) (* x x))") self.pe("(define (double x) (* x 2))") self.assertEquals(64, self.pe("(apply (compose square double) '(4))")) def testTooManyAndTooFew(self): self.pe("(define (square x) (* x x))") self.assertRaises(SchemeError, self.pe, "(square)") self.assertRaises(SchemeError, self.pe, "(square 1 2)") self.pe("(define (f x . y) (cons x y))") self.assertRaises(SchemeError, self.pe, "(f)") def testAnotherQuasiquote(self): self.pe("(define x 42)") self.assertEquals(42, self.pe("x")) self.assertEquals(pair.list (Symbol("x"), pair.list(Symbol("quote"), pair.list(Symbol("x"), 42))), self.pe("`(x '(x ,x))")) def testLet(self): self.assertEquals(5, self.pe("""(let ((x 3) (y 4) (z 2)) (+ x z))""")) def testAssignment(self): self.pe("(define x 0)") self.pe("""(define (inc*2) (set! x (+ x 1)) (set! x (+ x 1)))""") self.pe("(inc*2)") self.assertEquals(2, self.pe("x")) def defineRecursiveFunctions(self): self.pe("""(define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1)))))""") self.pe("""(define (even? x) (if (= x 0) #t (odd? (- x 1))))""") self.pe("""(define (odd? x) (if (= x 0) #f (even? (- x 1))))""") def testRecursion(self): self.defineRecursiveFunctions() self.assertEquals(6, self.pe("(factorial 3)")) self.assertEquals(symbol.true, self.pe("(even? 4)")) def testTailRecursiveEval(self): """This is a test to see that eval() doesn't break, even on deeply recursive functions. Basically, this is a test of the trampoline.""" self.defineRecursiveFunctions() def myFactorial(n): return reduce(lambda x, y: x*y, range(1,n+1), 1) self.assertEquals(myFactorial(50), self.pe("(factorial 50)")) self.assertEquals(symbol.true, self.pe("(even? 42)")) def testArithmetic(self): self.assertEquals(3, self.pe("(+ 1 2)")) self.assertEquals(7, self.pe("(- 8 1)")) self.assertEquals(42, self.pe("(* 2 21)")) self.assertEquals(2, self.pe("(/ 4 2)")) def testIf(self): self.assertEquals(Symbol("danny"), self.pe("(if 42 'danny 'yoo)")) self.assertEquals(Symbol("yoo"), self.pe("(if #f 'danny 'yoo)")) def testCond(self): self.assertEquals(Symbol("ok"), self.pe( """(define (even? x) (= (remainder x 2) 0))""")) self.assertEquals(Symbol("ok"), self.pe( """(define (odd? x) (not (even? x)))""")) self.assertEquals(Symbol("ok"), self.pe( """(cond ((even? 3) "not-ok") ((odd? 2) "still-not-ok") (#t 'ok))""")) def testListPrimitives(self): self.assertEquals(symbol.true, self.pe("(list? '(1 2 3))")) self.assertEquals(symbol.false, self.pe("(list? (cons 1 (cons 2 3)))")) def testStringEvaluating(self): self.assertEquals("hello", self.pe("\"hello\"")) def testLambda(self): self.pe("""(define square (lambda (x) (* x x)))""") self.assertEquals(49, self.pe("(square 7)")) def testWithYOperator(self): """A stress test with the infamous Y operator. This one comes from Taming the Y Operator, by Guillermo Juan Rozas.""" self.pe("""(define (y f) ((lambda (g) (g g)) (lambda (x) (f (lambda () (x x))))))""") self.pe("""(define factorial (y (lambda (self) (lambda (n) (if (= n 0) 1 (* n ((self) (- n 1))))))))""") self.assertEquals(3628800, self.pe("(factorial 10)")) def testGensymConstruction(self): self.pe("""(define (make-counter prefix) (let ((i 0)) (lambda () (begin (set! i (+ i 1)) (string->symbol (string-append (symbol->string prefix) (number->string i)))))))""") self.pe("""(define gensym1 (make-counter 'g))""") self.pe("""(define gensym2 (make-counter 'h))""") self.assertEquals(Symbol("g1"), self.pe("(gensym1)")) self.assertEquals(Symbol("g2"), self.pe("(gensym1)")) self.assertEquals(Symbol("g3"), self.pe("(gensym1)")) self.assertEquals(Symbol("h1"), self.pe("(gensym2)")) def testAndAndOr(self): self.pe("""(define (how-tall? height) (cond ((or (<= height 0) (>= height 12)) 'impossible) ((and (< 0 height) (< height 3)) 'midget) ((and (<= 3 height) (< height 6.5)) 'medium) ((and (<= 6.5 height) (< height 12)) 'giant)))""") ## according to http://usedwigs.com/lists.html self.pe("(define mini-me (+ 2 (/ 8.0 12)))") ## according to http://www.geocities.com/Colosseum/3522/facts.htm self.pe("(define shaq (+ 7 (/ 1.0 12)))") ## according to http://animatedtv.about.com/cs/faqs/a/simpsonbios.htm self.pe("(define homer 6)") self.assertEquals(Symbol("giant"), self.pe("(how-tall? shaq)")) self.assertEquals(Symbol("midget"), self.pe("(how-tall? mini-me)")) self.assertEquals(Symbol("medium"), self.pe("(how-tall? homer)")) self.assertEquals(Symbol("impossible"), self.pe("(how-tall? 0)")) self.assertEquals(Symbol("impossible"), self.pe("(how-tall? 100)")) def testSetCarCdr(self): self.pe("(define x '(hello world))") self.pe("(define y x)") self.pe("(define z (cons 'hi 'earth))") self.pe("(set-car! x 'hi)") self.pe("(set-cdr! y 'earth)") self.assertEquals(pair.cons(Symbol("hi"), Symbol("earth")), self.pe("x")) self.assert_(self.pe("(eq? x y)")) self.assert_(self.pe("(not (eq? x z))")) self.assert_(self.pe("(equal? x z)")) def defineCons(self): self.pe("""(define cons (lambda (x y) (lambda (dispatch) (if (= dispatch 'car) x y))))""") def testApplication(self): self.defineCons() self.assertEquals(Symbol("ok"), self.pe("(define myname (cons 'danny 'yoo))")) self.assertEquals(Symbol("danny"), self.pe("(myname 'car)")) self.assertEquals(Symbol("yoo"), self.pe("(myname 'cdr)")) class ExtendedSchemeTests: """Another set of tests that explore some of the more esoteric features. Assumes that there's a pe() method that does parse/eval.""" def testEvalCallcc(self): self.assertEquals(42, self.pe("(call/cc (lambda (x) 42))")) self.assertEquals(42, self.pe("(call/cc (lambda (x) (x 42)))")) self.assertEquals(15, self.pe("(+ 1 2 3 4 (call/cc (lambda (x) 5)))")) self.assertEquals(5, self.pe("(call/cc (lambda (x) (+ 1 2 3 4 (x 5))))")) self.pe(""" (define (member x l) (call/cc (lambda (exit) (cond ((null? l) (exit #f)) ((equal? x (car l)) l) (#t (member x (cdr l)))))))""") self.assertEquals(self.pe("(list 4 5)"), self.pe("(member 4 (list 1 2 3 4 5))")) def testLoad(self): ## A small test of the stack module in the 't' test directory. self.pe('(load "t/stack.scm")') self.pe("(define s (make-stack))") self.pe("(push s 42)") self.pe("(push s 'foobar)") self.pe("(push s (lambda (x) x))") self.assertEquals(Symbol("muhaha"), self.pe("((pop s) 'muhaha)")) self.assertEquals(Symbol("foobar"), self.pe("(pop s)")) self.assertEquals(42, self.pe("(pop s)")) self.assertRaises(SchemeError, self.pe, "(pop s)") def testDir(self): self.assertEquals(pair.list(), self.pe("((lambda () (dir)))")) self.assertEquals(pair.list(Symbol("x")), self.pe("((lambda (x) (dir)) 42)")) def testSchemeEval(self): self.assertEquals(42, self.pe("(+ 2 (eval '(+ 30 10)))")) self.assertEquals(42, self.pe("(+ 2 (eval '(+ 30 ((lambda () 10)))))")) self.assertEquals(Symbol("hello"), self.pe("((eval '(lambda () 'hello)))")) ## Quick test to see that EVAL'ed expressions are also ## expanded, since AND is derived. self.assertEquals(Symbol("foo"), self.pe("(eval '(AND 'this 'is 'a 'foo))")) def testParse(self): self.assertEquals(parser.parse("(this is ( a test))"), self.pe('(parse "(this is ( a test))")')) class BasicSchemeTestCase(RegularInterpreterMixin, BasicSchemeTests, unittest.TestCase): pass class ExtendedSchemeTestCase(RegularInterpreterMixin, ExtendedSchemeTests, unittest.TestCase): pass if __name__ == '__main__': unittest.main() libtpclient-py-0.3.2/tp/client/pyscheme/environment.py0000644000175000017500000000543311164415250021257 0ustar timtim"""Environment stuff. Exercise 4.11 asks us to develop an alternative environment implementation. This is an implementation of environments that uses a chained list of Python dictionaries. """ __license__ = "MIT License" from symbol import Symbol from error import SchemeError import pair def enclosingEnvironment(env): return env[1:] def firstFrame(env): return env[0] THE_EMPTY_ENVIRONMENT = [] def makeFrame(var_pairs, val_pairs): """Note: here I diverge from SICP's implementation: instead of using a cons, I use a Python list. The selectors frameVariables() and frameValues() reflect this.""" car, cdr, isNull = pair.car, pair.cdr, pair.isNull frame = {} while not isNull(var_pairs): if pair.isPair(var_pairs): if not isNull(val_pairs): frame[car(var_pairs)] = car(val_pairs) var_pairs, val_pairs = cdr(var_pairs), cdr(val_pairs) else: raise SchemeError, "Too few arguments supplied" else: frame[var_pairs] = val_pairs var_pairs, val_pairs = pair.NIL, pair.NIL if isNull(val_pairs): return frame else: raise SchemeError, "Too many arguments supplied" def addBindingToFrame(var, val, frame): frame[var] = val def extendEnvironment(var_pairs, val_pairs, base_env): """Extends an environment with a new set of bindings. Extended to support Scheme's dotted notation for varargs.""" ## if Symbol('.') in vars: ## vararg_start = vars.index(Symbol('.')) ## return [makeFrame(vars[:vararg_start] + [vars[vararg_start+1]], ## vals[:vararg_start] + [vals[vararg_start:]]) ## ] + base_env ## elif len(vars) == len(vals): return [makeFrame(var_pairs, val_pairs)] + base_env ## FIXME: add length check! ## elif len(vars) < len(vals): ## raise SchemeError, \ ## "Too many arguments supplied %s %s" % (vars, vals) ## raise SchemeError, "Too few arguments supplied %s %s" % (vars, vals) def lookupVariableValue(var, env): while 1: if env == THE_EMPTY_ENVIRONMENT: raise SchemeError, "Unbound variable " + var frame = firstFrame(env) if frame.has_key(var): return frame[var] env = enclosingEnvironment(env) def setVariableValue(var, val, env): while 1: if env == THE_EMPTY_ENVIRONMENT: raise SchemeError, "Unbound variable -- SET! " + var frame = firstFrame(env) if frame.has_key(var): frame[var] = val return env = enclosingEnvironment(env) def defineVariable(var, val, env): frame = firstFrame(env) if frame.has_key(var): frame[var] = val return addBindingToFrame(var, val, frame) libtpclient-py-0.3.2/tp/client/pyscheme/error.py0000644000175000017500000000024311164415250020036 0ustar timtim"""Let's define a personalized SchemeError exception that will be thrown if bad things happen.""" __license__ = "MIT License" class SchemeError(Exception): pass libtpclient-py-0.3.2/tp/client/pyscheme/prompt.py0000644000175000017500000000415111164415250020230 0ustar timtim"""A small prompting class. Danny Yoo (dyoo@hkn.eecs.berkeley.edu) This hasn't been too documented yet. There's some sample uses on the bottom with the test() function below. Slightly modified to work nicely with parentheses. """ __license__ = "MIT License" ## First, let's see if we can load up readline and make raw_input() ## nice to work with. try: import readline except ImportError: pass import sys import pprint class Prompt: def __init__(self, name, quit_str = 'quit', quit_cmd=sys.exit, callback=None): self.name = name self.quit_str = quit_str self.quit_cmd = quit_cmd self.callback = callback self.s = '' def makePrompt(self): if self.countOpenParens(): return '[%s%d)] >>> ' % \ ('.' * (len(self.name) - len(str(self.countOpenParens())) - 1), self.countOpenParens()) else: return '[%s] >>> ' % self.name def countOpenParens(self): """Returns the number of open parens in self.s""" return self.s.count('(') - self.s.count(')') def promptLoop(self): while 1: try: self.s = self.s + '\n' + raw_input(self.makePrompt()) if self.s == self.quit_str: raise EOFError elif self.countOpenParens() > 0: pass elif self.s.strip() and self.callback: result = self.callback(self.s) if result != None: pprint.pprint(result) self.s = '' except EOFError: print return self.quit_cmd() except KeyboardInterrupt: self.s = '' print ###################################################################### ## Note: I should probably use the unit testing modules. def test(): def echoCallback(s): print s p = Prompt('test', callback=echoCallback) p.promptLoop() if __name__ == '__main__': test() libtpclient-py-0.3.2/tp/client/pyscheme/symbol.py0000644000175000017500000000607311164415250020221 0ustar timtim"""A simple "symbol" class for Python. Symbols are like strings, except that symbols should only be comparable to other symbols. This module is designed so that one should be able to do a "from symbol import *" on this. To create a new Symbol, use the Symbol() function, like this: hello = Symbol("hello") All symbols are interned so that comparison using 'is' will work. For example, Symbol("hello") is Symbol("h" + "ello") should work. By default, all symbols are case insensitive, but if the module variable CASE_SENSITIVE is set to a true value, symbols will become case sensitive. Also, we use the UserString to make sure that symbols are of a different "class" than Strings. isSymbol() depends on this representation, so be careful about changing it. """ __license__ = "MIT License" from UserString import UserString as __UserString import weakref __all__ = ['Symbol', 'isSymbol'] """By default, symbols are case-insensitive.""" CASE_SENSITIVE = 0 class __Symbol(__UserString): """Support class for symbols.""" def __eq__(self, other): """Comparison should only be possible between symbols. Since all Symbols are interned, this is equivalent to an 'is' check.""" return self is other """A global dictionary that contains all known symbols. __interned_symbols: strings -> symbols The dictionary uses weak references to reduce the chance of symbol abuse. """ __interned_symbols = weakref.WeakValueDictionary({}) def Symbol(s): """Generates a new Symbol that we guarantee can be compared in constant time to any other symbol.""" global __interned_symbols ## Defensive programming measure: assert not isinstance(s, __Symbol), ("%s already a symbol!" % s) if not CASE_SENSITIVE: s = s.lower() if not __interned_symbols.has_key(s): ## Subtle note: we have to use a local variable here to store the newSymbol. ## If we had tried something shorter like: ## ## __interned_symbol[s] = __Symbol(s) ## ## then the new Symbol will immediately evaporate since there ## aren't any hard references! Weak references can be tricky. newSymbol = __Symbol(s) __interned_symbols[s] = newSymbol return __interned_symbols[s] """Here are definitions of symbols that we should know about.""" false = Symbol("#f") true = Symbol("#t") __empty_symbol = Symbol("") def isSymbol(x): """Returns True if x is already a symbol.""" return type(x) == type(__empty_symbol) def makeUniqueTemporary(_counter = [0]): """Constructs a symbol that does not collide with any other symbol. I'll use this to help with the macro-expansion. NOTE/FIXME: we do this by making an "illegal" symbol which starts with a number and contains a space. The parser module doesn't allow such symbols to exist... although it's possible to subvert this by calling STRING->SYMBOL. So this mechanism is not perfect. """ while ('%d*** temporary' % _counter[0]) in __interned_symbols: _counter[0] += 1 return Symbol('%d*** temporary' % _counter[0]) libtpclient-py-0.3.2/tp/client/pyscheme/analyzer.py0000644000175000017500000002060711164415250020540 0ustar timtim"""An analyzer that does syntactic analysis of an expression. Danny Yoo (dyoo@hkn.eecs.berkeley.edu) analyze() returns a function that can be evaluated just by calling it with the environment and continuation. It's based (or bastardized, depending on your perspective... *grin*) on material in Chapter 4.1.7 of Structure and Interpretation of Computer Programs: http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-26.html#%25_sec_4.1.7 This is slightly more complicated since I'm using continuation passing style and trampolining to get around Python's call stack. The core forms that the analyzer recognizes is the following: self evaluating expressions variable references QUOTE SET! DEFINE IF LAMBDA BEGIN function application Other special forms are handled by derivation: the expander module translates the derived forms into these core forms. FIXME: the analyzer itself is not in continuation passing mode. The analyzed expressions that it produces do use CPS. FIXME: quasiquotation hasn't been pushed off into the expander yet. """ __license__ = "MIT License" import exceptions import traceback import expressions import pogo import environment import pair from symbol import Symbol from error import SchemeError applyInUnderlyingPython = apply def analyze(exp): """analyze(exp) -> lambda env, cont: ... Given an expression, returns a new lambda function that can be applied on an environment and continuation.""" if expressions.isSelfEvaluating(exp): return analyzeSelfEvaluating(exp) if expressions.isVariable(exp): return analyzeVariable(exp) if expressions.isQuoted(exp): return analyzeQuoted(exp) if expressions.isAssignment(exp): return analyzeAssignment(exp) if expressions.isDefinition(exp): return analyzeDefinition(exp) if expressions.isIf(exp): return analyzeIf(exp) if expressions.isLambda(exp): return analyzeLambda(exp) if expressions.isBegin(exp): return analyzeBegin(exp) ## Application checking must come last, after all the special forms. ## have been tested. if expressions.isApplication(exp): return analyzeApplication(exp) ## And if we get here, bad things have happened. raise SchemeError, ("Unknown expression type -- eval " + expressions.toString(exp)) def analyzeSelfEvaluating(exp): return (lambda env, cont: pogo.bounce(cont, exp)) def analyzeVariable(exp): return (lambda env, cont: pogo.bounce(cont, environment.lookupVariableValue(exp, env))) def analyzeQuoted(exp): text = expressions.textOfQuotation(exp) ## the common case is to handle simple quotation. if not expressions.isQuasiquoted(exp): return (lambda env, cont: pogo.bounce(cont, text)) else: return analyze(expressions.expandQuasiquotation(text)) def analyzeAssignment(exp): varName = expressions.assignmentVariable(exp) analyzedValueBody = analyze(expressions.assignmentValue(exp)) def analyzed(env, cont): def c(varVal): environment.setVariableValue(varName, varVal, env) return pogo.bounce(cont, Symbol("ok")) return pogo.bounce(analyzedValueBody, env, c) return analyzed def analyzeDefinition(exp): defName = expressions.definitionVariable(exp) analyzedDefValue = analyze(expressions.definitionValue(exp)) def analyzed(env, cont): def c(defVal): environment.defineVariable(defName, defVal, env) return pogo.bounce(cont, Symbol("ok")) return pogo.bounce(analyzedDefValue, env, c) return analyzed def analyzeIf(exp): analyzedPredicate = analyze(expressions.ifPredicate(exp)) analyzedConsequent = analyze(expressions.ifConsequent(exp)) analyzedAlternative = analyze(expressions.ifAlternative(exp)) def analyzed(env, cont): def c(predicateVal): if expressions.isTrue(predicateVal): return pogo.bounce(analyzedConsequent, env, cont) else: return pogo.bounce(analyzedAlternative, env, cont) return pogo.bounce(analyzedPredicate, env, c) return analyzed def analyzeSequence(exps): def sequentially(analyzedFirst, analyzedSecond): def c(env, cont): def c_first_exec(ignoredVal): return pogo.bounce(analyzedSecond, env, cont) return pogo.bounce(analyzedFirst, env, c_first_exec) return c if pair.isNull(exps): raise SchemeError, "Empty sequence -- ANALYZE" analyzedSeqs = analyze(expressions.firstExp(exps)) exps = expressions.restExps(exps) while not pair.isNull(exps): analyzedSeqs = sequentially(analyzedSeqs, analyze(expressions.firstExp(exps))) exps = expressions.restExps(exps) return analyzedSeqs def analyzeLambda(exp): params = expressions.lambdaParameters(exp) analyzedBody = analyzeSequence(expressions.lambdaBody(exp)) def analyzed(env, cont): return pogo.bounce(cont, expressions.makeProcedure(params, analyzedBody, env)) return analyzed def analyzeBegin(exp): analyzedActions = analyzeSequence(expressions.beginActions(exp)) return analyzedActions def analyzeApplication(exp): analyzedOperator = analyze(expressions.operator(exp)) analyzedOperands = analyzeOperands(expressions.operands(exp)) def analyzed(env, cont): def c_operator_exec(operatorVal): def c_operands_exec(operandVals): return pogo.bounce(apply, operatorVal, operandVals, env, cont) return pogo.bounce(analyzedOperands, env, c_operands_exec) return pogo.bounce(analyzedOperator, env, c_operator_exec) return analyzed def analyzeOperands(operands): analyzedOperands = pair.listMap(analyze, operands) ## Simple case: if no operands, return something that just ## passes NIL to its continuation def analyzed(env, cont): return execRands(analyzedOperands, env, cont) return analyzed def execRands(rands, env, cont): """Executes each operand and constructs a new list.""" def eval_first_cont(headVal): def eval_rest_cont(tailVal): return pogo.bounce(cont, pair.cons(headVal, tailVal)) return execRands(expressions.restOperands(rands), env, eval_rest_cont) if pair.isNull(rands): return pogo.bounce(cont, pair.NIL) return texec(expressions.firstOperand(rands), env, eval_first_cont) def eval(exp, env): """This version of eval calls analyze, and then texec()s it against the environment.""" analyzedExp = analyze(exp) return pogo.pogo(texec(analyzedExp, env, pogo.land)) def texec(analyzedExp, env, cont): """texec applies the analyzedExpression against the environment. Trampolined by eval(), and will be used by primitives like EVAL and CALL/CC.""" return analyzedExp(env, cont) def apply(procedure, arguments, env, cont): """Applies a procedure on a list of arguments.""" if expressions.isPrimitiveProcedure(procedure): return applyPrimitiveProcedure(procedure, arguments, env, cont) elif expressions.isContinuationProcedure(procedure): return applyContinuationProcedure(procedure, arguments) elif expressions.isCompoundProcedure(procedure): newEnv = environment.extendEnvironment ( expressions.procedureParameters(procedure), arguments, expressions.procedureEnvironment(procedure)) return texec(expressions.procedureBody(procedure), newEnv, cont) raise SchemeError, "Unknown procedure type -- apply " + str(procedure) def applyPrimitiveProcedure(proc, args, env, cont): try: return applyInUnderlyingPython( expressions.primitiveImplementation(proc), [cont, env, pair.toPythonList(args)]) except Exception, e: if isinstance(e, exceptions.SystemExit): raise e ## traceback.print_exc(e) raise SchemeError, e def applyContinuationProcedure(proc, args): try: return applyInUnderlyingPython( expressions.continuationImplementation(proc), pair.toPythonList(args)) except Exception, e: if isinstance(e, exceptions.SystemExit): raise e raise SchemeError, e libtpclient-py-0.3.2/tp/client/pyscheme/evaluator.py0000644000175000017500000001462411164415250020717 0ustar timtim"""A evaluator based on CPS form and trampolining. FIXME: add a lot more documentation here about how this all works. The core forms that the evaluator recognizes is the following: self evaluating expressions variable references QUOTE SET! DEFINE IF LAMBDA BEGIN function application Other special forms are handled by derivation: the expander module translates the derived forms into these core forms. FIXME: quasiquotation hasn't been pushed off into the expander yet. """ __license__ = "MIT License" import pogo import expressions import environment import exceptions import traceback import types import pair from parser import parse from symbol import false, Symbol from error import SchemeError def identity(val): """The identity function.""" return val """We need to save Python's apply() function, since we use it later on to evaluate primitive procedure calls. For symmetry, I'm also saving Python's eval() function within evalInUnderlyingPython().""" evalInUnderlyingPython = eval applyInUnderlyingPython = apply ###################################################################### ## The heart of the interpreter is eval/apply. ## ## Some notes here: I'm using trampolined style to get around Python's ## lack of tail recursion. def eval(exp, env): return pogo.pogo(teval(exp, env, pogo.land)) def teval(exp, env, cont): """Evaluates an expression 'exp' in an environment 'env'. Exercise 4.3 asks us to rewrite this in a more natural data-directed manner. Pychecker, also, doesn't like seeing so many 'return' statements in one function. *grin* """ if expressions.isSelfEvaluating(exp): return pogo.bounce(cont, exp) if expressions.isVariable(exp): return pogo.bounce(cont, environment.lookupVariableValue(exp, env)) if expressions.isQuoted(exp): return evalQuoted(exp, env, cont) if expressions.isAssignment(exp): return evalAssignment(exp, env, cont) if expressions.isDefinition(exp): return evalDefinition(exp, env, cont) if expressions.isIf(exp): return evalIf(exp, env, cont) if expressions.isLambda(exp): return pogo.bounce(cont, expressions.makeProcedure (expressions.lambdaParameters(exp), expressions.lambdaBody(exp), env)) if expressions.isBegin(exp): return evalSequence(expressions.beginActions(exp), env, cont) if expressions.isApplication(exp): return evalApplication(exp, env, cont) raise SchemeError, "Unknown expression type -- eval " + str(exp) def apply(procedure, arguments, env, cont): """Applies a procedure on a list of arguments.""" if expressions.isPrimitiveProcedure(procedure): return applyPrimitiveProcedure(procedure, arguments, env, cont) elif expressions.isContinuationProcedure(procedure): return applyContinuationProcedure(procedure, arguments) if expressions.isCompoundProcedure(procedure): newEnv = environment.extendEnvironment( expressions.procedureParameters(procedure), arguments, expressions.procedureEnvironment(procedure)) return evalSequence(expressions.procedureBody(procedure), newEnv, cont) raise SchemeError, "Unknown procedure type -- apply " + str(procedure) def applyPrimitiveProcedure(proc, args, env, cont): try: return applyInUnderlyingPython(expressions.primitiveImplementation(proc), [cont, env, pair.toPythonList(args)]) except Exception, e: if isinstance(e, exceptions.SystemExit): raise e raise SchemeError, e def applyContinuationProcedure(proc, args): try: return applyInUnderlyingPython(expressions.continuationImplementation(proc), pair.toPythonList(args)) except Exception, e: if isinstance(e, exceptions.SystemExit): raise e raise SchemeError, e def evalRands(exps, env, cont): """Given a list of expressions, returns a new list containing the values of evaluating each on of them. If the continuation is given, then calls cont() on the evaluated operands instead.""" def c1(head_val): def c2(tail_val): return pogo.bounce(cont, pair.cons(head_val, tail_val)) return evalRands(expressions.restOperands(exps), env, c2) if expressions.isNoOperands(exps): return pogo.bounce(cont, pair.list()) return teval(expressions.firstOperand(exps), env, c1) def evalIf(exp, env, cont): def c(predicate_val): if expressions.isTrue(predicate_val): return teval(expressions.ifConsequent(exp), env, cont) else: return teval(expressions.ifAlternative(exp), env, cont) return teval(expressions.ifPredicate(exp), env, c) def evalSequence(exps, env, cont): def c(val): return evalSequence(expressions.restExps(exps), env, cont) if expressions.isLastExp(exps): return teval(expressions.firstExp(exps), env, cont) else: return teval(expressions.firstExp(exps), env, c) def evalAssignment(exp, env, cont): def c(val): environment.setVariableValue(expressions.assignmentVariable(exp), val, env) return pogo.bounce(cont, Symbol("ok")) return teval(expressions.assignmentValue(exp), env, c) def evalDefinition(exp, env, cont): def c(val): environment.defineVariable( expressions.definitionVariable(exp), val, env) return pogo.bounce(cont, Symbol("ok")) return teval(expressions.definitionValue(exp), env, c) def evalApplication(exp, env, cont): def c1(operator_val): def c2(operands_val): return apply(operator_val, operands_val, env, cont) return evalRands(expressions.operands(exp), env, c2) return teval(expressions.operator(exp), env, c1) def evalQuoted(exp, env, cont): """Returns a quoted expression, using deepQuotedEval to look for UNQUOTE. Consequently, quoted elements are properly turned into cons pairs. """ text = expressions.textOfQuotation(exp) if expressions.isQuasiquoted(exp): expandedExp = expressions.expandQuasiquotation(text) return pogo.bounce(teval, expandedExp, env, cont) else: return pogo.bounce(cont, text) libtpclient-py-0.3.2/tp/client/pyscheme/expressions.py0000644000175000017500000003501211164415250021271 0ustar timtim"""Provides functions to extract portions of the expression tree and to construct new expressions. Heavily used in eval.py""" __license__ = "MIT License" import sys from sets import Set import pair import types from symbol import isSymbol, false, Symbol import pogo import parser def isSelfEvaluating(exp): """Returns True if the expression is self-evaluating.""" return isNumber(exp) or isString(exp) or pair.isNull(exp) def isNumber(x): """Returns True if we see a number.""" return type(x) in (types.IntType, types.FloatType, types.LongType) def isString(x): """Returns True if we see a string.""" return type(x) == types.StringType def isVariable(exp): """Returns true if the expression looks like a symbol.""" return isSymbol(exp) def isTaggedList(exp, tag): """Returns true if the expression is tagged with the 'tag'.""" if pair.isList(exp) and pair.length(exp) > 0: return pair.car(exp) == tag return 0 ###################################################################### def isQuoted(exp): return (isTaggedList(exp, Symbol("quote")) or isTaggedList(exp, Symbol("quasiquote"))) def makeQuoted(exp): return pair.list(Symbol("quote"), exp) def isQuasiquoted(exp): return isTaggedList(exp, Symbol("quasiquote")) def isUnquoted(exp): return isTaggedList(exp, Symbol("unquote")) def textOfQuotation(exp): return pair.cadr(exp) def textOfUnquotation(exp): return pair.cadr(exp) def expandQuasiquotation(exp, depth=1): """Takes a quasiquoted expression and constructs a new quoted expression. FIXME: this function is SO evil. *grin* Must clean this up. """ if not pair.isList(exp): return makeQuoted(exp) if isUnquoted(exp): if depth == 1: return textOfUnquotation(exp) else: return pair.list( Symbol("list"), makeQuoted(Symbol("unquote")), expandQuasiquotation(textOfUnquotation(exp), depth - 1)) if isQuasiquoted(exp): return pair.list( Symbol("list"), makeQuoted(Symbol("quasiquote")), expandQuasiquotation(textOfUnquotation(exp), depth + 1)) else: return pair.cons( Symbol("list"), pair.listMap(lambda subexp: expandQuasiquotation(subexp, depth), exp)) ###################################################################### def isLet(exp): return isTaggedList(exp, Symbol("let")) def letBindings(exp): return pair.cadr(exp) def letBindingVariables(bindings): if pair.isNull(bindings): return pair.list() return pair.cons(pair.car(pair.car(bindings)), letBindingVariables(pair.cdr(bindings))) def letBindingValues(bindings): if pair.isNull(bindings): return pair.list() return pair.cons(pair.cadr(pair.car(bindings)), letBindingValues(pair.cdr(bindings))) def letBody(exp): return pair.cdr(pair.cdr(exp)) def letToApplication(exp): bindings = letBindings(exp) return pair.cons(makeLambda(letBindingVariables(bindings), letBody(exp)), letBindingValues(bindings)) ###################################################################### def isAssignment(exp): """Assignments have the form (set! ).""" return isTaggedList(exp, Symbol("set!")) def assignmentVariable(exp): return pair.cadr(exp) def assignmentValue(exp): return pair.caddr(exp) def makeAssignment(var, val): return pair.list(Symbol("set!"), var, val) ###################################################################### def isDefinition(exp): return isTaggedList(exp, Symbol("define")) def definitionVariable(exp): if isSymbol(pair.cadr(exp)): return pair.cadr(exp) return pair.car(pair.cadr(exp)) def definitionValue(exp): if isSymbol(pair.cadr(exp)): return pair.caddr(exp) return makeLambda(pair.cdr(pair.cadr(exp)), pair.cddr(exp)) def makeDefinition(var, val): return pair.list(Symbol("define"), var, val) ###################################################################### def isLambda(exp): return isTaggedList(exp, Symbol("lambda")) def lambdaParameters(exp): return pair.cadr(exp) def lambdaBody(exp): return pair.cddr(exp) def makeLambda(parameters, body): return pair.append(pair.list(Symbol("lambda"), parameters), body) ###################################################################### def isIf(exp): return isTaggedList(exp, Symbol("if")) def ifPredicate(exp): return pair.cadr(exp) def ifConsequent(exp): return pair.caddr(exp) def ifAlternative(exp): if not pair.isNull(pair.cdddr(exp)): return pair.cadddr(exp) return false def makeIf(predicate, consequent, alternative): return pair.list(Symbol("if"), predicate, consequent, alternative) ###################################################################### def isBegin(exp): return isTaggedList(exp, Symbol("begin")) def beginActions(exp): return pair.cdr(exp) def isLastExp(seq): return pair.isNull(pair.cdr(seq)) def firstExp(seq): return pair.car(seq) def restExps(seq): return pair.cdr(seq) def sequenceToExp(seq): if pair.isNull(seq): return seq if isLastExp(seq): return firstExp(seq) return makeBegin(seq) def makeBegin(seq): return pair.cons(Symbol("begin"), seq) ###################################################################### def isApplication(exp): return pair.isList(exp) def operator(exp): if pair.length(exp) == 0: raise SchemeError, \ "No operator given for procedure application -- OPERATOR" return pair.car(exp) def operands(exp): return pair.cdr(exp) def isNoOperands(ops): return pair.isNull(ops) def firstOperand(ops): return pair.car(ops) def restOperands(ops): return pair.cdr(ops) def makeApplication(operators, operands): return pair.cons(operators, operands) ###################################################################### def isCond(exp): return isTaggedList(exp, Symbol("cond")) def condClauses(exp): return pair.cdr(exp) def isCondElseClause(clause): return condPredicate(clause) == Symbol("else") def condPredicate(clause): return pair.car(clause) def condActions(clause): return pair.cdr(clause) def condToIf(exp): return expandClauses(condClauses(exp)) def expandClauses(clauses): if pair.isNull(clauses): return false first = pair.car(clauses) rest = pair.cdr(clauses) if isCondElseClause(first): if pair.isNull(rest): return sequenceToExp(condActions(first)) raise SchemeError, "else clause isn't last -- condToIf" + clauses return makeIf(condPredicate(first), sequenceToExp(condActions(first)), expandClauses(rest)) def isTrue(x): return (x != false) def isFalse(x): return (x == false) ###################################################################### def makeProcedure(parameters, body, env): return pair.list(Symbol("procedure"), parameters, body, env) def isCompoundProcedure(p): return isTaggedList(p, Symbol("procedure")) def procedureParameters(p): return pair.cadr(p) def procedureBody(p): return pair.caddr(p) def procedureEnvironment(p): return pair.cadddr(p) ###################################################################### """Here's where we define the primitives of Scheme.""" def isPrimitiveProcedure(proc): return isTaggedList(proc, Symbol("primitive")) def primitiveImplementation(proc): return pair.cadr(proc) def makePrimitiveProcedure(proc): return pair.list(Symbol("primitive"), proc) ## We have to tag a continuation slightly different: applying a ## continuation abandons the current computation. def isContinuationProcedure(proc): return isTaggedList(proc, Symbol("continuation")) def continuationImplementation(proc): return pair.cadr(proc) def makeContinuationProcedure(proc): """Procedure is defined to be a function that takes a single 'val' argument.""" return pair.list(Symbol("continuation"), proc) ###################################################################### def toString(expr, quoteStrings=1): """Given a Scheme expression, returns a string that tries to nicely return it as a string. If quoteStrings is true, puts quotes around string expressions. Notes: this is a little tricky just because we have to account for loopy structures. We keep a set of expressions that we've already seen, to make sure we don't retrace any steps. """ seenExpressionIds = Set() def isLoopyPair(expr): return (pair.isPair(expr) and id(pair.cdr(expr)) in seenExpressionIds) def markExprAsSeen(expr): if not pair.isNull(expr): seenExpressionIds.add(id(expr)) def t_expressionToString(expr, cont): """Helper function for toString: written in CPS/trampolined form to avoid growing control context.""" if id(expr) in seenExpressionIds: return pogo.bounce(cont, "[...]") if pair.isNull(expr): return pogo.bounce(cont, "()") if not pair.isPair(expr): return t_atomExpressionToString(expr, cont) elif isPrimitiveProcedure(expr) or isCompoundProcedure(expr): return t_procedureExpressionToString(expr, cont) else: return t_pairExpressionToString(expr, cont) def t_atomExpressionToString(expr, cont): """Converts an atomic expression (string, number) to a string. Written in CPS/trampolined form.""" if type(expr) is str and quoteStrings: return pogo.bounce(cont, "\"%s\"" % expr.replace('"', '\\"')) else: return pogo.bounce(cont, str(expr)) def t_procedureExpressionToString(expr, cont): """Converts a procedure expression to a string.""" if isPrimitiveProcedure(expr): return pogo.bounce(cont, ('(%s %s )') % ( Symbol('primitive-procedure'), primitiveImplementation(expr))) elif isCompoundProcedure(expr): def c_parameters(parameterString): def c_body(bodyString): return pogo.bounce(cont, '(%s %s %s )' % (Symbol('compound-procedure'), parameterString, bodyString)) return t_expressionToString(procedureBody(expr), c_body) return t_expressionToString(procedureParameters(expr), c_parameters) def t_pairExpressionToString(expr, cont): """Converts a pair expression to a string.""" pieces = [] def loop(loop_exp, loop_cont): if pair.isNull(loop_exp): return pogo.bounce(loop_cont, pieces) elif pair.isDottedPair(loop_exp) or isLoopyPair(loop_exp): def c_car(carString): def c_cdr(cdrString): pieces.append(carString) pieces.append(".") pieces.append(cdrString) return pogo.bounce(loop_cont, pieces) return t_expressionToString(pair.cdr(loop_exp), c_cdr) return t_expressionToString(pair.car(loop_exp), c_car) else: def c_car(carString): pieces.append(carString) return pogo.bounce(loop, pair.cdr(loop_exp), loop_cont) return pogo.bounce(t_expressionToString, pair.car(loop_exp), c_car) markExprAsSeen(expr) return loop(expr, lambda pieces: pogo.bounce(cont, '(' + ' '.join(pieces) + ')')) return pogo.pogo(t_expressionToString(expr, pogo.land)) ###################################################################### try: import unittest class ExpressionTests(unittest.TestCase): def setUp(self): self._oldrecursionlimit = sys.getrecursionlimit() sys.setrecursionlimit(50) def tearDown(self): sys.setrecursionlimit(self._oldrecursionlimit) def constructEvilNestedExpression(self, n): result = pair.list() for i in xrange(n-1): result = pair.list(result) return result def testSimpleExpressionToString(self): self.assertEquals("5", toString(5)) self.assertEquals("x", toString(Symbol("x"))) def testSimpleListExpressionToString(self): self.assertEquals("(x)", toString(pair.list(Symbol("x")))) self.assertEquals("(1 2)", toString(pair.list(1, 2))) self.assertEquals("(1 (2) 3)", toString( pair.list(1, pair.list(2), 3))) def testDottedPairs(self): self.assertEquals("(1 . 2)", toString(pair.cons(1, 2))) def testNullList(self): self.assertEquals("()", toString(pair.list())) self.assertEquals("(() ())", toString( pair.list(pair.list(), pair.list()))) def testTailRecursiveExpressionToString(self): n = 1000 evilExpression = self.constructEvilNestedExpression(n) self.assertEquals('(' * n + ')' * n, toString(evilExpression)) def testRecursiveExpressionsDontKillUs(self): ## Makes sure that circular structures do not kill us. loopyList = pair.cons(1, 1) pair.setCdrBang(loopyList, loopyList) self.assertEquals("(1 . [...])", toString(loopyList)) def testLargerExpression(self): program = parser.parse(""" (define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1)))))""") self.assertEquals("(define (factorial x) (if (= x 0) 1 (* x (factorial (- x 1)))))", toString(program)) if __name__ == '__main__': unittest.main() except ImportError: pass libtpclient-py-0.3.2/tp/client/pyscheme/pogo.py0000644000175000017500000000422411164415250017654 0ustar timtim"""'pogo' defines primitives to do trampolined-style programming.""" import traceback __license__ = "MIT License" def pogo(bouncer): """A trampoline that bounces a single bouncer. See: http://www.cs.indiana.edu/hyplan/sganz/publications/icfp99/paper.pdf """ try: while True: if bouncer[0] == 'land': return bouncer[1] elif bouncer[0] == 'bounce': bouncer = bouncer[1](*bouncer[2]) else: traceback.print_exc() raise TypeError, "not a bouncer" except TypeError: traceback.print_exc() raise TypeError, "not a bouncer" def bounce(function, *args): """Returns a new trampolined value that continues bouncing on the trampoline.""" return ('bounce', function, args) def land(value): """Returns a new trampolined value that lands off the trampoline.""" return ('land', value) ###################################################################### try: import unittest class PogoTest(unittest.TestCase): def testFactorial(self): def iter_fact(n): result = 1 while n != 0: result *= n n -= 1 return result def tramp_fact(n, k=1): if n == 0: return land(k) return bounce(tramp_fact, n-1, k*n) self.assertEquals(6, pogo(tramp_fact(3))) self.assertEquals(6, pogo(tramp_fact(3))) for i in [1, 10, 100, 1000]: self.assertEquals(iter_fact(i), pogo(tramp_fact(i))) def testEvenOdd(self): def tramp_even(n): if n == 0: return land(True) return bounce(tramp_odd, n-1) def tramp_odd(n): if n == 0: return land(False) return bounce(tramp_even, n-1) for i in range(20) + [100, 1000, 10000]: self.assertEquals(i % 2 == 0, pogo(tramp_even(i))) if __name__ == '__main__': unittest.main() except ImportError: pass libtpclient-py-0.3.2/tp/client/cache.py0000644000175000017500000005653211164415250016147 0ustar timtim # Python imports import os import sys import copy import base64 import pprint import struct import traceback if sys.platform == "darwin": import pickle as pickle else: import cPickle as pickle from datetime import datetime def df(time): if type(time) in (float, int, long): return datetime.utcfromtimestamp(time).strftime('%c') elif type(time) is datetime: return time.strftime('%c') else: raise TypeError("Unable to output this type...") try: set() except NameError: from sets import Set as set # Other library imports from tp.netlib import Connection, failed, constants, objects from tp.netlib.objects import Header, Description, OrderDescs, DynamicBaseOrder # Local imports # FIXME: Should I think about merging the ChangeList and ChangeDict? from ChangeDict import ChangeDict from ChangeList import ChangeList, ChangeNode from threads import Event from threadcheck import thread_checker, thread_safe __CACHE = None class Cache(object): """\ This is the a cache of the data downloaded from the network. It can be pickled and restored at a later date to preserve the data accross application runs. """ __metaclass__ = thread_checker version = 5 class CacheEvent(Event): """\ Raised when the game cache is made dirty. Contains a reference to what was updated. """ def __init__(self, what, action, id, *args, **kw): Event.__init__(self) if what in Cache.readonly: raise ValueError("Can not change that!") elif not what in Cache.readwrite: raise ValueError("Invalid value (%s) for what" % (what,)) else: self.what = what if what in Cache.compound: if not action in Cache.actions_compound: raise ValueError("Invalid action (%s)" % (action,)) else: if not action in Cache.actions: raise ValueError("Invalid action (%s)" % (action,)) self.action = action self.id = id args = list(args) if what in Cache.compound: if len(args) == 2: self.node = args.pop(0) elif kw.has_key('node'): self.node = kw['node'] elif kw.has_key('nodes'): if not action is "remove": raise ValueError("Slots is only valid with a remove action") self.node = None self.nodes = kw['nodes'] else: raise TypeError("A node is required for compound types.") if not hasattr(self, "nodes"): self.nodes = [self.node] # Do a type check for the nodes for node in self.nodes: if (not node is None) and not isinstance(node, ChangeNode): raise TypeError("Nodes must be of type ChangeNode not %s (%r)" % (type(node), node)) assert node.inlist() if len(args) == 1: self.change = args.pop(0) elif kw.has_key('change'): self.change = kw['change'] elif action is "remove": pass else: raise TypeError("The actual change needs to be added.") def __str__(self): if not self.what: return "<%s full-update>" % (self.__class__.__name__,) elif hasattr(self, 'node'): if self.node is None: return "<%s %s %s id=%i nodes=%r>" % (self.__class__.__name__, self.what, self.action, self.id, self.nodes) else: return "<%s %s %s id=%i node=%r>" % (self.__class__.__name__, self.what, self.action, self.id, self.node) else: return "<%s %s %s id=%i>" % (self.__class__.__name__, self.what, self.action, self.id) __repr__ = __str__ class CacheDirtyEvent(CacheEvent): """\ Raised when the game cache is made dirty. Contains a reference to what was updated. """ pass class CacheUpdateEvent(CacheEvent): """\ Raised when the game cache is changed. Contains a reference to what was updated. If the what is None a new cache has been created. """ def __init__(self, what, *args, **kw): if what == None: Event.__init__(self) self.what = None else: CacheEvent.__init__(self, what, *args, **kw) # Read Only things can only be updated via the network readonly = ("features", "objects", "orders_probe", "boards", "resources", "components", "properties", "players", "resources") # These can be updated via either side readwrite = ("orders", "messages", "categories", "designs") # How we can update the Cache actions = ("create", "remove", "change") actions_compound = ("create before", "create after", "remove", "change") compound = ("orders", "messages") @staticmethod def key(server, username): key = server p = ['tp://', 'tps://', 'http://', 'https://'] found = False for p in p: if key.startswith(p): found = True break if not found: key = 'tp://' + key if key.find('@') == -1: p, s = key.split('//', 1) key = "%s//%s@%s" % (p, username, s) return key @staticmethod def configkey(key): key = base64.encodestring(key)[:-2] return key @staticmethod def configdir(): dirs = [("APPDATA", "Thousand Parsec"), ("HOME", ".tp"), (".", "var")] for base, extra in dirs: if base in os.environ: base = os.environ[base] break elif base != ".": continue return os.path.join(base, extra) def __init__(self, key, configdir=None, new=False): """\ It is important that key constructed the following way, protocol://username@server:port/ Everything must be there, even if the port is the default. """ if configdir == None: configdir = Cache.configdir() if not os.path.exists(configdir): os.mkdir(configdir) key = Cache.configkey(key) self.file = os.path.join(configdir, "cache.%s" % (key,)) if os.path.exists(self.file) and not new: # Load the previously cached status print "Loading previous saved data (from %s)." % (self.file,) try: self.load() return except (IOError, EOFError, KeyError, pickle.PickleError), e: print e traceback.print_exc() print "Unable to load the data, saved cache must be corrupt." print "Creating the Cache fresh (%s)." % (self.file,) self.new() def new(self): # Features self.features = [] # The object stuff self.objects = ChangeDict() self.orders = ChangeDict() self.orders_probe = ChangeDict() # The message boards self.boards = ChangeDict() self.messages = ChangeDict() # Design stuff self.categories = ChangeDict() self.designs = ChangeDict() self.components = ChangeDict() self.properties = ChangeDict() self.players = ChangeDict() self.resources = ChangeDict() @thread_safe # FIXME: This probably isn't thread safe! def apply(self, *args, **kw): """\ Given a CacheDirtyEvent, it sets up the changes... """ evt = Cache.CacheDirtyEvent(*args, **kw) if not evt.what in Cache.compound: return evt else: d = getattr(self, evt.what)[evt.id] if evt.action == "remove": for node in evt.nodes: node.AddState("removing") evt.change = None elif evt.action == "change": assert len(evt.nodes) == 1 assert not evt.node.LastState in ("removed", "removing") evt.node.AddState("updating", evt.change) evt.change = evt.node elif evt.action.startswith("create"): assert len(evt.nodes) == 1 # Create the new node newnode = ChangeNode(None) newnode.AddState("creating", evt.change) # Insert the node if evt.action == "create after": d.insert_after(evt.node, newnode) elif evt.action == "create before": d.insert_before(evt.node, newnode) else: assert False, "Unknown action!" # Set the new node as the change evt.change = newnode else: assert False, "Unknown action!" return evt def commit(self, evt): """\ Given a CacheDirtyEvent it applies the changes to the cache. It should be called after the changes have been confimed by the server. It then mutates the event into a CacheUpdateEvent. """ if not isinstance(evt, self.CacheDirtyEvent): raise TypeError("I can only accept CacheDirtyEvents") if not evt.what in Cache.compound: if evt.action == "create" or evt.action == "change": getattr(self, evt.what)[evt.id] = (-1, evt.change) elif evt.action == "remove": del getattr(self, evt.what)[evt.id] else: d = getattr(self, evt.what)[evt.id] if evt.action.startswith("create") or evt.action == "change": node = evt.change assert node.CurrentState in ("creating", "updating"), "Current state (%s) doesn't match action %s" % (node.CurrentState, evt.action) node.PopState() elif evt.action == "remove": for node in evt.nodes: assert node.CurrentState == "removing" del d[node.id] node.PopState() else: raise SystemError("Unknown node state!") evt.__class__ = self.CacheUpdateEvent def load(self): """\ """ f = open(self.file, 'rb') # Read in the version number v, = struct.unpack('!I', f.read(4)) if v != self.version: raise IOError("The cache is not of this version! (It's version %s)" % (v,)) orderdescs, = struct.unpack('!I', f.read(4)) # Now load the order cache for i in xrange(0, orderdescs): d = f.read(Header.size) p = Header.fromstr(d) d = f.read(p.length) p.__process__(d) assert isinstance(p, Description) p.register() # First load the pickle d = pickle.load(f) if d.has_key('file'): del d['file'] # Stop the file being loaded for clist in d['orders'].values(): for node in clist: for pending in node._pending: pending.__class__ = OrderDescs()[pending.subtype] node._what.__class__ = OrderDescs()[node._what.subtype] self.__dict__.update(d) def save(self): """\ """ # We don't want this filename appearing in the cace file = self.file del self.file # Save the cache f = open(file, 'wb') f.write(struct.pack('!I', self.version)) # Save each dynamic order description descriptions = OrderDescs() f.write(struct.pack('!I', len(descriptions))) for orderdesc in descriptions.values(): f.write(str(orderdesc.packet)) p = copy.copy(self.__dict__) del p['_thread'] # Stop referencing the dynamic orders for clist in p['orders'].values(): for node in clist: for pending in node._pending: subtype = pending.subtype pending.__class__ = DynamicBaseOrder pending.subtype = subtype subtype = node._what.subtype node._what.__class__ = DynamicBaseOrder node._what.subtype = subtype pickle.dump(p, f) # FIXME: The above copy should not be mutating! for clist in p['orders'].values(): for node in clist: for pending in node._pending: pending.__class__ = OrderDescs()[pending.subtype] node._what.__class__ = OrderDescs()[node._what.subtype] f.close() # Restore the file self.file = file def update(self, connection, callback): """\ Updates the cache using the connection. The callback function is called in the following way, callback(group=, state=, message=) The message string is a human readable message about what is happening. Group is the current group of things been updated the possible choices are, objects orders orders_probe boards messages categories designs components properties players resources State is one of the following, start - no more arguments todownload - total, the total number of things to be downloaded progress - some sort of undetermined progress occured failure - some sort of failure when downloading occured downloaded - amount, the number of things which have been downloaded finished - no more arguments """ c = callback # FIXME: We should restart with an empty cache if the following has happened # FIXME: This should compare any read-only attributes and see if they have change # FIXME: This should check the current turn and see if the turn is strange (IE gone back in time) # FIXME: Should check that none of the Order definitions have changed # Get the features this server support # c("connecting", "todownload", message=_("Looking for supported features...")) self.features = connection.features() # c("connecting", "finished") c("orderdescs", "start", message=_("Getting order descriptions...")) c("orderdescs", "progess", message=_("Working out the number of order descriptions to get..")) ids = [] for id, time in connection.get_orderdesc_ids(iter=True): if OrderDescs().has_key(id) and hasattr(OrderDescs()[id], "modify_time"): if time <= OrderDescs()[id].modify_time: continue ids.append(id) c("orderdescs", "todownload", todownload=len(ids)) for id in ids: desc = connection.get_orderdescs(id=id)[0] # Did we download the order description okay? if not failed(desc): c("orderdescs", "downloaded", amount=1, \ message=_("Got order description %(order)s (ID: %(id)i) (last modified at %(time)s)...") % {'order': desc._name, 'id': id, 'time': time}) desc.register() else: c("orderdescs", "failure", message=_("Failed to get order description with ID %(id)i (last modified at %(time)s)...") % {'id': id, 'time': time}) c("orderdescs", "finished", message=_("Recieved all order descriptions...")) # Get all the objects ############################################################################# ############################################################################# toget = self.__getObjects(connection, "objects", callback) if toget > 0: self.__getSubObjects(connection, toget, "objects", "orders", "order_number", callback) else: c("orders", "finished", message=_("Don't have any orders to get..")) toget = self.__getObjects(connection, "boards", callback) if toget > 0: self.__getSubObjects(connection, toget, "boards", "messages", "number", callback) else: c("messages", "finished", message=_("Don't have any messages to get..")) self.__getObjects(connection, "categories", callback) self.__getObjects(connection, "designs", callback) self.__getObjects(connection, "components", callback) self.__getObjects(connection, "properties", callback) # self.__getObjects(connection, "players", callback) self.__getObjects(connection, "resources", callback) c("players", "start", message=_("Getting player objects...")) playerids = self.players.keys() if len(playerids) > 0: playerids.sort() i = playerids[-1]+1 else: i = 0 while True: player = connection.get_players(i) if failed(player): break else: self.players[i] = player[0] c("players", "downloaded", amount=1, \ message=_("Got player %(player)s (ID: %(id)i)...") % {'player': player[0].name, 'id': player[0].id}) i += 1 c("players", "finished", message=_("Received all player objects...")) # self.players[0] = connection.get_players(0)[0] def __getObjects(self, connection, plural_name, callback): """\ Get a thing which has a container. """ c = callback pn = plural_name if pn[-3:] == 'ies': sn = pn[:-3]+'y' elif pn[-1:] == 's': sn = pn[:-1] else: sn = pn def cache(id=None, self=self, pn=pn): if id==None: return getattr(self, pn) else: return getattr(self, pn)[id] c(pn, "start", message=_("Getting %s...") % pn) # Figure out the IDs to download c(pn, "progess", message=_("Working out the number of %s to get..") % pn) toget = [] ids = [] for id, time in getattr(connection, "get_%s_ids" % sn)(iter=True): ids.append(id) if not cache().has_key(id): c(pn, "info", message=_("%(plural_name)s: Getting %(id)s as not cache().has_key(id)") % {'plural_name': pn, 'id': id}) toget.append(id) elif time > cache().times[id]: c(pn, "info", message=_("%(plural_name)s: Getting %(id)s (%(name)s) as %(time)s > %(cached_time)s") % {'plural_name': pn, 'id': id, 'name': cache(id).name, 'time': time, 'cached_time': cache().times[id]}) toget.append(id) else: c(pn, "info", message=_("%(plural_name)s: Not getting %(id)s (%(name)s) as %(time)s <= %(cached_time)s") % {'plural_name': pn, 'id': id, 'name': cache(id).name, 'time': time, 'cached_time': cache().times[id]}) # Callback function def OnPacket(p, c=c, pn=pn, sn=sn, objects=objects): if isinstance(p, getattr(objects, sn.title())): c(pn, "downloaded", amount=1, \ message=_("Got %(singular_name)s %(name)s (ID: %(id)i) (last modified at %(time)s)...") % {'singular_name': sn, 'name': p.name, 'id': p.id, 'time': p.modify_time}) if len(toget) < 1: c(pn, "finished", message=_("No %s to get, skipping...") % pn) return 0 # Download the XXX c(pn, "todownload", \ message=_("Have %(ammount)i %(plural_name)s to get...") % {'ammount': len(toget), 'plural_name': pn}, todownload=len(toget)) frames = getattr(connection, "get_%s" % pn)(ids=toget, callback=OnPacket) if failed(frames): raise IOError("Strange error occured, unable to request %s." % pn) # Match the results to the associated ids for id, frame in zip(toget, frames): if not failed(frame): if cache().has_key(id): c(pn, "info", \ message=_("%(plural_name)s: Updating %(id)s (%(name)s - %(frame_name)s) with modtime %(time)s") % {'plural_name': pn, 'id': id, 'name': cache(id).name, 'frame_name': frame.name, 'time': frame.modify_time}) else: c(pn, "info", \ message=_("%(plural_name)s: Updating %(id)s (%(frame_name)s - New!) with modtime %(time)s") % {'plural_name': pn, 'id': id, 'frame_name': frame.name, 'time': frame.modify_time}) cache()[id] = (frame.modify_time, frame) else: if cache().has_key(id): c(pn, "failure", \ message=_("Failed to get the %(singular_name)s which was previously called %(name)s.") % {'singular_name': sn, 'name': cache(id).name}) else: c(pn, "failure", \ message=_("Failed to get the %(singular_name)s with ID %(id)s.") % {'singular_name': sn, 'id': id}) # Don't get any sub-objects for this toget.remove(id) # This object does not really exist on the server ids.remove(id) c(pn, "progress", message=_("Cleaning up %s which have disappeared...") % pn) # Remove any objects which are no longer on the server onserver = set(ids) havelocal = set(cache().keys()) for id in havelocal-onserver: c(pn, "progress", \ message=_("Removing %(singular_name)s %(name)s as it has disappeared...") % {'singular_name': sn, 'name': cache(id).name}) del cache()[id] if pn == "objects": c(pn, "progress", \ message=_("Building two way tree of the universe for speed...")) def build(frame, parent=None, self=self): if parent: frame.parent = parent.id for id in frame.contains: try: build(cache(id), frame) except KeyError: from threads import NetworkThread raise NetworkThread.NetworkFailureEvent("%s (ID %i) references an object with ID %i, which does not exist!" % (frame.name, frame.id, id)) build(cache(0)) c(pn, "finished", message=_("Received all %s...") % pn) return toget def __getSubObjects(self, connection, toget, plural_name, subname, number, callback=None): c = callback pn = plural_name sn = plural_name[:-1] def cache(id=None, self=self, pn=pn): if id==None: return getattr(self, pn) else: return getattr(self, pn)[id] c = callback sb = subname c(sb, "start", message=_("Getting %s..") % sb) c(sb, "todownload", message=_("Have to get %(name)s for %(ammount)i %(plural_name)s..") % {'name': sb, 'ammount': len(toget), 'plural_name': pn}, todownload=len(toget)) # Set the blocking so we can pipeline the requests connection.setblocking(True) empty = [] for id in toget: frame = cache(id) if getattr(frame, number) > 0: c(sb, "progress", \ message=_("Sending a request for all %(name)s on %(frame_name)s..") % {'name': sb, 'frame_name': unicode(frame.name)}) getattr(connection, "get_%s" % sb)(id, range(0, getattr(frame, number))) else: c(sb, "progress", \ message=_("Skipping requesting %(name)s on %(frame_name)s as there are none!") % {'name': sb, 'frame_name': unicode(frame.name)}) empty.append(id) for id in empty: getattr(self, sb)[id] = (cache(id).modify_time, ChangeList()) toget.remove(id) # Wait for the response to the order requests while len(toget) > 0: result = None while result is None: result = connection.poll() id = toget.pop(0) frame = cache(id) if failed(result): c(sb, "failure", \ message=_("Failed to get %(name)s for %(frame_name)s (ID: %(id)s) (%(result)s)...") % {'name': sb, 'frame_name': unicode(frame.name), 'id': frame.id, 'result': result[1]}) result = [] else: c(sb, "downloaded", amount=1, \ message=_("Got %(ammount)i %(name)s for %(frame_name)s (ID: %(id)s)...") % {'ammount': len(result), 'name': sb, 'frame_name': unicode(frame.name), 'id': frame.id}) subs = ChangeList() for sub in result: subs.append(ChangeNode(sub)) getattr(self, sb)[id] = (cache(id).modify_time, subs) c(sb, "progress", message=_("Cleaning up any stray %s..") % sb) for id in getattr(self, sb).keys(): if not cache().has_key(id): c(sb, "progress", message=_("Found stray %(name)s for %(id)s..") % {'name': sb, 'id': id}) del getattr(self, sb)[id] connection.setblocking(False) c(sb, "finished", message=_("Received all the %s..") % sb) def apply(connection, evt, cache): """\ Applies a CacheDirty event to a connection. """ if evt.what == "orders": d = cache.orders[evt.id] if evt.action == "remove": slots = [] for node in evt.nodes: assert isinstance(node, ChangeNode) slots.append(d.slot(node)) slots.sort(reverse=True) if failed(connection.remove_orders(evt.id, slots)): raise IOError("Unable to remove the order...") elif evt.action in ("create after", "create before", "change"): assert len(evt.nodes) == 1, "%s event has multiple slots! (%r) WTF?" % (evt.action, evt.nodes) assert evt.change in d slot = d.slot(evt.change) if evt.action == "change": # Remove the old order if failed(connection.remove_orders(evt.id, slot)): raise IOError("Unable to remove the order...") assert not evt.change.CurrentState == "idle" assert not evt.change.PendingOrder is None if failed(connection.insert_order(evt.id, slot, evt.change.PendingOrder)): raise IOError("Unable to insert the order...") o = connection.get_orders(evt.id, slot)[0] if failed(o): raise IOError("Unable to get the order..." + o[1]) evt.change.UpdatePending(o) else: raise SystemError("Unknown Action") elif evt.what == "messages" and evt.action == "remove": d = cache.messages[evt.id] slots = [] for node in evt.nodes: slots.append(d.slot(node)) slots.sort(reverse=True) if failed(connection.remove_messages(evt.id, slots)): raise IOError("Unable to remove the message...") elif evt.what == "designs": # FIXME: Assuming that these should succeed is BAD! if evt.action == "remove": if failed(connection.remove_designs(evt.change)): raise IOError("Unable to remove the design...") if evt.action == "change": if failed(connection.change_design(evt.change)): raise IOError("Unable to change the design...") if evt.action == "create": result = connection.insert_design(evt.change) if failed(result): raise IOError("Unable to add the design...") # Need to update the event with the new ID of the design. evt.id = result.id elif evt.what == "categories": # FIXME: Assuming that these should succeed is BAD! if evt.action == "remove": if failed(connection.remove_categories(evt.change)): raise IOError("Unable to remove the category...") if evt.action == "change": if failed(connection.change_category(evt.change)): raise IOError("Unable to change the category...") if evt.action == "create": result = connection.insert_category(evt.change) if failed(result): raise IOError("Unable to add the category...") # Need to update the event with the new ID of the design. evt.id = result.id else: raise ValueError("Can't deal with that yet!") cache.commit(evt) return evt libtpclient-py-0.3.2/tp/client/threads.py0000644000175000017500000004634011164415250016532 0ustar timtim import pprint import socket import sys import time import traceback from media import Media from config import load_data, save_data from version import version def nop(*args, **kw): return class Event(Exception): """ Base class for all events which get posted. """ def type(self): return self.__class__.__name__[:-5] type = property(type) def __init__(self, *args, **kw): self.message = "" Exception.__init__(self, *args, **kw) if self.__class__.__name__[-5:] != "Event": raise SystemError("All event class names must end with Event!") self.time = time.time() def __str__(self): return self.__unicode__().encode('ascii', 'replace') def __unicode__(self): return unicode(self.message) from cache import Cache from ChangeList import ChangeNode class Application(object): """ Container for all the applications threads and the network cache. Calling accross threads requires you to use the .Call method on each thread - DO NOT call directly! The cache can be accessed by either thread at any time - be careful. """ MediaClass = None FinderClass = None CacheClass = None def __init__(self): if self.CacheClass is None: from cache import Cache self.CacheClass = Cache try: import signal # Make sure these signals go to me, rather then a child thread.. signal.signal(signal.SIGINT, self.Exit) signal.signal(signal.SIGTERM, self.Exit) except ImportError: pass print self.GUIClass, self.NetworkClass, self.MediaClass, self.FinderClass self.gui = self.GUIClass(self) if not self.MediaClass is None: self.media = self.MediaClass(self) else: self.media = None if not self.FinderClass is None: self.finder = self.FinderClass(self) else: self.finder = None self.cache = None if hasattr(self.GUIClass, "Create"): self.gui.Create() # Load the Configuration self.ConfigLoad() def Run(self): """\ Set the application running. """ self.StartNetwork() if not self.media is None: self.media.start() if not self.finder is None: self.finder.start() self.gui.start() def StartNetwork(self): self.network = self.NetworkClass(self) self.network.start() def ConfigSave(self): """\ """ config = self.gui.ConfigSave() save_data(self.ConfigFile, config) print "Saving the config...\n" + pprint.pformat(config) def ConfigLoad(self): """\ """ config = load_data(self.ConfigFile) if config is None: config = {} self.gui.ConfigLoad(config) def Post(self, event, source=None): """\ Post an application wide event to every thread. """ event.source = source self.network.Post(event) self.finder.Post(event) self.media.Post(event) #print "Post", event, event.source #import traceback #traceback.print_stack() self.gui.Call(self.gui.Post, event) def Exit(self, *args, **kw): """ Exit the program. """ if hasattr(self, "closing"): return self.closing = True self.finder.Cleanup() self.network.Cleanup() self.media.Cleanup() self.gui.Cleanup() import threading from threadcheck import thread_checker, thread_safe class CallThreadStop(Exception): pass ThreadStop = CallThreadStop class CallThread(threading.Thread): """\ A call thread is thread which lets you queue up functions to be called in the thread. Functions are called in the order they are queue and there is no prempting or other fancy stuff. """ __metaclass__ = thread_checker def __init__(self): threading.Thread.__init__(self, name=self.name) self.exit = False self.reset = False self.tocall = [] @thread_safe def run(self): self._thread = threading.currentThread() try: while not self.exit: self.every() if len(self.tocall) <= 0: self.idle() continue method, args, kw = self.tocall.pop(0) try: method(*args, **kw) except CallThreadStop, e: self.Reset() self.reset = False except Exception, e: self.error(e) except Exception, e: self.error(e) self.Cleanup() def every(self): """\ Called every time th run goes around a loop. It is called before functions are poped of the tocall list. This mean it could be used to reorganise the pending requests (or even remove some). By default it does nothing. """ pass def idle(self): """\ Called when there is nothing left to do. Will keep getting called until there is something to be done. The default sleeps for 100ms (should most probably sleep if you don't want to consume 100% of the CPU). """ time.sleep(0.1) def error(self, error): """\ Called when an exception occurs in a function which was called. The default just prints out the traceback to stderr. """ pass @thread_safe def Reset(self): #del self.tocall[:] self.reset = True @thread_safe def Cleanup(self): """\ Ask the thread to try and exit. """ del self.tocall[:] self.exit = True @thread_safe def Call(self, method, *args, **kw): """\ Queue a call to method in on thread. """ self.tocall.append((method, args, kw)) @thread_safe def Post(self, event): func = 'On' + event.type if hasattr(self, func): self.Call(getattr(self, func), event) class NotImportantEvent(Event): """\ Not Important events are things like download progress events. They occur often and if one is missed there is not huge problem. The latest NotImportantEvent is always the most up to date and if there are pending updates only the latest in a group should be used. """ pass from tp.netlib import Connection, failed from tp.netlib import objects as tpobjects class NetworkThread(CallThread): """\ The network thread deals with talking to the server via the network. """ name = "Network" ## These are network events class NetworkFailureEvent(Event): """\ Raised when the network connection fails for what ever reason. """ pass class NetworkFailureUserEvent(NetworkFailureEvent): """\ Raised when there was a network failure because the user does not exist. """ pass class NetworkFailurePasswordEvent(NetworkFailureEvent): """\ Raised when there was a network failure because the password was incorrect. """ pass class NetworkConnectEvent(Event): """\ Raised when the network connects to a server. """ def __init__(self, msg, features, games): Event.__init__(self, msg) self.features = features self.games = games class NetworkAccountEvent(Event): """\ Raised when an account is successful created on a server. """ pass class NetworkAsyncFrameEvent(Event): """\ Raised when an async frame (such as TimeRemaining) is received. """ def __init__(self, frame): Event.__init__(self) self.frame = frame class NetworkTimeRemainingEvent(NetworkAsyncFrameEvent): """\ Called when an async TimeRemaining frame is received. """ def __init__(self, frame): if not isinstance(frame, tpobjects.TimeRemaining): raise SyntaxError("NetworkTimeRemainingEvent requires a TimeRemaining frame!? (got %r)", frame) NetworkThread.NetworkAsyncFrameEvent.__init__(self, frame) self.gotat = time.time() self.remaining = frame.time ###################################### def __init__(self, application): CallThread.__init__(self) self.application = application self.connection = Connection() def every(self): """\ Check's if there are any async frames pending. If so creates the correct events and posts them. """ try: self.connection.pump() pending = self.connection.buffered['frames-async'] while len(pending) > 0: if not isinstance(pending[0], tpobjects.TimeRemaining): break frame = pending.pop(0) self.application.Post(self.NetworkTimeRemainingEvent(frame)) except (AttributeError, KeyError), e: print e def error(self, error): traceback.print_exc() if isinstance(error, (IOError, socket.error)): s = _(u"There was an unknown network error.\n") s += _("Any changes since last save have been lost.\n") if getattr(self.connection, 'debug', False): s += _("A traceback of the error was printed to the console.\n") print error print repr(s) self.application.Post(self.NetworkFailureEvent(s)) else: raise def NewAccount(self, username, password, email): """\ """ result, message = self.connection.account(username, password, email) if result: self.application.Post(self.NetworkAccountEvent(message)) else: self.application.Post(self.NetworkFailureEvent(message)) def Connect(self, host, debug=False, callback=nop, cs="unknown"): """\ """ try: if self.connection.setup(host=host, debug=debug): s = _("The client was unable to connect to the host.\n") s += _("This could be because the server is down or there is a problem with the network.\n") self.application.Post(self.NetworkFailureEvent(s)) return False except (IOError, socket.error), e: s = _("The client could not connect to the host.\n") s += _("This could be because the server is down or you mistyped the server address.\n") self.application.Post(self.NetworkFailureEvent(s)) return False callback("connecting", "downloaded", _("Successfully connected to the host..."), amount=1) try: callback("connecting", "progress", _("Looking for Thousand Parsec Server...")) if failed(self.connection.connect(("libtpclient-py/%s.%s.%s " % version[:3])+cs)): raise socket.error("") except (IOError, socket.error), e: s = _("The client connected to the host but it did not appear to be a Thousand Parsec server.\n") s += _("This could be because the server is down or the connection details are incorrect.\n") self.application.Post(self.NetworkFailureEvent(s)) return False callback("connecting", "downloaded", _("Found a Thousand Parsec Server..."), amount=1) callback("connecting", "progress", _("Looking for supported features...")) features = self.connection.features() if failed(features): s = _("The client connected to the host but it did not appear to be a Thousand Parsec server.\n") s += _("This could be because the server is down or the connection details are incorrect.\n") self.application.Post(self.NetworkFailureEvent(s)) return False callback("connecting", "downloaded", _("Got the supported features..."), amount=1) callback("connecting", "progress", _("Looking for running games...")) games = self.connection.games() if failed(games): games = [] else: for game in games: callback("connecting", "progress", _("Found %(game)s playing %(ruleset)s (%(version)s)") % {'game': game.name, 'ruleset': game.rule, 'version': game.rulever}) callback("connecting", "downloaded", _("Got the supported features..."), amount=1) self.application.Post(self.NetworkConnectEvent("Connected to %s" % host, features, games)) return def ConnectTo(self, host, username, password, debug=False, callback=nop, cs="unknown"): """\ Connect to a given host using a certain username and password. """ callback("connecting", "start", _("Connecting...")) callback("connecting", "todownload", todownload=5) try: if self.Connect(host, debug, callback, cs) is False: return False callback("connecting", "progress", _("Trying to Login to the server...")) if failed(self.connection.login(username, password)): s = _("The client connected to the host but could not login because the username of password was incorrect.\n") s += _("This could be because you are connecting to the wrong server or mistyped the username or password.\n") self.application.Post(self.NetworkFailureUserEvent(s)) return False callback("connecting", "downloaded", _("Logged in okay!"), amount=1) # Create a new cache self.application.cache = self.application.CacheClass(self.application.CacheClass.key(host, username)) return True finally: callback("connecting", "finished", "") def CacheUpdate(self, callback): try: callback("connecting", "alreadydone", "Already connected to the server!") self.application.cache.update(self.connection, callback) self.application.cache.save() except ThreadStop, e: pass except Exception, e: self.application.Post(self.NetworkFailureEvent(e)) raise def RequestEOT(self, callback=None): if callback is None: def callback(self, *args, **kw): pass try: if not hasattr(self.connection, "turnfinished"): print "Was unable to request turnfinished." return if failed(self.connection.turnfinished()): print "The request for end of turn failed." return except Exception, e: print e def OnCacheDirty(self, evt): """\ When the cache gets dirty we have to push the changes to the server. """ try: from cache import apply self.application.Post(apply(self.connection, evt, self.application.cache)) except Exception, e: type, val, tb = sys.exc_info() sys.stderr.write("".join(traceback.format_exception(type, val, tb))) self.application.Post(self.NetworkFailureEvent(e)) "There where the following errors when trying to send changes to the server:" "The following updates could not be made:" class MediaThread(CallThread): """\ The media thread deals with downloading media off the internet. """ name = "Media" ## These are network events class MediaFailureEvent(Event): """\ Raised when the media connection fails for what ever reason. """ pass class MediaDownloadEvent(Event): """ Base class for media download events. """ def __init__(self, file, progress=0, size=0, localfile=None, amount=0): Event.__init__(self) self.file = file self.amount = amount self.progress = progress self.size = size self.localfile = localfile def __str__(self): return "<%s %s>" % (self.__class__.__name__, self.file) __repr__ = __str__ class MediaDownloadStartEvent(MediaDownloadEvent): """\ Posted when a piece of media is started being downloaded. """ pass class MediaDownloadProgressEvent(MediaDownloadEvent, NotImportantEvent): """\ Posted when a piece of media is being downloaded. """ pass class MediaDownloadDoneEvent(MediaDownloadEvent): """\ Posted when a piece of media has been downloaded. """ pass class MediaDownloadAbortEvent(MediaDownloadEvent): """\ Posted when a piece of media started downloading but was canceled. """ def __str__(self): return "<%s>" % (self.__class__.__name__) class MediaUpdateEvent(Event): """\ Posted when the media was download. """ def __init__(self, files): Event.__init__(self) self.files = files ###################################### def __init__(self, application): CallThread.__init__(self) self.application = application self.todownload = {} self.tostop = [] def idle(self): if len(self.todownload) <= 0: CallThread.idle(self) return file, timestamp = self.todownload.iteritems().next() def callback(blocknum, blocksize, size, self=self, file=file, tostop=self.tostop): progress = min(blocknum*blocksize, size) if blocknum == 0: self.application.Post(self.MediaDownloadStartEvent(file, progress, size)) self.application.Post(self.MediaDownloadProgressEvent(file, progress, size, amount=blocksize)) if file in tostop: tostop.remove(file) raise self.MediaDownloadAbortEvent(file) try: localfile = self.cache.get(file, callback=callback) self.application.Post(self.MediaDownloadDoneEvent(file, localfile=localfile)) except self.MediaDownloadAbortEvent, e: self.application.Post(e) del self.todownload[file] def error(self, error): if isinstance(error, (IOError, socket.error)): s = _("There was an unknown network error.\n") s += _("Any changes since last save have been lost.\n") self.application.Post(self.MediaFailureEvent(s)) raise @thread_safe def Cleanup(self): for file in self.todownload: self.tostop.append(file) CallThread.Cleanup(self) @thread_safe def Post(self, event): """ Post an Event the current thread. """ pass @thread_safe def StopFile(self, file): self.tostop.append(file) @thread_safe def GetFile(self, file): """\ Get a File, return directly or start a download. """ location = self.cache.newest(file) if location: return location self.todownload[file] = None def ConnectTo(self, host, username, debug=False): """\ ConnectTo """ self.cache = Media("http://svn.thousandparsec.net/svn/media/client/") # FIXME: Hack to prevent cross thread calling - should fix the media object files = [] for file in self.cache.getpossible(['png', 'gif']): files.append(file) self.application.Post(self.MediaUpdateEvent(files)) from tp.netlib.discover import LocalBrowser as LocalBrowserB from tp.netlib.discover import RemoteBrowser as RemoteBrowserB class LocalBrowser(LocalBrowserB, threading.Thread): name="LocalBrowser" def __init__(self, *args, **kw): threading.Thread.__init__(self, name=self.name) LocalBrowserB.__init__(self, *args, **kw) class RemoteBrowser(RemoteBrowserB, threading.Thread): name="RemoteBrowser" def __init__(self, *args, **kw): threading.Thread.__init__(self, name=self.name) RemoteBrowserB.__init__(self, *args, **kw) class FinderThread(CallThread): """\ The finder thread deals with finding games. It uses both Zeroconf and talks to the metaserver to get the information it needs. """ name="Finder" ## These are network events class GameEvent(Event): """ Base class for all game found/lost events. """ def __init__(self, game): Event.__init__(self) self.game = game class LostGameEvent(GameEvent): """\ Raised when the finder loses a game. """ pass class FoundGameEvent(GameEvent): """\ Raised when the finder finds a game. """ pass class LostLocalGameEvent(FoundGameEvent): """\ Raised when the finder loses a local game. """ pass class FoundLocalGameEvent(FoundGameEvent): """\ Raised when the finder finds a local game. """ pass class LostRemoteGameEvent(FoundGameEvent): """\ Raised when the finder loses a remote game. """ pass class FoundRemoteGameEvent(FoundGameEvent): """\ Raised when the finder finds a remote game. """ pass class FinderErrorEvent(Event): """\ Raised when the finder has an error finding games. """ pass class FinderFinishedEvent(Event): """\ Raised when the finder has finished searching for new games. """ pass def __init__(self, application): CallThread.__init__(self) self.application = application self.local = LocalBrowser() self.local.GameFound = self.FoundLocalGame self.local.GameGone = self.LostLocalGame self.remote = RemoteBrowser() self.remote.GameFound = self.FoundRemoteGame self.remote.GameGone = self.LostRemoteGame @thread_safe def FoundLocalGame(self, game): self.application.Post(FinderThread.FoundLocalGameEvent(game)) @thread_safe def FoundRemoteGame(self, game): self.application.Post(FinderThread.FoundRemoteGameEvent(game)) @thread_safe def LostLocalGame(self, game): self.application.Post(FinderThread.LostLocalGameEvent(game)) @thread_safe def LostRemoteGame(self, game): self.application.Post(FinderThread.LostRemoteGameEvent(game)) @thread_safe def Games(self): """\ Get all the currently known games. """ return self.local.games, self.remote.games @thread_safe def Cleanup(self): self.local.exit() self.remote.exit() @thread_safe def Post(self, event): """ Post an Event the current thread. """ pass @thread_safe def run(self): self._thread = threading.currentThread() self.local.start() self.remote.start() libtpclient-py-0.3.2/tp/client/version.py0000644000175000017500000000342311164415270016562 0ustar timtim version = (0, 3, 2) # Add the git version if in a git tree... import os, os.path __path__ = os.path.realpath(os.path.dirname(__file__)) installpath = os.path.split(os.path.split(__path__)[0])[0] # Get the git version this tree is based on if os.path.exists(os.path.join(installpath, '.git')): # Read in git's 'HEAD' file which points to the correct reff to look at h = open(os.path.join(installpath, '.git', 'HEAD')).readline().strip() try: # Read in the ref ref = h.split(': ', 1)[1] # This file has the SHA1 p = open(os.path.join(installpath, '.git', ref)) del ref version_git = p.read().strip() except IndexError: version_git = h # What version are we trying to get too? import time if version[2] >= 99: version_target = (version[0], version[1]+1, 0) else: version_target = (version[0], version[1], version[2]+1) version_target_str = "%i.%i.%i" % version_target version_str = "%i.%i.%i" % version[:3] if __name__ == "__main__": import sys if len(sys.argv) > 1 and sys.argv[1] == '--fix': print """ import os, os.path __path__ = os.path.realpath(os.path.dirname(__file__)) installpath = os.path.split(os.path.split(__path__)[0])[0] """ for value in dir(): if value.startswith('__') or value in ('installpath',): continue if isinstance(eval(value), (tuple, str, unicode)): exec("print '%s =', repr(%s)" % (value, value)) print """ try: print version_str+'+'+version_target_str, "(git %s)" % version_git, "(installed at %s)" % installpath except ValueError: print version_str, "(installed at %s)" % installpath """ sys.exit(0) if os.path.exists(os.path.join(installpath, '.git')): print version_str+'+'+version_target_str, "(git %s)" % version_git, "(installed at %s)" % installpath else: print version_str, "(installed at %s)" % installpath libtpclient-py-0.3.2/tp/client/decorator.py0000644000175000017500000001276411164415250017065 0ustar timtim## The basic trick is to generate the source code for the decorated function ## with the right signature and to evaluate it. ## Uncomment the statement 'print >> sys.stderr, func_src' in _decorate ## to understand what is going on. __all__ = ["decorator", "update_wrapper", "getinfo"] import inspect, sys def getinfo(func): """ Returns an info dictionary containing: - name (the name of the function : str) - argnames (the names of the arguments : list) - defaults (the values of the default arguments : tuple) - signature (the signature : str) - doc (the docstring : str) - module (the module name : str) - dict (the function __dict__ : str) >>> def f(self, x=1, y=2, *args, **kw): pass >>> info = getinfo(f) >>> info["name"] 'f' >>> info["argnames"] ['self', 'x', 'y', 'args', 'kw'] >>> info["defaults"] (1, 2) >>> info["signature"] 'self, x, y, *args, **kw' """ assert inspect.ismethod(func) or inspect.isfunction(func) regargs, varargs, varkwargs, defaults = inspect.getargspec(func) argnames = list(regargs) if varargs: argnames.append(varargs) if varkwargs: argnames.append(varkwargs) signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, formatvalue=lambda value: "")[1:-1] return dict(name=func.__name__, argnames=argnames, signature=signature, defaults = func.func_defaults, doc=func.__doc__, module=func.__module__, dict=func.__dict__, globals=func.func_globals, closure=func.func_closure) def update_wrapper(wrapper, wrapped, create=False): """ An improvement over functools.update_wrapper. By default it works the same, but if the 'create' flag is set, generates a copy of the wrapper with the right signature and update the copy, not the original. Moreovoer, 'wrapped' can be a dictionary with keys 'name', 'doc', 'module', 'dict', 'defaults'. """ if isinstance(wrapped, dict): infodict = wrapped else: # assume wrapped is a function infodict = getinfo(wrapped) assert not '_wrapper_' in infodict["argnames"], \ '"_wrapper_" is a reserved argument name!' if create: # create a brand new wrapper with the right signature src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict # import sys; print >> sys.stderr, src # for debugging purposes wrapper = eval(src, dict(_wrapper_=wrapper)) try: wrapper.__name__ = infodict['name'] except: # Python version < 2.4 pass wrapper.__doc__ = infodict['doc'] wrapper.__module__ = infodict['module'] wrapper.__dict__.update(infodict['dict']) wrapper.func_defaults = infodict['defaults'] return wrapper # the real meat is here def _decorator(caller, func): infodict = getinfo(func) argnames = infodict['argnames'] assert not ('_call_' in argnames or '_func_' in argnames), \ 'You cannot use _call_ or _func_ as argument names!' src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict dec_func = eval(src, dict(_func_=func, _call_=caller)) return update_wrapper(dec_func, func) def decorator(caller, func=None): """ General purpose decorator factory: takes a caller function as input and returns a decorator with the same attributes. A caller function is any function like this:: def caller(func, *args, **kw): # do something return func(*args, **kw) Here is an example of usage: >>> @decorator ... def chatty(f, *args, **kw): ... print "Calling %r" % f.__name__ ... return f(*args, **kw) >>> chatty.__name__ 'chatty' >>> @chatty ... def f(): pass ... >>> f() Calling 'f' For sake of convenience, the decorator factory can also be called with two arguments. In this casem ``decorator(caller, func)`` is just a shortcut for ``decorator(caller)(func)``. """ if func is None: # return a decorator function return update_wrapper(lambda f : _decorator(caller, f), caller) else: # return a decorated function return _decorator(caller, func) if __name__ == "__main__": import doctest; doctest.testmod() ####################### LEGALESE ################################## ## Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## Redistributions in bytecode form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in ## the documentation and/or other materials provided with the ## distribution. ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, ## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, ## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS ## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR ## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE ## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH ## DAMAGE. libtpclient-py-0.3.2/tp/client/ChangeDict.py0000644000175000017500000000160511164415250017064 0ustar timtimfrom types import TupleType class ChangeDict(dict): """\ A simple dictionary which also stores the "times" an object was last updated. Used so that we only upload/download items which have changed. """ def __init__(self): dict.__init__(self) self.times = {} def __setitem__(self, key, value): """\ This set item is special, it only takes keys of the form, (, ) """ if type(value) is TupleType and len(value) == 2: time, value = value else: time = -1 if time != -1 and self.times.has_key(key) and self.times[key] > time: raise ValueError("The object isn't new enough to update the dictionary with! Current %s, update %s" % (self.times[key], time)) self.times[key] = time dict.__setitem__(self, key, value) def __delitem__(self, key): del self.times[key] dict.__delitem__(self, key) ChangeDict.__repr__ = dict.__repr__ libtpclient-py-0.3.2/tp/client/threadcheck.py0000644000175000017500000000400511164415250017335 0ustar timtimimport threading from decorator import decorator @decorator def thread_check_callable(func, self, *args, **kw): assert self._thread is threading.currentThread(), "%s can only be called by %s not %s" % (func, self._thread, threading.currentThread()) return func(self, *args, **kw) @decorator def thread_check_init(func, self, *args, **kw): self._thread = threading.currentThread() return func(self, *args, **kw) def thread_safe(func): """ Mark this function as thread safe so can be called accross thread boundaries. """ func.__threadsafe = True return func # FIXME: This doesn't also check the classes base class! import inspect class thread_checker(type): """ This class can be set as the metaclass for an object to make sure that it is only access from a single thread. Methods can be marked "thread_safe" with the thread_safe decorator also found in this module. """ def __new__(cls,classname,bases,classdict): for attr, item in classdict.items(): if attr == "__init__": classdict[attr] = thread_check_init(item) elif inspect.isfunction(item) and not hasattr(item, "__threadsafe"): classdict[attr] = thread_check_callable(item) t = type.__new__(cls,classname,bases,classdict) return t __all__ = ["thread_checker", "thread_safe"] if __name__ == "__main__": class B(object): __metaclass__ = thread_checker def __init__(self): pass test_simple = 2 test_complex = [1, 3] def test_unsafe(self): return "test_unsafe worked" @thread_safe def test_safe(self): return "test_safe worked" class t1(threading.Thread): def __init__(self, ishouldnotaccess): self.i = ishouldnotaccess threading.Thread.__init__(self) def run(self): print self.i.test_simple print self.i.test_complex print self.i.test_safe() print self.i.test_unsafe() ot = t1(B()) print ot.i.test_safe print inspect.getargspec(ot.i.test_safe) print ot.i.test_unsafe print inspect.getargspec(ot.i.test_unsafe) ot.run() # this should fail ot.start() import time time.sleep(10) libtpclient-py-0.3.2/tp/client/ChangeList.py0000644000175000017500000001534611164415250017123 0ustar timtim#! /bin/python """ This file includes an implimentation of a datastructure that can handle all the problems associated with the evil slot interface we use in Thousand Parsec. """ class ChangeNode(object): NodeCounter = 0 states = ["creating", "idle", "updating", "removing", "removed"] def __init__(self, what): # FIXME: Must be a better way to do this... ChangeNode.NodeCounter += 1 self.id = ChangeNode.NodeCounter self.left = None self.right = None self._what = what self._pending = [] def AddState(self, state, pending=None): if self.LastState in ("removing", "removed"): raise SystemError("Can not add new states to the node if it is being removed!") self._pending.append([state, pending]) def UpdatePending(self, pending): assert len(self._pending) > 0 self._pending[0] = (self._pending[0][0], pending) @property def LastState(self): if len(self._pending) == 0: return "idle" else: return self._pending[-1][0] @property def CurrentState(self): if len(self._pending) == 0: return "idle" else: return self._pending[0][0] @property def ServerOrder(self): """\ The value as it currently exists on the server. """ return self._what @property def PendingOrder(self): """\ Returns the first pending change. """ if len(self._pending) == 0: return self._what else: return self._pending[0][1] @property def CurrentOrder(self): """\ Returns the 'latest' order """ order = None for state, order in reversed(self._pending): if not order is None: break if not order is None: return order return self._what def PopState(self): assert len(self._pending) > 0 state, pending = self._pending.pop(0) if state == "updating" or state == "creating": assert not pending is None self._what = pending def inlist(self): return \ (self.left is None or self.left.right == self) \ and \ (self.right is None or self.right.left == self) def __repr__(self): if self.left is None: l = "None" else: l = hex(self.left.id) if self.right is None: r = "None" else: r = hex(self.right.id) return ">" % (self.id, self._what, l, r) __str__ = __repr__ def __eq__(self, other): try: return self._what is other._what except AttributeError: return False def __neq__(self, other): return not self.__eq__(other) @property def pending(self): assert False @property def what(self): assert False class ChangeHead(ChangeNode): def __init__(self): ChangeNode.__init__(self, None) self.__dict__['CurrentState'] = "head" self.__dict__['LastState'] = "head" def __repr__(self): return ChangeNode.__repr__(self).replace("Node", "Head") __str__ = __repr__ def SetState(self, *args, **kw): raise SystemError("Should not be calling this on a head node!") class ChangeList(object): def __init__(self): self.head = ChangeHead() def __getitem__(self, id): if isinstance(id, slice): return self.__iter__(id) node = self.head.right while node != None: if node.id == id: return node node = node.right raise KeyError("No node exists with id %s" % id) def __delitem__(self, id): node = self[id] node.left.right = node.right if not node.right is None: node.right.left = node.left def __contains__(self, tofind): if tofind is self.head: return True try: self[tofind.id] return True except KeyError: return False def __len__(self): l = 0 node = self.head.right while node != None: l += 1 node = node.right return l def __iter__(self, sliceme=None): # FIXME: Should be better way to do this if sliceme is None: sliceme = slice(0, len(self), 1) start = sliceme.start stop = sliceme.stop step = sliceme.step if step is None: step = 1 if start is None: start = 0 if stop is None: stop = len(self) if start < 0: start += len(self) if stop < 0: stop += len(self) i = 0 node = self.head.right while node != None: if i >= start and i < stop and i % step == 0: yield node node = node.right i += 1 raise StopIteration def index(self, needle): if needle is self.head: return -1 for i, node in enumerate(self): if node is needle: return i raise IndexError("%s was not found in the list!" % needle) def slot(self, needle): if needle is self.head: return -1 i = 0 for node in self: if node is needle: return i if node.CurrentState != "creating": i += 1 raise IndexError("%s was not found in the list!" % needle) def append(self, toappend): node = self.head while node.right != None: node = node.right self.insert_after(node, toappend) def find(self, what): node = self.head while node.right != None: if node._what is what: return node node = node.right @property def first(self): if self.head.right is None: return self.head return self.head.right @property def last(self): node = self.head while node.right != None: node = node.right assert not node is None return node def insert_after(self, node, ins): before = node after = node.right if not node.inlist(): before = node.right.left before.right = ins ins.left = before if not after is None: after.left = ins ins.right = after def insert_before(self, node, ins): before = node.left after = node if not node.inlist(): after = node.left.right after.left = ins ins.right = after if not before is None: before.right = ins ins.left = before if __name__ == "__main__": l = ChangeList() n1 = ChangeNode(1) n2 = ChangeNode(2) nt = ChangeNode('t') l.insert_after(l.head, n1) print '--', l.head for i, node in enumerate(l): print '->', i, node print l.insert_after(n1, n2) print '--', l.head for i, node in enumerate(l): print '->', i, node print l.insert_before(n2, nt) print '--', l.head for i, node in enumerate(l): print '->', i, node print del l[nt.id] print 't', nt print print '--', l.head for i, node in enumerate(l): print '->', i, node print l.insert_after( n1, ChangeNode('>')) l.insert_before(n2, ChangeNode('<')) print '--', l.head for i, node in enumerate(l): print '->', i, node print l.insert_after( nt, ChangeNode("a")) l.insert_before(nt, ChangeNode("b")) print 't', nt print print '--', l.head for i, node in enumerate(l): print i, node print for i in l[1:-1]: print i print print 0, l.index(n1), l.first print -1, l.index(n2), l.last print print '--', l.head for i, node in enumerate(l): print i, node print del l[l.last.id] print '--', l.head for i, node in enumerate(l): print i, node print nodes = [] for node in l: nodes.append(node) for node in nodes: del l[node.id] print '--', l.head for i, node in enumerate(l): print i, node print