### msg.py - Message System for POO and other projects ### 6 Mar 1997 - Amit Patel ### 25 Mar 1997 - added changes required to run in restricted exec mode ### 26 Mar 1997 - changed genders from objects to dictionaries ### 2 Aug 1997 - Joe Strout: added handling of "n't" on verb end ###################################################################### # Module Documentation # The module is divided into Utilities, Gender Objects, and Messages # Utilities are used internally but might also be a useful library # Gender Objects are used internally but can be extended or replaced # by custom gender objects # The Message section handles processing of a message (with %-codes) # into a string ###################################################################### # Message System documentation: # # The message system is a way to combine a _message_, which is a # specification of information you want, with a list of objects # and/or strings that provide the information. If you are familiar # with C's printf or Python's '%' operator, the idea is similar here: # a format string (message) indicates what information to display and # how to display it, and the list of objects (or values) provides the # information to display. Like C and Python formatting, this system # may seem complex at first. Looking at some examples (at the end # of this documentation) may help. # # An message is a string with special codes prefixed by '%'. # Following the '%' is either another '%', in which case a single '%' # is used, or an object selector followed by an information selector. # # An object selector is a digit 0-9 or letter a-z/A-Z optionally followed # by a sequence of l, L, or c modifiers. The digit represents which # object is being selected. The modifiers extract other objects from the # previously selected object. # l - the object's location # L - the object's outermost location (usually a $place) # c - the contents of the object # Examples of object selectors are: # 3 - object 3 # al - the location of the object marked 'a' # ALc - the contents of the outermost location of object 'A' # (Note: object selectors are case sensitive) # Some of this will be more clear with the examples presented later. # # An information selector is one of the following: # (the following are 'relative' selectors) # s - subject pronoun (he,she,it,they) # o - object pronoun (him,her,it,them) # r - reflexive pronoun (himself,herself,itself,theirselves) # p - possessive adjective (his,her,its,their) # q - possessive pronoun (his,hers,its,theirs) # n - name of object (apple,banana,carrots) # i - name plus indirect article (an apple,a banana,some carrots) # d - name plus direct article (the apple,the banana,the carrots) # 'n - name as possessive (apple's,banana's,carrots') # 'i - name+indir. as poss. (an apple's,a banana's,some carrots') # 'd - name+dir. as poss. (the apple's,the banana's,the carrots') # :(verbs) - conjugates singular 'verbs' for the object (verbs,verb) # (the following are 'absolute' selectors) # # - object number (#313) # .(prop) - finds the 'prop' property on the object # '(noun) - changes singular 'noun' to match plural flag (noun,nouns) # x{a1 a2..} - special extension specific to each object # In addition, all of the codes are case sensitive. If any code except # 'x' is capitalized, the resulting information will be capitalized. # The difference between an absolute and a relative selector is that # relative selectors depend on your point of view. If you are the # object being referred to, you may see something different for a # relative selector than a third party. (For example 's' would turn # into 'you' or 'him' depending on who you are.) # Examples of information selectors are: # s - the subject pronoun for the object # :(runs) - the verb 'runs' conjugated appropriately for the object # N - the capitalized name of the object # .(name) - the 'name' property on the object (usually equivalent to n) # # The message system also requires a set of object specifiers which provide # information. An object specifier (objspec) can be one of: # obj - a single POO or Python object # str - a string (noun) # (count,obj) or (count,str) - count indicates the number of objects # [objspec,...] - a list of object specifiers # Examples of object specifiers are: # Joe - a single object, Joe # (3,'duck') - 3 ducks # [(1,'apple'),'banana',(3,'carrot')] - an apple, a banana, and 3 carrots # # Here are some examples of putting object selectors, information specifiers, # and object specifiers together: # '%1N %1:(looks) at %1r.' with {1:Joe} # => 'Joe looks at himself.' # '%AN %A:(looks) at %Ar.' with {'A':[Joe,Guido]} # => 'Joe and Guido look at themselves.' # '%1I spontaneously %1:(combusts)!' with {1:Joe} # => 'Joe spontaneously combusts!' # '%1I spontaneously %1:(combusts)!' with {1:[(3,'carrot'),'apple']} # => '3 carrots and an apple spontaneously combust!' ###################################################################### # Imports # I know, I know, this is supposed to be bad for your health from string import * # End of imports ###################################################################### # Utilities # The following utility functions and values are adapted from JHCore class English: def to_list(self,strings,andstr=' and ',sep=', ', finalsep=',',nothing='nothing'): if len(strings) == 0: return nothing if len(strings) == 1: return strings[0] if len(strings) == 2: return strings[0]+andstr+strings[1] return join( strings[:-1], sep ) + finalsep + andstr + strings[-1] vowel_list = ('a','e','i','o','u') vowel_exceptions = ("usu", "uke", "uvu", "use", "UPI", "unit", "univ", "unic", "uniq", "unix", "eur", "uu", "ubiq", "union", "one", "once", "uti") nonvowel_exceptions = ("honor", "honest", "heir") verb_exceptions = {"has": "have", "is": "are", "was": "were", "can": "can"} noun_exceptions = {"child": "children", "deer": "deer", "moose": "moose", "sheep": "sheep", "goose": "geese", "louse": "lice", "ox": "oxen", "mouse": "mice"} def add_s(self,s): if len(s) < 2: return s+'s' if s[-1] == 'y' and find('aeiou',s[-2]) == -1: return s+'ies' if s[-1] == 'o' and find('aeiouy',s[-2]) == -1: return s+'es' if s[-1] in ('s','x'): return s+'es' if s[-2:] in ('ch','sh'): return s+'es' return s+'s' def remove_s(self,s): if len(s) <= 3 or s[-1] != 's': return s if s[-2] != 'e': return s[:-1] if s[-3] == 'h' and s[-4] in ('c','s'): return s[:-2] if s[-3] in ('o','x'): return s[:-2] if s[-3] == 's' and find('aeiouy',s[-4]) == -1: return s[:-2] if s[-3] == 'i': return s[:-3]+'y' return s[:-1] english = English() # End of utility section ###################################################################### # Genders # A gender value contains the necessary information for msg substitions. # s,o,r,p,q are pronouns and plural is a flag indicating whether this # gender is plural. Note that gender values don't necessarily # represent real genders, but could indicate special genders like # 2nd person or 1st person. # A gender value is a dictionary with mappings for s,o,r,p,q,plural. # # builtin_genders maps 'm' to male, 'f' to female, 'n' to neuter, # 'e' to either, 't' to plural(they), and 'y' to you. # This function sets up the standard genders def _setup_genders(): def Gender(s,o,r,p,q,plural): return {'s':s,'o':o,'r':r,'p':p,'q':q,'plural':plural} # These are the common genders male = Gender('he','him','himself','his','his',0) female = Gender('she','her','herself','her','hers',0) neuter = Gender('it','it','itself','its','its',0) either = Gender('s/he','him/her','himself/herself','his/her','his/hers',0) # These are special genders plural = Gender('they','them','themselves','their','theirs',1) you = Gender('you','you','yourself','your','yours',1) return {'m':male,'f':female,'n':neuter,'e':either,'t':plural,'y':you} # Standard genders are in a dictionary mapping one-char strings to genders builtin_genders = _setup_genders() # GenderedObject gives us objects that have a gender class GenderedObject: def __init__(self,name,gender,articles): self.name = name self.gender = gender self.articles = articles # You is a special object You = GenderedObject('you','y',('','')) # End of gender section ###################################################################### # Message Substitution # The Msg class handles parsing and substitution of messages # The internal codes, after parsing, are tuples: # {s,o,r,p,q},cap - subject,object,reflexive pronouns, and # possessive adjective,possessive pronoun # for the object; cap is 1 if it should be # capitalized # {n,d,i},cap - name, direct article+name, indirect # article+name; cap is 1 if it should be capped # '{n,d,i},cap - object's name (+article if d or i) as possessive # # - the object's number # :,verbs - verbs (singular) conjugated for the object # .,prop - the prop property of the object # ',noun - noun is singular or plural depending on obj's gender # l,code - use the location of the object instead of the object # L,code - use the outermost location of the object instead # c,code - use the contents of the object instead of the object # x,text - special class Msg: # First the parsing functions def parse_part(self,text): # Returns (one-letter-code,data,...), remaining-text if not text: raise 'Parse' code,text = text[:1],text[1:] if find('sonidprq',lower(code)) >= 0: cap = (lower(code)!=code) return (lower(code),cap),text if code=='#': return (code,None),text if find(":.'",code) >= 0 and text[:1] == '(': i = find(text,')') if i < 0: raise 'Parse','could not find )' return (code,text[1:i]),text[i+1:] if find("lLc",code) >= 0: result,text = self.parse_part(text) return (code,result),text if code == "'" and find('nid',lower(text[:1])) >= 0: cap = (lower(text[:1]) != text[:1]) return (code+lower(text[:1]),cap),text[1:] if code == 'x' and text[:1] == '{': i = find(text,'}') if i < 0: raise 'Parse','could not find }' return (code,split(text[1:i])),text[i+1:] return None,code+text def parse_selector(self,text): # Return [partial parsed list], remaining-text t,rest = text[:1],text[1:] if (t >= '0' and t <= '9' or t >= 'a' and t <= 'z' or t >= 'A' and t <= 'Z'): try: which = atoi(t) except: which = t result,text = self.parse_part(rest) if result==None: return ['%'],t+text else: return [(which,result)], text if t == '%': return ['%'], rest else: return ['%'], text # A parsed list is a list of strings or tuples, # where tuples are parsed %codes def parse(self,text): if type(text) == type([]): return text # already parsed i = find(text,'%') result = [] while i >= 0: result.append(text[:i]) r,text = self.parse_selector(text[i+1:]) for x in r: result.append(x) i = find(text,'%') if text: result.append(text) return result ###################################################################### # The substitution section def add_default_article(self,name,code,plural): if code=='d': return 'the '+name if code=='i': if plural: return 'some '+name # Find the first alphanumeric okay = letters+digits i = 0 while i < len(name) and ( find(okay,name[i]) == -1 ): i = i+1 # Now use that first useful char to determine a or an use_an = ( name[i:i+1] in english.vowel_list or name[i:i+1] == '8' ) for v in english.vowel_exceptions: if name[i:i+len(v)] == v: use_an = 0 for v in english.nonvowel_exceptions: if name[i:i+len(v)] == v: use_an = 1 if use_an: return 'an '+name else: return 'a '+name return name def canonical_objspec(self,objspec): # Returns a list of tuples rather than a more free-form objspec # The remainder of the functions expect a canonical objspec rather # than the free-form one. if type(objspec) == type( () ): return [objspec] if type(objspec) == type([]): r = [] for x in objspec: if type(x) != type( () ): r = r + self.canonical_objspec(x) else: r.append(x) return r return [ (None,objspec) ] def get_gender(self,gender,prop): if type(gender) == type(''): try: gender = builtin_genders[gender] except: gender = builtin_genders['e'] try: return gender[prop] except: return builtin_genders['e'][prop] def is_plural(self,objspec): # Returns 1 if objspec represents a plural object or set of objects if type(objspec) == type([]): if len(objspec) >= 2: return 1 if len(objspec) == 1: return self.is_plural(objspec[0]) else: return 0 if type(objspec) == type( () ): return ( objspec[0] >= 2 ) or self.is_plural(objspec[1]) if type(objspec) == type( '' ): return 0 else: return self.get_gender(objspec.gender,'plural') def get_name(self,objspec,code): # code is one of {n,i,d,'n,'i,'d} # objspec should be a canonical object specification list results = [] possessive,code = (code[0]=="'"), code[-1] for count,obj in objspec: # Determine the name and article type of the object articles = 'normal' if type(obj) == type(''): name = self.get_noun(obj,(count != None and count != 1)) else: try: name = obj.name except: name = '#<no-name>' try: articles = obj.articles except: pass # Handle counts if count != None: name = `count` + ' ' + name if articles == 'normal': articles = ('','the') # Determine special treatment for article types c = code if articles == 'unique' and c == 'i': c = 'd' if articles == 'proper': c = 'n' if type(articles) == type( () ): # Custom article type a = '' if c == 'i': a = articles[0] elif c == 'd': a = articles[1] if a: name = a + ' ' + name else: # Other article type name = self.add_default_article(name,c,self.is_plural(obj)) # Handle possessives if possessive: if self.is_plural( (count,obj ) ): name = name+"'" else: name = name+"'s" # Add the name to the list results.append(name) return english.to_list(results) def get_pronoun(self,objspec,code): # code in {s,o,r,p,q} if objspec == []: return builtin_genders['n'][code] # Collapse if type(objspec) == type([]): if len(objspec) > 1: for count,x in objspec: if x == You: return builtin_genders['y'][code] return builtin_genders['t'][code] else: objspec = objspec[0] if type(objspec) == type( () ): if objspec[0] != None and objspec[0] > 1: if not self.is_plural(objspec): return builtin_genders['t'][code] objspec = objspec[1] if type(objspec) == type(''): return builtin_genders['n'][code] else: return self.get_gender(objspec.gender,code) def get_objnum(self,objspec): # This will have to be changed for POO results = [] for count,x in objspec: if type(x) == type(''): results.append( '#<string '+`x`+'>' ) else: results.append( '#'+`id(objspec)` ) return english.to_list(results) def get_property(self,objspec,property): # Get a property from the objects results = [] for count,x in objspec: try: s = getattr(x,property) if type(s) != type(''): s = `s` except AttributeError: s = '#<not-found>' except: s = '#<error>' results.append(s) return english.to_list(results) def get_verb(self,verbname,plural): # Handle exceptions first i = find(verbname,'/') if i >= 0: if plural: return verbname[i+1:] else: return verbname[:i] # Conjugate a verb if plural: if verbname[-3:] == "n't": return self.get_verb(verbname[:-3],plural) + "n't" if verbname in english.verb_exceptions.keys(): return english.verb_exceptions[verbname] elif verbname[-2:] == "'s": # Dunno why this rule is on JHM return verbname[:-2] + "'ve" else: return english.remove_s(verbname) return verbname def get_noun(self,nounname,plural): # Handle exceptions first i = find(nounname,'/') if i >= 0: if plural: return nounname[i+1:] else: return nounname[:i] # Decline a noun if plural: if nounname in english.noun_exceptions.keys(): return noun_exceptions[nounname] else: return english.add_s(nounname) return nounname def get_special(self,objspec,args): # Call a function on all the objects if not args: return '#<invalid x{} specifier>' results = [] v,args = 'sub_'+args[0],tuple(args[1:]) for count,x in objspec: try: s = getattr(x,v) s = apply(s,args) except AttributeError: s = '#<not-found>' except: s = '#<error>' results.append(s) return english.to_list(results) def get_part(self,objspec,part,absolute_only=0): # A message is a list of (string or parsed part) # This function turns a part into a string, or # if absolute_only is set, it might return None to indicate # that the part is relative and not absolute c,options = part # First, try the absolute selectors if c == '.': return self.get_property(objspec,options) if c == '#': return self.get_objnum(objspec) if c == "'": return self.get_noun(options,self.is_plural(objspec)) if c == 'x': return self.get_special(objspec,options) if c in ('l','L','c'): results = [] for count,obj in objspec: if type(obj) != type(''): try: if c == 'l': add = [obj.location] if c == 'L': add = [obj.outer_location()] if c == 'c': add = obj.contents except AttributeError: add = [] except: return '#<error>' for a in add: if a not in results: results.append(a) return self.get_part(self.canonical_objspec(results), options,absolute_only) # Now try the relative selectors if absolute_only: return None if c in ('n','i','d',"'n","'i","'d"): name = self.get_name(objspec,c) if options: name = capitalize(name) return name if c in ('s','o','r','p','q'): name = self.get_pronoun(objspec,c) if options: name = capitalize(name) return name if c == ':': return self.get_verb(options,self.is_plural(objspec)) return '#<unknown code:'+`c`+'>' def lookup_in_objlist(self,objlist,key): try: obj = objlist[key] except: try: obj = objlist[lower(key)] except: obj = [] return obj def canonical_objlist(self,objlist): canonical = {} for k in objlist.keys(): canonical[k] = self.canonical_objspec(objlist[k]) return canonical def sub_parties(self,objlist,msg,parties): # Handle message substitution for many points of view # First we preprocess the message to eliminate absolute refs # (This is an optimization step and isn't strictly necessary) objlist = self.canonical_objlist(objlist) msg = self.parse(msg) for i in range(len(msg)): m = msg[i] if type(m) != type(''): v = self.get_part(self.lookup_in_objlist(objlist,m[0]),m[1],1) if v: msg[i] = v # Now msg has all absolute references processed results = [] for p in parties: # Make a copy of the objlist and replace p with magic `you' object def subst_obj(objlist,p=p): temp = objlist[:] for i in range(len(temp)): if temp[i][1] == p: temp[i] = (None,You) return temp rel_objlist = {} for key in objlist.keys(): rel_objlist[key] = subst_obj(objlist[key]) results.append(self.sub(rel_objlist,msg)) return results,self.sub(objlist,msg) def sub(self,objlist,msg): # Handle message substitution for one point of view s = '' objlist = self.canonical_objlist(objlist) for m in self.parse(msg): if type(m) == type(''): s = s+m else: objspec = self.lookup_in_objlist(objlist,m[0]) s = s+self.get_part(objspec,m[1]) return s # End of message substitution ###################################################################### # Testing code def test(): msg = Msg() Stephen = GenderedObject('Stephen','m','proper') Joe = GenderedObject('Joe','m','proper') Guido = GenderedObject('Guido','m','proper') Amit = GenderedObject('Amit','m','proper') Museum = GenderedObject('museum','n','unique') Boat = GenderedObject('boat','n','normal') Hut = GenderedObject('hut','n','normal') Joe.location = Museum Stephen.location = Boat Guido.location = Museum Amit.location = Hut ale = GenderedObject('ale','n','normal') ducks = GenderedObject('ducks','t','normal') ducks.location = Boat m1 = "While standing in %1ld, %1I %1:(looks) at %1r (%1#) oddly." m2 = "%1'i %1'(hand) %1:(looks) bloody. %1P %1'(glove) %1:(are) not %1q." m3 = "%1I %1:(hits) %2i (%2#) with %1p %1'(hand)." m4 = "In %1li, %1i %1:(raises) %1p %1'(eyebrow) and %1:(peers) at %2i suspiciously." v1 = {1:[ducks,(3,'soldier'),'fooble'],2:ale} print '1:',msg.sub({1:[Joe,Guido]},m1) print '2:',msg.sub({1:[Guido,Joe]},m2) print '3:',msg.sub(v1,m3) print '4:',msg.sub({1:Joe,2:ducks},m3) print ' Test 5 gives the message processed for Amit and Stephen, ' print ' and also a generic message for the rest of the world:' print '5:',msg.sub_parties({1:[Stephen,ducks],2:Amit},m4,[Amit,Stephen]) raw_input() #test() class Gender: def __init__(self): self.foo = 42 # Possible object selector: # x{...} # Unfinished object specifiers: # Instead of (num,string), it should be possible to have (num,s1,s2,s0) # where s1 is the normal singular string, s2 is the plural string, and # s0 is the string to use when the count is 0. # Also unfinished: # Error checking to check for invalid objspecs, invalid property refs,... # Possible addition # Randomness (useful for making messages a little more interesting) # Also needs to be tested a lot more.