class Response(object): """Response class to handle all output related tasks in one place. This class is basically a wrapper arround a ``StringIO`` instance. It also provides methods for managing http headers. """ def __init__(self, request): """Sets the ``Request`` object that leaded to this response. Creates a ``StringIO`` that is used as a output buffer. """ self._request = request self._out = StringIO() self._headers_sent = False self.headers = {} self.status = "200 OK" self.close = self._out.close self.flush = self._out.flush self.read = self._out.read self.readline = self._out.readline self.readlines = self._out.readlines self.seek = self._out.seek self.tell = self._out.tell self.write = self._out.write self.writelines = self._out.writelines def __iter__(self): """Can't copy the ``__iter__`` method over from the ``StringIO`` instance because iter looks for the method in the class instead of the instance. See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252151 """ return self._out def set_status(self, status): """Sets the status code for this response. The status should be a valid HTTP response status. Examples: >>> resp = Response('some fake request') >>> resp.set_status("200 OK") >>> resp.set_status("404 Not Found") :param status: the status string. """ self.status = status setStatus = tools.deprecated_function(set_status) def get_status(self): """Returns the status code and message of this response. """ return self.status def add_header(self, key, value): """Populates the HTTP header with lines of text. Sets the status code on this response object if the given argument list contains a 'Status' header. Example: >>> resp = Response('some fake request') >>> resp.add_header("Content-type", "text/plain") >>> resp.add_header("Content-Length", "10500") :raises ValueError: This happens when the parameters are not correct. """ key = key.strip() if key.find(' ') != -1 or key.find(':') != -1: raise ValueError, 'There should be no spaces in header keys' value = value.strip() if key.lower() == "status": self.setStatus(str(value)) else: self.headers.update({key: str(value)}) addHeader = tools.deprecated_function(add_header) def get_headers(self): """Returns the headers. """ return self.headers getHeaders = tools.deprecated_function(get_headers) def send_headers(self, out): """Send HTTP Headers to the given output stream. .. Note:: This prints the headers and then the ``\\n\\n`` that separates headers from the body. :param out: The file-like object to print headers to. """ out.write("Status: %s\n" % self.status) out.write('\n'.join([ '%s: %s' % (hkey, self.headers[hkey]) for hkey in self.headers.keys() ])) out.write('\n\n') self._headers_sent = True sendHeaders = tools.deprecated_function(send_headers) def send_body(self, out): """Send the response body to the given output stream. :param out: the file-like object to print the body to. """ self.seek(0) try: out.write(self.read()) except IOError: # this is usually a Broken Pipe because the client dropped the # connection. so we skip it. pass sendBody = tools.deprecated_function(send_body)
class RendererBase: """ Pyblosxom core handles the Input and Process of the system and passes the result of the process to the Renderers for output. All renderers are child classes of RendererBase. RenderBase will contain the public interfaces for all Renderer object. """ def __init__(self, request, stdoutput=sys.stdout): """ Constructor: Initializes the Renderer :param request: The ``Pyblosxom.pyblosxom.Request`` object :param stdoutput: File like object to print to. """ self._request = request # this is a list of tuples of the form (key, value) self._header = [] self._out = stdoutput self._content = None self._content_mtime = None self._needs_content_type = 1 self.rendered = None def write(self, data): """ Convenience method for programs to use instead of accessing self._out.write() Other classes can override this if there is a unique way to write out data, for example, a two stream output, e.g. one output stream and one output log stream. Another use for this could be a plugin that writes out binary files, but because renderers and other frameworks may probably not want you to write to ``stdout`` directly, this method assists you nicely. For example:: def cb_start(args): req = args['request'] renderer = req['renderer'] if reqIsGif and gifFileExists(theGifFile): # Read the file data = open(theGifFile).read() # Modify header renderer.addHeader('Content-type', 'image/gif') renderer.addHeader('Content-Length', len(data)) renderer.showHeaders() # Write to output renderer.write(data) # Tell pyblosxom not to render anymore as data is # processed already renderer.rendered = 1 This simple piece of pseudo-code explains what you could do with this method, though I highly don't recommend this, unless pyblosxom is running continuously. :param data: Piece of string you want printed """ self._out.write(data) def add_header(self, *args): """ Populates the HTTP header with lines of text :param args: Paired list of headers :raises ValueError: This happens when the parameters are not correct """ args = list(args) if len(args) % 2 != 0: raise ValueError('Headers recieved are not in the correct form') while args: key = args.pop(0).strip() if key.find(' ') != -1 or key.find(':') != -1: raise ValueError('There should be no spaces in header keys') value = args.pop(0).strip() self._header.append( (key, value) ) addHeader = tools.deprecated_function(add_header) def set_content(self, content): """ Sets the content. The content can be any of the following: * dict * list of entries :param content: the content to be displayed """ self._content = content if isinstance(self._content, dict): mtime = self._content.get("mtime", time.time()) elif isinstance(self._content, list): mtime = self._content[0].get("mtime", time.time()) else: mtime = time.time() self._content_mtime = mtime setContent = tools.deprecated_function(set_content) def get_content(self): """ Return the content field This is exposed for blosxom callbacks. :returns: content """ return self._content getContent = tools.deprecated_function(get_content) def needs_content_type(self, flag): """ Use the renderer to determine 'Content-Type: x/x' default is to use the renderer for Content-Type, set flag to None to indicate no Content-Type generation. :param flag: True of false value """ self._needs_content_type = flag needsContentType = tools.deprecated_function(needs_content_type) def show_headers(self): """ Updated the headers of the ``Response<Pyblosxom.pyblosxom.Response>`` instance. This is here for backwards compatibility. """ response = self._request.getResponse() for k, v in self._header: response.addHeader(k, v) showHeaders = tools.deprecated_function(show_headers) def render(self, header=True): """ Do final rendering. :param header: whether (True) or not (False) to show the headers """ if header: if self._header: self.show_headers() else: self.add_header('Content-Type', 'text/plain') self.show_headers() if self._content: self.write(self._content) self.rendered = 1
class Pyblosxom: """Main class for Pyblosxom functionality. It handles initialization, defines default behavior, and also pushes the request through all the steps until the output is rendered and we're complete. """ def __init__(self, config, environ, data=None): """Sets configuration and environment and creates the Request object. :param config: dict containing the configuration variables. :param environ: dict containing the environment variables. :param data: dict containing data variables. """ # FIXME: These shouldn't be here. config['pyblosxom_name'] = "pyblosxom" config['pyblosxom_version'] = __version__ self._config = config self._request = Request(config, environ, data) def initialize(self): """The initialize step further initializes the Request by setting additional information in the ``data`` dict, registering plugins, and entryparsers. """ data = self._request.get_data() py_http = self._request.get_http() config = self._request.get_configuration() # initialize the locale, if wanted (will silently fail if locale # is not available) if config.get('locale', None): try: locale.setlocale(locale.LC_ALL, config['locale']) except locale.Error: # invalid locale pass # initialize the tools module tools.initialize(config) data["pyblosxom_version"] = __version__ data['pi_bl'] = '' # if the user specifies base_url in config, we use that. # otherwise we compose it from SCRIPT_NAME in the environment # or we leave it blank. if not "base_url" in config: if py_http.has_key('SCRIPT_NAME'): # allow http and https config['base_url'] = '%s://%s%s' % \ (py_http['wsgi.url_scheme'], py_http['HTTP_HOST'], py_http['SCRIPT_NAME']) else: config["base_url"] = "" # take off the trailing slash for base_url if config['base_url'].endswith("/"): config['base_url'] = config['base_url'][:-1] data_dir = config["datadir"] if data_dir.endswith("/") or data_dir.endswith("\\"): data_dir = data_dir[:-1] config['datadir'] = data_dir # import and initialize plugins plugin_utils.initialize_plugins(config.get("plugin_dirs", []), config.get("load_plugins", None)) # entryparser callback is run here first to allow other # plugins register what file extensions can be used data['extensions'] = tools.run_callback("entryparser", {'txt': blosxom_entry_parser}, mappingfunc=lambda x, y: y, defaultfunc=lambda x: x) def cleanup(self): """This cleans up Pyblosxom after a run. This should be called when Pyblosxom has done everything it needs to do before exiting. """ # log some useful stuff for debugging # this will only be logged if the log_level is "debug" log = tools.getLogger() response = self.get_response() log.debug("status = %s" % response.status) log.debug("headers = %s" % response.headers) def get_request(self): """Returns the Request object for this Pyblosxom instance. """ return self._request getRequest = tools.deprecated_function(get_request) def get_response(self): """Returns the Response object associated with this Request. """ return self._request.getResponse() getResponse = tools.deprecated_function(get_response) def run(self, static=False): """This is the main loop for Pyblosxom. This method will run the handle callback to allow registered handlers to handle the request. If nothing handles the request, then we use the ``default_blosxom_handler``. :param static: True if Pyblosxom should execute in "static rendering mode" and False otherwise. """ self.initialize() # buffer the input stream in a StringIO instance if dynamic # rendering is used. This is done to have a known/consistent # way of accessing incoming data. if not static: self.get_request().buffer_input_stream() # run the start callback tools.run_callback("start", {'request': self._request}) # allow anyone else to handle the request at this point handled = tools.run_callback("handle", {'request': self._request}, mappingfunc=lambda x, y: x, donefunc=lambda x: x) if not handled == 1: blosxom_handler(self._request) # do end callback tools.run_callback("end", {'request': self._request}) # we're done, clean up. # only call this if we're not in static rendering mode. if not static: self.cleanup() def run_callback(self, callback="help"): """This method executes the start callback (initializing plugins), executes the requested callback, and then executes the end callback. This is useful for scripts outside of Pyblosxom that need to do things inside of the Pyblosxom framework. If you want to run a callback from a plugin, use ``tools.run_callback`` instead. :param callback: the name of the callback to execute. :returns: the results of the callback. """ self.initialize() # run the start callback tools.run_callback("start", {'request': self._request}) # invoke all callbacks for the 'callback' handled = tools.run_callback(callback, {'request': self._request}, mappingfunc=lambda x, y: x, donefunc=lambda x: x) # do end callback tools.run_callback("end", {'request': self._request}) return handled runCallback = tools.deprecated_function(run_callback) def run_render_one(self, url, headers): """Renders a single page from the blog. :param url: the url to render--this has to be relative to the base url for this blog. :param headers: True if you want headers to be rendered and False if not. """ self.initialize() config = self._request.get_configuration() if url.find("?") != -1: url = url[:url.find("?")] query = url[url.find("?") + 1:] else: query = "" url = url.replace(os.sep, "/") response = tools.render_url(config, url, query) if headers: response.send_headers(sys.stdout) response.send_body(sys.stdout) print response.read() # we're done, clean up self.cleanup() def run_static_renderer(self, incremental=False): """This will go through all possible things in the blog and statically render everything to the ``static_dir`` specified in the config file. This figures out all the possible ``path_info`` settings and calls ``self.run()`` a bazillion times saving each file. :param incremental: Whether (True) or not (False) to incrementally render the pages. If we're incrementally rendering pages, then we render only the ones that have changed. """ self.initialize() config = self._request.get_configuration() data = self._request.get_data() print "Performing static rendering." if incremental: print "Incremental is set." static_dir = config.get("static_dir", "") data_dir = config["datadir"] if not static_dir: print "Error: You must set static_dir in your config file." return 0 flavours = config.get("static_flavours", ["html"]) index_flavours = config.get("static_index_flavours", ["html"]) render_me = [] month_names = config.get("static_monthnames", True) month_numbers = config.get("static_monthnumbers", False) year_indexes = config.get("static_yearindexes", True) dates = {} categories = {} # first we handle entries and categories listing = tools.walk(self._request, data_dir) for mem in listing: # skip the ones that have bad extensions ext = mem[mem.rfind(".") + 1:] if not ext in data["extensions"].keys(): continue # grab the mtime of the entry file mtime = time.mktime(tools.filestat(self._request, mem)) # remove the datadir from the front and the bit at the end mem = mem[len(data_dir):mem.rfind(".")] # this is the static filename fn = os.path.normpath(static_dir + mem) # grab the mtime of one of the statically rendered file try: smtime = os.stat(fn + "." + flavours[0])[8] except: smtime = 0 # if the entry is more recent than the static, we want to # re-render if smtime < mtime or not incremental: # grab the categories temp = os.path.dirname(mem).split(os.sep) for i in range(len(temp) + 1): p = os.sep.join(temp[0:i]) categories[p] = 0 # grab the date mtime = time.localtime(mtime) year = time.strftime("%Y", mtime) month = time.strftime("%m", mtime) day = time.strftime("%d", mtime) if year_indexes: dates[year] = 1 if month_numbers: dates[year + "/" + month] = 1 dates[year + "/" + month + "/" + day] = 1 if month_names: monthname = tools.num2month[month] dates[year + "/" + monthname] = 1 dates[year + "/" + monthname + "/" + day] = 1 # toss in the render queue for f in flavours: render_me.append((mem + "." + f, "")) print "rendering %d entries." % len(render_me) # handle categories categories = categories.keys() categories.sort() # if they have stuff in their root category, it'll add a "/" # to the category list and we want to remove that because it's # a duplicate of "". if "/" in categories: categories.remove("/") print "rendering %d category indexes." % len(categories) for mem in categories: mem = os.path.normpath(mem + "/index.") for f in index_flavours: render_me.append((mem + f, "")) # now we handle dates dates = dates.keys() dates.sort() dates = ["/" + d for d in dates] print "rendering %d date indexes." % len(dates) for mem in dates: mem = os.path.normpath(mem + "/index.") for f in index_flavours: render_me.append((mem + f, "")) # now we handle arbitrary urls additional_stuff = config.get("static_urls", []) print "rendering %d arbitrary urls." % len(additional_stuff) for mem in additional_stuff: if mem.find("?") != -1: url = mem[:mem.find("?")] query = mem[mem.find("?") + 1:] else: url = mem query = "" render_me.append((url, query)) # now we pass the complete render list to all the plugins via # cb_staticrender_filelist and they can add to the filelist # any (url, query) tuples they want rendered. print "(before) building %s files." % len(render_me) tools.run_callback( "staticrender_filelist", { 'request': self._request, 'filelist': render_me, 'flavours': flavours, 'incremental': incremental }) render_me = sorted(set(render_me)) print "building %s files." % len(render_me) for url, q in render_me: url = url.replace(os.sep, "/") print "rendering '%s' ..." % url tools.render_url_statically(dict(config), url, q) # we're done, clean up self.cleanup()
class Request(object): """ This class holds the Pyblosxom request. It holds configuration information, HTTP/CGI information, and data that we calculate and transform over the course of execution. There should be only one instance of this class floating around and it should get created by ``pyblosxom.cgi`` and passed into the Pyblosxom instance which will do further manipulation on the Request instance. """ def __init__(self, config, environ, data): """Sets configuration and environment. Creates the Response object which handles all output related functionality. :param config: dict containing configuration variables. :param environ: dict containing environment variables. :param data: dict containing data variables. """ # this holds configuration data that the user changes in # config.py self._configuration = config # this holds HTTP/CGI oriented data specific to the request # and the environment in which the request was created self._http = EnvDict(self, environ) # this holds run-time data which gets created and transformed # by pyblosxom during execution if data is None: self._data = dict() else: self._data = data # this holds the input stream. initialized for dynamic # rendering in Pyblosxom.run. for static rendering there is # no input stream. self._in = StringIO() # copy methods to the Request object. self.read = self._in.read self.readline = self._in.readline self.readlines = self._in.readlines self.seek = self._in.seek self.tell = self._in.tell # this holds the FieldStorage instance. # initialized when request.get_form is called the first time self._form = None self._response = None # create and set the Response self.setResponse(Response(self)) def __iter__(self): """ Can't copy the __iter__ method over from the StringIO instance cause iter looks for the method in the class instead of the instance. See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/252151 """ return self._in def buffer_input_stream(self): """ Buffer the input stream in a StringIO instance. This is done to have a known/consistent way of accessing incoming data. For example the input stream passed by mod_python does not offer the same functionality as ``sys.stdin``. """ # TODO: tests on memory consumption when uploading huge files py_http = self.get_http() winput = py_http['wsgi.input'] method = py_http["REQUEST_METHOD"] # there's no data on stdin for a GET request. pyblosxom # will block indefinitely on the read for a GET request with # thttpd. if method != "GET": try: length = int(py_http.get("CONTENT_LENGTH", 0)) except ValueError: length = 0 if length > 0: self._in.write(winput.read(length)) # rewind to start self._in.seek(0) def set_response(self, response): """Sets the Response object. """ self._response = response # for backwards compatibility self.get_configuration()['stdoutput'] = response setResponse = tools.deprecated_function(set_response) def get_response(self): """Returns the Response for this request. """ return self._response getResponse = tools.deprecated_function(get_response) def _getform(self): form = cgi.FieldStorage(fp=self._in, environ=self._http, keep_blank_values=0) # rewind the input buffer self._in.seek(0) return form def get_form(self): """Returns the form data submitted by the client. The ``form`` instance is created only when requested to prevent overhead and unnecessary consumption of the input stream. :returns: a ``cgi.FieldStorage`` instance. """ if self._form is None: self._form = self._getform() return self._form getForm = tools.deprecated_function(get_form) def get_configuration(self): """Returns the *actual* configuration dict. The configuration dict holds values that the user sets in their ``config.py`` file. Modifying the contents of the dict will affect all downstream processing. """ return self._configuration getConfiguration = tools.deprecated_function(get_configuration) def get_http(self): """Returns the *actual* http dict. Holds HTTP/CGI data derived from the environment of execution. Modifying the contents of the dict will affect all downstream processing. """ return self._http getHttp = tools.deprecated_function(get_http) def get_data(self): """Returns the *actual* data dict. Holds run-time data which is created and transformed by pyblosxom during execution. Modifying the contents of the dict will affect all downstream processing. """ return self._data getData = tools.deprecated_function(get_data) def add_http(self, d): """Takes in a dict and adds/overrides values in the existing http dict with the new values. """ self._http.update(d) addHttp = tools.deprecated_function(add_http) def add_data(self, d): """Takes in a dict and adds/overrides values in the existing data dict with the new values. """ self._data.update(d) addData = tools.deprecated_function(add_data) def add_configuration(self, newdict): """Takes in a dict and adds/overrides values in the existing configuration dict with the new values. """ self._configuration.update(newdict) addConfiguration = tools.deprecated_function(add_configuration) def __getattr__(self, name): if name in ["config", "configuration", "conf"]: return self._configuration if name == "data": return self._data if name == "http": return self._http raise AttributeError, name def __repr__(self): return "Request"
template = args["template"] # FIXME - the finaltext.replace(...) below causes \$ to get # unescaped in title and body text which is wrong. this # fix alleviates that somewhat, but there are still edge # cases regarding function data. need a real template # engine with a real parser here. entry = dict(args["entry"]) for k, v in entry.items(): if isinstance(v, basestring): entry[k] = v.replace(r"\$", r"\\$") finaltext = tools.parse(self._request, entry, template) return finaltext.replace(r'\$', '$') renderTemplate = tools.deprecated_function(render_template) def _run_callback(self, chain, input): """ Makes calling blosxom callbacks a bit easier since they all have the same mechanics. This function merely calls run_callback with the arguments given and a mappingfunc. The mappingfunc copies the ``template`` value from the output to the input for the next function. Refer to run_callback for more details. """ input.update({"renderer": self}) input.update({"request": self._request})
class EntryBase: """ EntryBase is the base class for all the Entry classes. Each instance of an Entry class represents a single entry in the weblog, whether it came from a file, or a database, or even somewhere off the InterWeeb. EntryBase derivatives are dict-like except for one key difference: when doing ``__getitem__`` on a nonexistent key, it returns None by default. For example: >>> entry = EntryBase('some fake request') >>> None == entry["some_nonexistent_key"] True """ def __init__(self, request): self._data = "" self._metadata = dict(tools.STANDARD_FILTERS) self._id = "" self._mtime = BIGNUM self._request = request def __repr__(self): """ Returns a friendly debug-able representation of self. Useful to know on what entry pyblosxom fails on you (though unlikely) :returns: Identifiable representation of object """ return "<Entry instance: %s>\n" % self.getId() def get_id(self): """ This should return an id that's unique enough for caching purposes. Override this. :returns: string id """ return self._id getId = tools.deprecated_function(get_id) def get_data(self): """ Returns the data string. This method should be overridden to provide from pulling the data from other places. Override this. :returns: the data as a string """ return str(self._data) getData = tools.deprecated_function(get_data) def set_data(self, data): """ Sets the data content for this entry. If you are not creating the entry, then you have no right to set the data of the entry. Doing so could be hazardous depending on what EntryBase subclass you're dealing with. Override this. :param data: the data """ self._data = data setData = tools.deprecated_function(set_data) def get_metadata(self, key, default=None): """ Returns a given piece of metadata. Override this. :param key: the key being sought :param default: the default to return if the key does not exist :return: either the default (if the key did not exist) or the value of the key in the metadata dict """ return self._metadata.get(key, default) getMetadata = tools.deprecated_function(get_metadata) def set_metadata(self, key, value): """ Sets a key/value pair in the metadata dict. Override this. :param key: the key string :param value: the value string """ self._metadata[key] = value setMetadata = tools.deprecated_function(set_metadata) def get_metadata_keys(self): """ Returns the list of keys for which we have values in our stored metadata. .. Note:: This list gets modified later downstream. If you cache your list of metadata keys, then this method should return a copy of that list and not the list itself lest it get adjusted. Override this. :returns: list of metadata keys """ return self._metadata.keys() getMetadataKeys = tools.deprecated_function(get_metadata_keys) def get_from_cache(self, entryid): """ Retrieves information from the cache that pertains to this specific entryid. This is a helper method--call this to get data from the cache. Do not override it. :param entryid: a unique key for the information you're retrieving :returns: dict with the values or None if there's nothing for that entryid """ cache = tools.get_cache(self._request) # cache.__getitem__ returns None if the id isn't there if cache.has_key(entryid): return cache[entryid] return None getFromCache = tools.deprecated_function(get_from_cache) def add_to_cache(self, entryid, data): """ Over-writes the cached dict for key entryid with the data dict. This is a helper method--call this to add data to the cache. Do not override it. :param entryid: a unique key for the information you're storing :param data: the data to store--this should probably be a dict """ mycache = tools.get_cache(self._request) if mycache: # This could be extended to cover all keys used by # set_time(), but this is the key most likely to turn # up in metadata. If #date is not blocked from caching # here, the templates will use the raw string value # from the user metadata, rather than the value # derived from mtime. if data.has_key('date'): data.pop('date') mycache[entryid] = data addToCache = tools.deprecated_function(add_to_cache) def set_time(self, timetuple): """ This takes in a given time tuple and sets all the magic metadata variables we have according to the items in the time tuple. :param timetuple: the timetuple to use to set the data with--this is the same thing as the mtime/atime portions of an os.stat. This time is expected to be local time, not UTC. """ self['timetuple'] = timetuple self._mtime = time.mktime(timetuple) gmtimetuple = time.gmtime(self._mtime) self['mtime'] = self._mtime self['ti'] = time.strftime('%H:%M', timetuple) self['mo'] = time.strftime('%b', timetuple) self['mo_num'] = time.strftime('%m', timetuple) self['da'] = time.strftime('%d', timetuple) self['dw'] = time.strftime('%A', timetuple) self['yr'] = time.strftime('%Y', timetuple) self['fulltime'] = time.strftime('%Y%m%d%H%M%S', timetuple) self['date'] = time.strftime('%a, %d %b %Y', timetuple) # YYYY-MM-DDThh:mm:ssZ self['w3cdate'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', gmtimetuple) # Temporarily disable the set locale, so RFC-compliant date is # really RFC-compliant: directives %a and %b are locale # dependent. Technically, we're after english locale, but # only 'C' locale is guaranteed to exist. loc = locale.getlocale(locale.LC_ALL) locale.setlocale(locale.LC_ALL, 'C') self['rfc822date'] = time.strftime('%a, %d %b %Y %H:%M GMT', \ gmtimetuple) # set the locale back locale.setlocale(locale.LC_ALL, loc) setTime = tools.deprecated_function(set_time) # everything below this point involves convenience functions # that work with the above functions. def __getitem__(self, key, default=None): """ Retrieves an item from this dict based on the key given. If the item does not exist, then we return the default. If the item is ``CONTENT_KEY``, it calls ``get_data``, otherwise it calls ``get_metadata``. Don't override this. .. Warning:: There's no reason to override this--override ``get_data`` and ``get_metadata`` instead. :param key: the key being sought :param default: the default to return if the key does not exist :returns: the value of ``get_metadata`` or ``get_data`` """ if key == CONTENT_KEY: return self.get_data() return self.get_metadata(key, default) def get(self, key, default=None): """ Retrieves an item from the internal dict based on the key given. All this does is turn aroun and call ``__getitem__``. .. Warning:: There's no reason to override this--override ``get_data`` and ``get_metadata`` instead. :param key: the key being sought :param default: the default to return if the key does not exist :returns: the value of ``get_metadata`` or ``get_data`` (through ``__getitem__``) """ return self.__getitem__(key, default) def __setitem__(self, key, value): """ Sets the metadata[key] to the given value. This uses ``set_data`` and ``set_metadata``. Don't override this. :param key: the given key name :param value: the given value """ if key == CONTENT_KEY: self.set_data(value) else: self.set_metadata(key, value) def update(self, newdict): """ Updates the contents in this entry with the contents in the dict. It does so by calling ``set_data`` and ``set_metadata``. .. Warning:: There's no reason to override this--override ``set_data`` and ``set_metadata`` instead. :param newdict: the dict we're updating this one with """ for mem in newdict.keys(): if mem == CONTENT_KEY: self.set_data(newdict[mem]) else: self.set_metadata(mem, newdict[mem]) def has_key(self, key): """ Returns whether a given key is in the metadata dict. If the key is the ``CONTENT_KEY``, then we automatically return true. .. Warning:: There's no reason to override this--override ``get_metadata`` instead. :param key: the key to check in the metadata dict for :returns: whether (True) or not (False) the key exists """ if key == CONTENT_KEY or key == CONTENT_KEY + "_escaped": return True value = self.get_metadata(key, DOESNOTEXIST) if value == DOESNOTEXIST: value = self.get_metadata(key, DOESNOTEXIST2) if value == DOESNOTEXIST2: return False return True def keys(self): """ Returns a list of the keys that can be accessed through ``__getitem__``. .. Warning:: There's no reason to override this--override ``get_metadata_keys`` instead. :returns: list of key names """ keys = self.get_metadata_keys() if CONTENT_KEY not in keys: keys.append(CONTENT_KEY) return keys
class FileEntry(base.EntryBase): """ This class gets it's data and metadata from the file specified by the filename argument. """ def __init__(self, request, filename, root, datadir=""): """ :param request: the Request object :param filename: the complete filename for the file in question including path :param root: i have no clue what this is :param datadir: the datadir """ base.EntryBase.__init__(self, request) self._config = request.get_configuration() self._filename = filename.replace(os.sep, '/') self._root = root.replace(os.sep, '/') self._datadir = datadir or self._config["datadir"] if self._datadir.endswith(os.sep): self._datadir = self._datadir[:-1] self._timetuple = tools.filestat(self._request, self._filename) self._mtime = time.mktime(self._timetuple) self._fulltime = time.strftime("%Y%m%d%H%M%S", self._timetuple) self._populated_data = 0 def __repr__(self): return "<fileentry f'%s' r'%s'>" % (self._filename, self._root) def get_id(self): """ Returns the id for this content item--in this case, it's the filename. :returns: the id of the fileentry (the filename) """ return self._filename getId = tools.deprecated_function(get_id) def get_data(self): """ Returns the data for this file entry. The data is the parsed (via the entryparser) content of the entry. We do this on-demand by checking to see if we've gotten it and if we haven't then we get it at that point. :returns: the content for this entry """ if self._populated_data == 0: self._populatedata() return self._data getData = tools.deprecated_function(get_data) def get_metadata(self, key, default=None): """ This overrides the ``base.EntryBase`` ``get_metadata`` method. .. Note:: We populate our metadata lazily--only when it's requested. This delays parsing of the file as long as we can. """ if self._populated_data == 0: self._populatedata() return self._metadata.get(key, default) getMetadata = tools.deprecated_function(get_metadata) def _populatedata(self): """ Fills the metadata dict with metadata about the given file. This metadata consists of things we pick up from an os.stat call as well as knowledge of the filename and the root directory. We then parse the file and fill in the rest of the information that we know. """ file_basename = os.path.basename(self._filename) path = self._filename.replace(self._root, '') path = path.replace(os.path.basename(self._filename), '') path = path[:-1] absolute_path = self._filename.replace(self._datadir, '', 1) absolute_path = absolute_path.replace(file_basename, '') absolute_path = absolute_path[1:][:-1] if absolute_path and absolute_path[-1] == "/": absolute_path = absolute_path[0:-1] filename_no_ext = os.path.splitext(file_basename)[0] if absolute_path == '': file_path = filename_no_ext else: file_path = '/'.join((absolute_path, filename_no_ext)) tb_id = '%s/%s' % (absolute_path, filename_no_ext) tb_id = re.sub(r'[^A-Za-z0-9]', '_', tb_id) self['path'] = path self['tb_id'] = tb_id self['absolute_path'] = absolute_path self['file_path'] = file_path self['fn'] = filename_no_ext self['filename'] = self._filename self.set_time(self._timetuple) data = self._request.get_data() entry_dict = self.get_from_cache(self._filename) if not entry_dict: file_ext = os.path.splitext(self._filename) if file_ext: file_ext = file_ext[1][1:] eparser = data['extensions'][file_ext] entry_dict = eparser(self._filename, self._request) self.add_to_cache(self._filename, entry_dict) self.update(entry_dict) self._populated_data = 1
class BlosxomRenderer(RendererBase): """ This is the default blosxom renderer. It tries to match the behavior of the blosxom renderer. """ def __init__(self, request, stdoutput=sys.stdout): RendererBase.__init__(self, request, stdoutput) config = request.get_configuration() self._request = request self.flavour = None def get_parse_vars(self): """Returns a dict starting with standard filters, config information, then data information. This allows vars to override each other correctly. For example, plugins should be adding to the data dict which will override stuff in the config dict. """ parsevars = dict(tools.STANDARD_FILTERS) parsevars.update(self._request.config) parsevars.update(self._request.data) return parsevars def get_flavour(self, taste='html'): """ This retrieves all the template files for a given flavour taste. This will first pull the templates for the default flavour of this taste if there are any. Then it looks at EITHER the configured datadir OR the flavourdir (if configured). It'll go through directories overriding the template files it has already picked up descending the category path of the Pyblosxom request. For example, if the user requested the ``html`` flavour and is looking at an entry in the category ``dev/pyblosxom``, then ``get_flavour`` will: 1. pick up the flavour files in the default html flavour 2. start in EITHER datadir OR flavourdir (if configured) 3. override the default html flavour files with html flavour files in this directory or in ``html.flav/`` subdirectory 4. override the html flavour files it's picked up so far with html files in ``dev/`` or ``dev/html.flav/`` 5. override the html flavour files it's picked up so far with html files in ``dev/pyblosxom/`` or ``dev/pyblosxom/html.flav/`` If it doesn't find any flavour files at all, then it returns None which indicates the flavour doesn't exist in this blog. :param taste: the taste to retrieve flavour files for. :returns: mapping of template name to template file data """ data = self._request.get_data() config = self._request.get_configuration() datadir = config["datadir"] # if they have flavourdir set, then we look there. otherwise # we look in the datadir. flavourdir = config.get("flavourdir", datadir) # first we grab the flavour files for the included flavour (if # we have one). template_d = get_included_flavour(taste) if not template_d: template_d = {} pathinfo = list(data["path_info"]) # check the root of flavourdir for templates new_files = get_flavour_from_dir(flavourdir, taste) if new_files: template_d.update(new_files) # go through all the directories from the flavourdir all # the way up to the root_datadir. this way template files # can override template files in parent directories. while len(pathinfo) > 0: flavourdir = os.path.join(flavourdir, pathinfo.pop(0)) if os.path.isfile(flavourdir): break if not os.path.isdir(flavourdir): break new_files = get_flavour_from_dir(flavourdir, taste) if new_files: template_d.update(new_files) # if we still haven't found our flavour files, we raise an exception if not template_d: raise NoSuchFlavourException("Flavour '%s' does not exist." % taste) for k in template_d.keys(): try: flav_template = open(template_d[k]).read() template_d[k] = flav_template except (OSError, IOError): pass return template_d def render_content(self, content): """ Processes the content for the story portion of a page. :param content: the content to be rendered :returns: the content string """ data = self._request.get_data() outputbuffer = [] if callable(content): # if the content is a callable function, then we just spit out # whatever it returns as a string outputbuffer.append(content()) elif isinstance(content, dict): # if the content is a dict, then we parse it as if it were an # entry--except it's distinctly not an EntryBase derivative var_dict = self.get_parse_vars() var_dict.update(content) output = tools.parse(self._request, var_dict, self.flavour['story']) outputbuffer.append(output) elif isinstance(content, list): if len(content) > 0: current_date = content[0]["date"] if current_date and "date_head" in self.flavour: parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date, "yr": content[0]["yr"], "mo": content[0]["mo"], "da": content[0]["da"]}) outputbuffer.append( self.render_template(parse_vars, "date_head")) for entry in content: if entry["date"] and entry["date"] != current_date: if "date_foot" in self.flavour: parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date, "yr": content[0]["yr"], "mo": content[0]["mo"], "da": content[0]["da"]}) outputbuffer.append( self.render_template(parse_vars, "date_foot")) if "date_head" in self.flavour: current_date = entry["date"] parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date, "yr": content[0]["yr"], "mo": content[0]["mo"], "da": content[0]["da"]}) outputbuffer.append( self.render_template(parse_vars, "date_head")) if data['content-type'] == 'text/plain': s = tools.Stripper() s.feed(entry.get_data()) s.close() p = [' ' + line for line in s.gettext().split('\n')] entry.set_data('\n'.join(p)) parse_vars = self.get_parse_vars() parse_vars.update(entry) outputbuffer.append( self.render_template(parse_vars, "story", override=1)) args = {"entry": parse_vars, "template": ""} args = self._run_callback("story_end", args) outputbuffer.append(args["template"]) if current_date and "date_foot" in self.flavour: parse_vars = self.get_parse_vars() parse_vars.update({"date": current_date}) outputbuffer.append( self.render_template(parse_vars, "date_foot")) return outputbuffer renderContent = tools.deprecated_function(render_content) def render(self, header=True): """ Figures out flavours and such and then renders the content according to which flavour we're using. :param header: whether (True) or not (False) to render the HTTP headers """ # if we've already rendered, then we don't want to do so again if self.rendered: return data = self._request.get_data() config = self._request.get_configuration() try: self.flavour = self.get_flavour(data.get("flavour", "html")) except NoSuchFlavourException, nsfe: error_msg = str(nsfe) try: self.flavour = self.get_flavour("error") except NoSuchFlavourException: self.flavour = get_included_flavour("error") error_msg += " And your error flavour doesn't exist, either." resp = self._request.getResponse() resp.set_status("404 Not Found") self._content = {"title": "HTTP 404: Flavour error", "body": error_msg} data['content-type'] = self.flavour['content_type'].strip() if header: if self._needs_content_type and data['content-type'] != "": self.add_header('Content-type', '%(content-type)s' % data) self.show_headers() if self._content: if "head" in self.flavour: self.write(self.render_template(self.get_parse_vars(), "head")) if "story" in self.flavour: content = self.render_content(self._content) for i, mem in enumerate(content): if isinstance(mem, unicode): content[i] = mem.encode("utf-8") content = "".join(content) self.write(content) if "foot" in self.flavour: self.write(self.render_template(self.get_parse_vars(), "foot")) self.rendered = 1