March 28, 2012 5 Comments
Boss (unfolding big map across table; talking carefully) Right … this is the plan then. … At 10:52, I shall approach the counter and purchase a watch costing £5.18.3d. I shall then give the watch to you, Vic. You’ll go straight to Norman’s Garage in East Street. You lads continue back up here at 10:56 and we rendezvous in the back room at the Cow and Sickle, at 11:15. All right, any questions?
Larry We don’t seem to be doing anything illegal.
Boss What do you mean?
Larry Well … we’re paying for the watch.
Boss (patiently) Yes…
Larry (hesitating) Well… why are we paying for the watch?
Boss (heavily) They wouldn’t give it to us if we didn’t pay for it, would they… eh?
This is our final instalment [sic] of our Minecraft config editor. In the earlier tutorials we have done everything except actually updating the file. Before we do update the file though, we need to make a backup of it, so it’s these two things that we’re going to do now.
All of the action will be in changing the behaviour of the ‘Ok’ button so that it makes a copy of server.properties into a new file called server.properties.bup and then writes the updated data to the server.properties file. There is a small amount of work in making a copy of the file so we are going to do it in a separate function. The function is not very smart. It reads all of the data from the existing file and then just writes it out to the new file:
def backupFile(fileName): ''' Quick and dirty copy of file to fileName+".bup" - might also use os.rename(), but behaviour of os.rename is platform dependent ''' fileObject = open(fileName,'rb') fileData = fileObject.read() fileObject.close() fileObject= open(fileName+".bup",'wb') # will overwrite if it exists fileObject.write(fileData) fileObject.close()
The function could also use the rename() method from the os module. Unfortunately, this behaves differently depending on the operating system you are using, so to keep it simple I have avoided it.
Exercise: Work out what this function is supposed to do, then confirm that it does it eg: start up a Python console, paste the definition in and then call the function with “server.properties” as a parameter.
Extra Points: If you are on a Unix based system, use the diff command to show the differences between the original and .bup (there shouldn’t be any).
With that done we can hook up the backup to the ‘Ok’ callback, and also write the new data:
def okClicked(): '''Get the edited values and write them to the file then quit''' #TODO: add a confirmation dialog global fileName backupFile(fileName) dataToWrite =  for c in configLines: c.update() dataToWrite.append(c.item2ConfigLine()) # have updated and printed each line, now exit fileObject = open(fileName,'wt') fileObject.write('\n'.join(dataToWrite)+'\n') # '\n' is technically not the newline character on Windows # but by default Python converts \n to the correct character on write # not sure if minecraft needs a final '\n', so included one just in case fileObject.close() exit()
Here we have introduced an array called dataToWrite. Where, in the last tute we just printed out the line, in this tute we are appending those lines to the dataToWrite array. Then, once they have been accumulated, we open the server.properties file (clearing it) then write our new data into it. Only one generation of backup is saved.
We have used the global statement here to use the value of the fileName variable. This is a little messy, but is a consequence of how the program has evolved.
One of the things that you might include here is a confirmation step. Before the data gets overwritten we might ask the user to confirm that they are going to write over their data, giving them a second chance if they clicked “Ok” by mistake.
Complete code here:
# -*- coding: utf-8 -*- '''Minecraft config editor: This is an editor for the Minecraft server.properties file. It: * opens the file server.properties * reads, then closes the file * parses each line by -- stripping leading and trailing whitespace -- if the line starts with "#", marks it as a comment -- splits the line into a key, value pair, with the pair separated by a "=" sign -- if the value of the pair is either "true" or "false", the entry is marked as a boolean (ie its only values are either true or false) * displays each key, value entry on the screen allowing you to edit it * renames the server.properties file to server.properties.bup (overwriting any existing file of that name from earlier edits) * opens a new file called server.properties * writes each of the entries to that new file * closes the server.properties file. ''' from Tkinter import * class configItem(object): # name of the class, it is based on an object called 'object' def __init__(self, line):# this is called each time an instance of the class is created line = line.strip() # this removes any white space at the start or end of the line # if it starts with a # it's a comment so check for it if line[:1] == "#": self.configKey = "#" self.configVal = line[1:] else: # otherwise assume it's of the form x = y spam = line.split("=") self.configKey = spam self.configVal = spam # now check to see whether the config item takes only the values "true" and "false" if self.configVal.lower() in ["true","false"]: self.isTrueFalse = True else: self.isTrueFalse = False def item2ConfigLine(self): if self.configKey=="#": '''If the key is '#' then this is a comment, so don't include an '=' sign''' return "%s%s"%(self.configKey, self.configVal) else: '''otherwise, it has the form key=value''' return "%s=%s"%(self.configKey,self.configVal) class guiConfigItem(configItem): def __init__(self,line): super(guiConfigItem,self).__init__(line) # run configItem's __init__ method self.frame = Frame() self.keyLabel = Label(self.frame, text = self.configKey) self.valueEntry = Entry(self.frame, width="60") self.valueEntry.insert("0",self.configVal) self.keyLabel.pack(side=LEFT ) self.valueEntry.pack(side=RIGHT) self.frame.pack(side=TOP, fill="x") def update(self): ''' Get the value which is currently in the Entry widget and save it to configVal''' if self.isTrueFalse: '''if isTrueFalse is True, then we should only have the values 'true' and 'false' in this Entry. So, only update the configuration value if it is one of these two. Otherwise, ignore it. ''' spam = self.valueEntry.get() if spam in ['true','false']: self.configVal = spam else: '''this is not a variable which is limited to 'true' and 'false', so store the whole text''' self.configVal = self.valueEntry.get() # get data from the file fileName = "ser_ver.properties" fileObject = open(fileName,'rb') fileData = fileObject.read() fileObject.close() root = Tk() configLines =  for line in fileData.split('\n'): # this splits it into individual lines if line.strip()=='': continue configLines.append(guiConfigItem(line=line)) # 1. create callbacks for each of the buttons, def okClicked(): '''Get the edited values and write them to the file then quit''' #TODO: add a confirmation dialog global fileName backupFile(fileName) dataToWrite =  for c in configLines: c.update() dataToWrite.append(c.item2ConfigLine()) # have updated and printed each line, now exit fileObject = open(fileName,'wt') fileObject.write('\n'.join(dataToWrite)+'\n') # '\n' is technically not the newline character on Windows # but by default Python converts \n to the correct character on write # not sure if minecraft needs a final '\n', so included one just in case fileObject.close() exit() def backupFile(fileName): ''' Quick and dirty copy of file to fileName+".bup" - might also use os.rename(), but behaviour of os.rename is platform dependent ''' fileObject = open(fileName,'rb') fileData = fileObject.read() fileObject.close() fileObject= open(fileName+".bup",'wb') # will overwrite if it exists fileObject.write(fileData) fileObject.close() def cancelClicked(): '''Cancel edits and quit''' exit() # 2. create a frame for the buttons to go in bottomFrame = Frame(root) # 3. create the buttons, hooking up each of the buttons up to the callback okWidget = Button(bottomFrame, text= "Ok", command = okClicked) cancelWidget = Button(bottomFrame, text="Cancel", command = cancelClicked) # 4. pack the buttons, then, finally, okWidget.pack(side=LEFT) cancelWidget.pack(side=RIGHT) # 5. then pack the frame: bottomFrame.pack(side=BOTTOM) root.mainloop()
Exercise: confirm that “Ok” saves your edits (open server.properties in a text editor or (extra points) write some Python to read and print the contents of the file) and that “Cancel” doesn’t.
The code is a little messy because of how it has evolved in the course of explaining it. Having code growing organically and getting messy is not unusual. Every once in a while you need to stop and clean it up. Cleaning it up can also allow you to restructure your code in ways you didn’t realise when you were writing it in the first place.
My class names are naughty. They should start with a capital letter.