This is a guide to people who are interested in reading and understanding (and possibly modifying) the source code for Moosic. It is not a guide for developers writing new Moosic clients. See Moosic_API.{html,1,pod} for that. === Code Organization === The source code is organized into different files in the following way: Modules that are useful to any part of Moosic: moosic/utilities.py - generally useful classes and functions. Modules that implement the server/daemon (moosicd): moosic/server/main.py - the program's entry point. moosic/server/methods.py - the methods exported by the Moosic client API. moosic/server/support.py - various helper classes and functions for moosicd. moosic/server/xmlrpc_registry.py - a class for collecting the server methods, dispatching them, and providing introspection for them. moosic/server/daemonize.py - a function for turning a program into a daemon. Modules that are useful for any Moosic client: moosic/client/factory.py - functions which create Moosic server proxies. Modules that implement the command-line client (moosic): moosic/client/cli/main.py - the program's entry point. moosic/client/cli/dispatcher.py - the functions that implement the commands recognized by moosic. All files that do not end with the .py suffix are not program source code, but are either documentation or part of the installation system. The installation system is based upon distutils, which is a Python standard for module distribution and installation. === Adding New Commands or Methods === moosicd and moosic are programmed in a mostly procedural style, and so the basic unit of program modularity and extensibility is the procedure. Both programs each have a registration facility that is designed to allow the addition of new commands or server methods cleanly and easily, with all code for the new command or method unified in a single location in the source code. --------- Adding a New Command to "moosic" --------- The main steps of this task are: (1) Add a function with a specific signature to the moosic/client/cli/dispatcher.py module, (2) decorate this function with a few special attributes, and (3) insert the new function into the "dispatcher" dictionary object contained in moosic/client/cli/dispatcher.py. After this is done, the moosic program will automatically support a new command whose name is the same as the name of the function that was added (more or less). In more detail: (1) The function must take three arguments. The first argument, "moosic", is a proxy object that lets you execute any of the server's methods. This object is most useful for actually implementing the body of the function. The second argument, "arglist", is a list of the arguments that were specified on the command line by the user. arglist is similar to sys.argv, but it does not contain any command line options, nor the program name (argv[0]), nor the name of the moosic command. The third argument, "options", is a dictionary of information that varies from one invoacation of moosic to the next. This information includes such data as the shuffling mode and the location information for contacting the Moosic server. The return value of the function can be any sort of object. The only meaning that this return value has comes from the fact that this value is passed as the parameter to sys.exit(). Therefore, it affects the exit code of the program as a whole, but does nothing more. Refer to the documentation for sys.exit() to see how different object values affect a program's exit code. (2) After the new function has been written, it should have some auxilliary data attributes associated with it. First of all, the new function should have a doc-string that summarizes its syntax for a command-line user. This doc-string should start with the command's name, followed by a terse summary of the command's arguments (using meta-syntactic variables when appropriate), followed by a dash, and finally ending with a brief description of what the command will do. The whole doc string should be preferably a single line less than 80 characters long, and should be no longer than two lines (each 80 characters long). Next, the command's function object should have an attribute named "category" whose value is a string that is equal to one of the keys in the dictionary named "command_categories", which is defined in moosic/client/cli/dispatcher.py. An appropriate category from command_categories should be chosen. This value, like the above-mentioned doc-string, is only used when generating documentation for the commands and doesn't affect the behavior of the command being implemented. If the "category" attribute is not one of the keys of command_categories, then the new command will not be listed by the "help" command nor the "--showcommands" option. The new command's function object should also have an attribute named "num_args" whose value is a short string that describes how many arguments the user will be allowed to specify for the command. This value is used by the check_args() function (which is defined in moosic/client/cli/dispatcher.py) to verify that an acceptable number of arguments have been given to a particular command. The valid values for num_args are mentioned in the documentation for the check_args() function. If num_args doesn't have one of these valid values, then argument checking simply won't be done for the new command. (3) Finally, the new function can be added to the "dispatcher" dictionary. This should be done by creating a new dictionary entry whose key is the name of the command and whose value is the newly defined function. If you look at the existing moosic commands as examples, you'll notice that the key for each entry in the dispatcher dictionary is automatically extracted from the function object by feeding the function's "__name__" attribute to the unmangle() utility function (defined in moosic/client/cli/dispatcher.py). You are encouraged to follow this example, but it is hardly necessary. That's it. If you've done these steps, you've added a new command to moosic. --------- Adding a New Server Method to "moosicd" --------- On the surface, this task seems much simpler than adding a new command to moosic: simply add a new function to moosic/server/methods.py and feed it to the register() method of the moosicd_methods object (which is defined within moosic/server/methods.py). The new function can have any number of arguments, and doesn't need to have any unusual attributes set upon it. However, actually programming a new server method requires awareness of the stateful environment that exists outside of the local function that implements the method. By contrast, "moosic" is a relatively stateless program (since the state is primarily maintained by the server), and therefore each function that implements each moosic command doesn't have to worry about any objects that were not explicitly passed to the function as arguments. In order to keep the task of programming server methods relatively sane, all of the server's global data is contained within an object named "data" (originally defined in moosic/server/support.py, and imported by the other modules that comprise moosicd). This lets a programmer easily distinguish between a method's local variables and the server's state data. The public attributes of this "data" object are listed and described in detail in the Moosic API documentation (which is available in several formats, including HTML, manpage, and POD). The private attributes are documented with comments in moosic/server/support.py. Note that trying to add new attributes to the "data" object during the course of normal use will raise an exception. Adding new attributes to "data" must be done in its class definition. If the new server method is going to modify any of the server's state data, then the appropriate lock should be acquired first by calling "data.lock.acquire()". The code that modifies the state should follow immediately after the lock is acquired. This modification should be done within a "try" block that ends with a "finally" clause that contains a call to "data.lock.release()". The time spent holding the lock should be minimized as much as possible to avoid delaying any other threads that might wish to acquire the lock. The following code demonstrates how to perform such a modification with the proper locking: data.lock.acquire() try: # mutate one of the attributes of data finally: data.lock.release() Since server methods are exported to clients via the XML-RPC protocol, the types of the objects that the new server method takes as parameters and outputs through its return value should be limited to those types supported by XML-RPC. A discussion of these supported types and their relationship to the Python type system can be found in the documentation for the "xmlrpclib" module in the Python Library Reference. One corollary to the requirement to conform to XML-RPC types is that the new function should have an explicit return value. If it doesn't, then Python will automatically make the function return None, which isn't supported as a valid value for passing through XML-RPC (according to the more strict implementations of the XML-RPC standard). As mentioned very briefly above, after the function that implements the new server method has been defined in moosic/server/methods.py, it must be registered into the moosicd_methods object by calling that object's register() method. This register() method takes the function to be registered as its first argument, and a specification of that function's type signature as its second argument. A third argument can be given to specify that the function should be registered under a name other than the identifier used to reference the function in the Python source code (i.e. the function's name). However, this argument is commonly not needed. The second argument to register() warrants further elaboration. The information in this argument is used only for the purposes of documentation, which is quite important since the developer of a Moosic client will need to know how to call the new server method before she can use it. The form of this argument is a list of one or more lists. Each of these lists specifies a valid type signature for the method. (More than one signature is possible because XML-RPC supports function overloading.) The first element of each signature list specifies the type of the method's return value, and all subsequent elements specify the types of the method's parameters (in order). These list elements are symbolic constants that represent datatypes. These constants are imported from the xmlrpc_registry module which is a part of moosicd's source code. After adding the new function and registering it, the process of adding a new server method is complete.