pyxdg-0.25/0000775000175000017500000000000012060214733013403 5ustar thomasthomas00000000000000pyxdg-0.25/TODO0000664000175000017500000000010712025321321014062 0ustar thomasthomas00000000000000TODO: ===== Never Finished: - Performance improvements - Debug Info pyxdg-0.25/setup.py0000664000175000017500000000133712060213666015125 0ustar thomasthomas00000000000000#!/usr/bin/python from distutils.core import setup setup( name = "pyxdg", version = "0.25", description = "PyXDG contains implementations of freedesktop.org standards in python.", maintainer = "Freedesktop.org", maintainer_email = "xdg@lists.freedesktop.org", url = "http://freedesktop.org/wiki/Software/pyxdg", packages = ['xdg'], classifiers = [ "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Topic :: Desktop Environment", ], ) pyxdg-0.25/xdg/0000775000175000017500000000000012060214733014165 5ustar thomasthomas00000000000000pyxdg-0.25/xdg/Mime.py0000664000175000017500000003714512057761216015451 0ustar thomasthomas00000000000000""" This module is based on a rox module (LGPL): http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log This module provides access to the shared MIME database. types is a dictionary of all known MIME types, indexed by the type name, e.g. types['application/x-python'] Applications can install information about MIME types by storing an XML file as /packages/.xml and running the update-mime-database command, which is provided by the freedesktop.org shared mime database package. See http://www.freedesktop.org/standards/shared-mime-info-spec/ for information about the format of these files. (based on version 0.13) """ import os import stat import sys import fnmatch from xdg import BaseDirectory import xdg.Locale from xml.dom import minidom, XML_NAMESPACE from collections import defaultdict FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' types = {} # Maps MIME names to type objects exts = None # Maps extensions to types globs = None # List of (glob, type) pairs literals = None # Maps liternal names to types magic = None PY3 = (sys.version_info[0] >= 3) def _get_node_data(node): """Get text of XML node""" return ''.join([n.nodeValue for n in node.childNodes]).strip() def lookup(media, subtype = None): """Get the MIMEtype object for this type, creating a new one if needed. The name can either be passed as one part ('text/plain'), or as two ('text', 'plain'). """ if subtype is None and '/' in media: media, subtype = media.split('/', 1) if (media, subtype) not in types: types[(media, subtype)] = MIMEtype(media, subtype) return types[(media, subtype)] class MIMEtype: """Type holding data about a MIME type""" def __init__(self, media, subtype): "Don't use this constructor directly; use mime.lookup() instead." assert media and '/' not in media assert subtype and '/' not in subtype assert (media, subtype) not in types self.media = media self.subtype = subtype self._comment = None def _load(self): "Loads comment for current language. Use get_comment() instead." resource = os.path.join('mime', self.media, self.subtype + '.xml') for path in BaseDirectory.load_data_paths(resource): doc = minidom.parse(path) if doc is None: continue for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' goodness = 1 + (lang in xdg.Locale.langs) if goodness > self._comment[0]: self._comment = (goodness, _get_node_data(comment)) if goodness == 2: return # FIXME: add get_icon method def get_comment(self): """Returns comment for current language, loading it if needed.""" # Should we ever reload? if self._comment is None: self._comment = (0, str(self)) self._load() return self._comment[1] def canonical(self): """Returns the canonical MimeType object if this is an alias.""" update_cache() s = str(self) if s in aliases: return lookup(aliases[s]) return self def inherits_from(self): """Returns a set of Mime types which this inherits from.""" update_cache() return set(lookup(t) for t in inheritance[str(self)]) def __str__(self): return self.media + '/' + self.subtype def __repr__(self): return '<%s: %s>' % (self, self._comment or '(comment not loaded)') class MagicRule: def __init__(self, f): self.next=None self.prev=None #print line ind=b'' while True: c=f.read(1) if c == b'>': break ind+=c if not ind: self.nest=0 else: self.nest=int(ind.decode('ascii')) start = b'' while True: c = f.read(1) if c == b'=': break start += c self.start = int(start.decode('ascii')) hb=f.read(1) lb=f.read(1) self.lenvalue = ord(lb)+(ord(hb)<<8) self.value = f.read(self.lenvalue) c = f.read(1) if c == b'&': self.mask = f.read(self.lenvalue) c = f.read(1) else: self.mask=None if c == b'~': w = b'' while c!=b'+' and c!=b'\n': c=f.read(1) if c==b'+' or c==b'\n': break w+=c self.word=int(w.decode('ascii')) else: self.word=1 if c==b'+': r=b'' while c!=b'\n': c=f.read(1) if c==b'\n': break r+=c #print r self.range = int(r.decode('ascii')) else: self.range = 1 if c != b'\n': raise ValueError('Malformed MIME magic line') def getLength(self): return self.start+self.lenvalue+self.range def appendRule(self, rule): if self.nest%d=[%d]%r&%r~%d+%d>' % (self.nest, self.start, self.lenvalue, self.value, self.mask, self.word, self.range) class MagicType: def __init__(self, mtype): self.mtype=mtype self.top_rules=[] self.last_rule=None def getLine(self, f): nrule=MagicRule(f) if nrule.nest and self.last_rule: self.last_rule.appendRule(nrule) else: self.top_rules.append(nrule) self.last_rule=nrule return nrule def match(self, buffer): for rule in self.top_rules: if rule.match(buffer): return self.mtype def __repr__(self): return '' % self.mtype class MagicDB: def __init__(self): self.types={} # Indexed by priority, each entry is a list of type rules self.maxlen=0 def mergeFile(self, fname): with open(fname, 'rb') as f: line = f.readline() if line != b'MIME-Magic\0\n': raise IOError('Not a MIME magic file') while True: shead = f.readline().decode('ascii') #print shead if not shead: break if shead[0] != '[' or shead[-2:] != ']\n': raise ValueError('Malformed section heading') pri, tname = shead[1:-2].split(':') #print shead[1:-2] pri = int(pri) mtype = lookup(tname) try: ents = self.types[pri] except: ents = [] self.types[pri] = ents magictype = MagicType(mtype) #print tname #rline=f.readline() c=f.read(1) f.seek(-1, 1) while c and c != b'[': rule=magictype.getLine(f) #print rule if rule and rule.getLength() > self.maxlen: self.maxlen = rule.getLength() c = f.read(1) f.seek(-1, 1) ents.append(magictype) #self.types[pri]=ents if not c: break def match_data(self, data, max_pri=100, min_pri=0): for priority in sorted(self.types.keys(), reverse=True): #print priority, max_pri, min_pri if priority > max_pri: continue if priority < min_pri: break for type in self.types[priority]: m=type.match(data) if m: return m def match(self, path, max_pri=100, min_pri=0): try: with open(path, 'rb') as f: buf = f.read(self.maxlen) return self.match_data(buf, max_pri, min_pri) except: pass def __repr__(self): return '' % self.types # Some well-known types text = lookup('text', 'plain') inode_block = lookup('inode', 'blockdevice') inode_char = lookup('inode', 'chardevice') inode_dir = lookup('inode', 'directory') inode_fifo = lookup('inode', 'fifo') inode_socket = lookup('inode', 'socket') inode_symlink = lookup('inode', 'symlink') inode_door = lookup('inode', 'door') app_exe = lookup('application', 'executable') _cache_uptodate = False def _cache_database(): global exts, globs, literals, magic, aliases, inheritance, _cache_uptodate _cache_uptodate = True exts = {} # Maps extensions to types globs = [] # List of (glob, type) pairs literals = {} # Maps literal names to types aliases = {} # Maps alias Mime types to canonical names inheritance = defaultdict(set) # Maps to sets of parent mime types. magic = MagicDB() def _import_glob_file(path): """Loads name matching information from a MIME directory.""" with open(path) as f: for line in f: if line.startswith('#'): continue line = line[:-1] type_name, pattern = line.split(':', 1) mtype = lookup(type_name) if pattern.startswith('*.'): rest = pattern[2:] if not ('*' in rest or '[' in rest or '?' in rest): exts[rest] = mtype continue if '*' in pattern or '[' in pattern or '?' in pattern: globs.append((pattern, mtype)) else: literals[pattern] = mtype for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs')): _import_glob_file(path) for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): magic.mergeFile(path) # Sort globs by length globs.sort(key=lambda x: len(x[0]) ) # Load aliases for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')): with open(path, 'r') as f: for line in f: alias, canonical = line.strip().split(None, 1) aliases[alias] = canonical # Load subclasses for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')): with open(path, 'r') as f: for line in f: sub, parent = line.strip().split(None, 1) inheritance[sub].add(parent) def update_cache(): if not _cache_uptodate: _cache_database() def get_type_by_name(path): """Returns type of file by its name, or None if not known""" update_cache() leaf = os.path.basename(path) if leaf in literals: return literals[leaf] lleaf = leaf.lower() if lleaf in literals: return literals[lleaf] ext = leaf while 1: p = ext.find('.') if p < 0: break ext = ext[p + 1:] if ext in exts: return exts[ext] ext = lleaf while 1: p = ext.find('.') if p < 0: break ext = ext[p+1:] if ext in exts: return exts[ext] for (glob, mime_type) in globs: if fnmatch.fnmatch(leaf, glob): return mime_type if fnmatch.fnmatch(lleaf, glob): return mime_type return None def get_type_by_contents(path, max_pri=100, min_pri=0): """Returns type of file by its contents, or None if not known""" update_cache() return magic.match(path, max_pri, min_pri) def get_type_by_data(data, max_pri=100, min_pri=0): """Returns type of the data, which should be bytes.""" update_cache() return magic.match_data(data, max_pri, min_pri) def get_type(path, follow=True, name_pri=100): """Returns type of file indicated by path. path : pathname to check (need not exist) follow : when reading file, follow symbolic links name_pri : Priority to do name matches. 100=override magic This tries to use the contents of the file, and falls back to the name. It can also handle special filesystem objects like directories and sockets. """ update_cache() try: if follow: st = os.stat(path) else: st = os.lstat(path) except: t = get_type_by_name(path) return t or text if stat.S_ISREG(st.st_mode): t = get_type_by_contents(path, min_pri=name_pri) if not t: t = get_type_by_name(path) if not t: t = get_type_by_contents(path, max_pri=name_pri) if t is None: if stat.S_IMODE(st.st_mode) & 0o111: return app_exe else: return text return t elif stat.S_ISDIR(st.st_mode): return inode_dir elif stat.S_ISCHR(st.st_mode): return inode_char elif stat.S_ISBLK(st.st_mode): return inode_block elif stat.S_ISFIFO(st.st_mode): return inode_fifo elif stat.S_ISLNK(st.st_mode): return inode_symlink elif stat.S_ISSOCK(st.st_mode): return inode_socket return inode_door def install_mime_info(application, package_file): """Copy 'package_file' as ``~/.local/share/mime/packages/.xml.`` If package_file is None, install ``/.xml``. If already installed, does nothing. May overwrite an existing file with the same name (if the contents are different)""" application += '.xml' new_data = open(package_file).read() # See if the file is already installed package_dir = os.path.join('mime', 'packages') resource = os.path.join(package_dir, application) for x in BaseDirectory.load_data_paths(resource): try: old_data = open(x).read() except: continue if old_data == new_data: return # Already installed global _cache_uptodate _cache_uptodate = False # Not already installed; add a new copy # Create the directory structure... new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application) # Write the file... open(new_file, 'w').write(new_data) # Update the database... command = 'update-mime-database' if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')): os.unlink(new_file) raise Exception("The '%s' command returned an error code!\n" \ "Make sure you have the freedesktop.org shared MIME package:\n" \ "http://standards.freedesktop.org/shared-mime-info/" % command) pyxdg-0.25/xdg/IniFile.py0000664000175000017500000003131612057215757016077 0ustar thomasthomas00000000000000""" Base Class for DesktopEntry, IconTheme and IconData """ import re, os, stat, io from xdg.Exceptions import (ParsingError, DuplicateGroupError, NoGroupError, NoKeyError, DuplicateKeyError, ValidationError, debug) import xdg.Locale from xdg.util import u def is_ascii(s): """Return True if a string consists entirely of ASCII characters.""" try: s.encode('ascii', 'strict') return True except UnicodeError: return False class IniFile: defaultGroup = '' fileExtension = '' filename = '' tainted = False def __init__(self, filename=None): self.content = dict() if filename: self.parse(filename) def __cmp__(self, other): return cmp(self.content, other.content) def parse(self, filename, headers=None): '''Parse an INI file. headers -- list of headers the parser will try to select as a default header ''' # for performance reasons content = self.content if not os.path.isfile(filename): raise ParsingError("File not found", filename) try: # The content should be UTF-8, but legacy files can have other # encodings, including mixed encodings in one file. We don't attempt # to decode them, but we silence the errors. fd = io.open(filename, 'r', encoding='utf-8', errors='replace') except IOError as e: if debug: raise e else: return # parse file for line in fd: line = line.strip() # empty line if not line: continue # comment elif line[0] == '#': continue # new group elif line[0] == '[': currentGroup = line.lstrip("[").rstrip("]") if debug and self.hasGroup(currentGroup): raise DuplicateGroupError(currentGroup, filename) else: content[currentGroup] = {} # key else: try: key, value = line.split("=", 1) except ValueError: raise ParsingError("Invalid line: " + line, filename) key = key.strip() # Spaces before/after '=' should be ignored try: if debug and self.hasKey(key, currentGroup): raise DuplicateKeyError(key, currentGroup, filename) else: content[currentGroup][key] = value.strip() except (IndexError, UnboundLocalError): raise ParsingError("Parsing error on key, group missing", filename) fd.close() self.filename = filename self.tainted = False # check header if headers: for header in headers: if header in content: self.defaultGroup = header break else: raise ParsingError("[%s]-Header missing" % headers[0], filename) # start stuff to access the keys def get(self, key, group=None, locale=False, type="string", list=False): # set default group if not group: group = self.defaultGroup # return key (with locale) if (group in self.content) and (key in self.content[group]): if locale: value = self.content[group][self.__addLocale(key, group)] else: value = self.content[group][key] else: if debug: if group not in self.content: raise NoGroupError(group, self.filename) elif key not in self.content[group]: raise NoKeyError(key, group, self.filename) else: value = "" if list == True: values = self.getList(value) result = [] else: values = [value] for value in values: if type == "boolean": value = self.__getBoolean(value) elif type == "integer": try: value = int(value) except ValueError: value = 0 elif type == "numeric": try: value = float(value) except ValueError: value = 0.0 elif type == "regex": value = re.compile(value) elif type == "point": x, y = value.split(",") value = int(x), int(y) if list == True: result.append(value) else: result = value return result # end stuff to access the keys # start subget def getList(self, string): if re.search(r"(? 0: key = key + "[" + xdg.Locale.langs[0] + "]" try: self.content[group][key] = value except KeyError: raise NoGroupError(group, self.filename) self.tainted = (value == self.get(key, group)) def addGroup(self, group): if self.hasGroup(group): if debug: raise DuplicateGroupError(group, self.filename) else: self.content[group] = {} self.tainted = True def removeGroup(self, group): existed = group in self.content if existed: del self.content[group] self.tainted = True else: if debug: raise NoGroupError(group, self.filename) return existed def removeKey(self, key, group=None, locales=True): # set default group if not group: group = self.defaultGroup try: if locales: for name in list(self.content[group]): if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: del self.content[group][name] value = self.content[group].pop(key) self.tainted = True return value except KeyError as e: if debug: if e == group: raise NoGroupError(group, self.filename) else: raise NoKeyError(key, group, self.filename) else: return "" # misc def groups(self): return self.content.keys() def hasGroup(self, group): return group in self.content def hasKey(self, key, group=None): # set default group if not group: group = self.defaultGroup return key in self.content[group] def getFileName(self): return self.filename pyxdg-0.25/xdg/Menu.py0000664000175000017500000011324412046047251015453 0ustar thomasthomas00000000000000""" Implementation of the XDG Menu Specification Version 1.0.draft-1 http://standards.freedesktop.org/menu-spec/ Example code: from xdg.Menu import parse, Menu, MenuEntry def print_menu(menu, tab=0): for submenu in menu.Entries: if isinstance(submenu, Menu): print ("\t" * tab) + unicode(submenu) print_menu(submenu, tab+1) elif isinstance(submenu, MenuEntry): print ("\t" * tab) + unicode(submenu.DesktopEntry) print_menu(parse()) """ import locale, os, xml.dom.minidom import subprocess from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs from xdg.DesktopEntry import DesktopEntry from xdg.Exceptions import ParsingError, ValidationError, debug from xdg.util import PY3 import xdg.Locale import xdg.Config ELEMENT_NODE = xml.dom.Node.ELEMENT_NODE def _strxfrm(s): """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. See Python bug #2481. """ if (not PY3) and isinstance(s, unicode): s = s.encode('utf-8') return locale.strxfrm(s) class Menu: """Menu containing sub menus under menu.Entries Contains both Menu and MenuEntry items. """ def __init__(self): # Public stuff self.Name = "" self.Directory = None self.Entries = [] self.Doc = "" self.Filename = "" self.Depth = 0 self.Parent = None self.NotInXml = False # Can be one of Deleted/NoDisplay/Hidden/Empty/NotShowIn or True self.Show = True self.Visible = 0 # Private stuff, only needed for parsing self.AppDirs = [] self.DefaultLayout = None self.Deleted = "notset" self.Directories = [] self.DirectoryDirs = [] self.Layout = None self.MenuEntries = [] self.Moves = [] self.OnlyUnallocated = "notset" self.Rules = [] self.Submenus = [] def __str__(self): return self.Name def __add__(self, other): for dir in other.AppDirs: self.AppDirs.append(dir) for dir in other.DirectoryDirs: self.DirectoryDirs.append(dir) for directory in other.Directories: self.Directories.append(directory) if other.Deleted != "notset": self.Deleted = other.Deleted if other.OnlyUnallocated != "notset": self.OnlyUnallocated = other.OnlyUnallocated if other.Layout: self.Layout = other.Layout if other.DefaultLayout: self.DefaultLayout = other.DefaultLayout for rule in other.Rules: self.Rules.append(rule) for move in other.Moves: self.Moves.append(move) for submenu in other.Submenus: self.addSubmenu(submenu) return self # FIXME: Performance: cache getName() def __cmp__(self, other): return locale.strcoll(self.getName(), other.getName()) def _key(self): """Key function for locale-aware sorting.""" return _strxfrm(self.getName()) def __lt__(self, other): try: other = other._key() except AttributeError: pass return self._key() < other def __eq__(self, other): try: return self.Name == unicode(other) except NameError: # unicode() becomes str() in Python 3 return self.Name == str(other) """ PUBLIC STUFF """ def getEntries(self, hidden=False): """Interator for a list of Entries visible to the user.""" for entry in self.Entries: if hidden == True: yield entry elif entry.Show == True: yield entry # FIXME: Add searchEntry/seaqrchMenu function # search for name/comment/genericname/desktopfileide # return multiple items def getMenuEntry(self, desktopfileid, deep = False): """Searches for a MenuEntry with a given DesktopFileID.""" for menuentry in self.MenuEntries: if menuentry.DesktopFileID == desktopfileid: return menuentry if deep == True: for submenu in self.Submenus: submenu.getMenuEntry(desktopfileid, deep) def getMenu(self, path): """Searches for a Menu with a given path.""" array = path.split("/", 1) for submenu in self.Submenus: if submenu.Name == array[0]: if len(array) > 1: return submenu.getMenu(array[1]) else: return submenu def getPath(self, org=False, toplevel=False): """Returns this menu's path in the menu structure.""" parent = self names=[] while 1: if org: names.append(parent.Name) else: names.append(parent.getName()) if parent.Depth > 0: parent = parent.Parent else: break names.reverse() path = "" if toplevel == False: names.pop(0) for name in names: path = os.path.join(path, name) return path def getName(self): """Returns the menu's localised name.""" try: return self.Directory.DesktopEntry.getName() except AttributeError: return self.Name def getGenericName(self): """Returns the menu's generic name.""" try: return self.Directory.DesktopEntry.getGenericName() except AttributeError: return "" def getComment(self): """Returns the menu's comment text.""" try: return self.Directory.DesktopEntry.getComment() except AttributeError: return "" def getIcon(self): """Returns the menu's icon, filename or simple name""" try: return self.Directory.DesktopEntry.getIcon() except AttributeError: return "" """ PRIVATE STUFF """ def addSubmenu(self, newmenu): for submenu in self.Submenus: if submenu == newmenu: submenu += newmenu break else: self.Submenus.append(newmenu) newmenu.Parent = self newmenu.Depth = self.Depth + 1 class Move: "A move operation" def __init__(self, node=None): if node: self.parseNode(node) else: self.Old = "" self.New = "" def __cmp__(self, other): return cmp(self.Old, other.Old) def parseNode(self, node): for child in node.childNodes: if child.nodeType == ELEMENT_NODE: if child.tagName == "Old": try: self.parseOld(child.childNodes[0].nodeValue) except IndexError: raise ValidationError('Old cannot be empty', '??') elif child.tagName == "New": try: self.parseNew(child.childNodes[0].nodeValue) except IndexError: raise ValidationError('New cannot be empty', '??') def parseOld(self, value): self.Old = value def parseNew(self, value): self.New = value class Layout: "Menu Layout class" def __init__(self, node=None): self.order = [] if node: self.show_empty = node.getAttribute("show_empty") or "false" self.inline = node.getAttribute("inline") or "false" self.inline_limit = node.getAttribute("inline_limit") or 4 self.inline_header = node.getAttribute("inline_header") or "true" self.inline_alias = node.getAttribute("inline_alias") or "false" self.inline_limit = int(self.inline_limit) self.parseNode(node) else: self.show_empty = "false" self.inline = "false" self.inline_limit = 4 self.inline_header = "true" self.inline_alias = "false" self.order.append(["Merge", "menus"]) self.order.append(["Merge", "files"]) def parseNode(self, node): for child in node.childNodes: if child.nodeType == ELEMENT_NODE: if child.tagName == "Menuname": try: self.parseMenuname( child.childNodes[0].nodeValue, child.getAttribute("show_empty") or "false", child.getAttribute("inline") or "false", child.getAttribute("inline_limit") or 4, child.getAttribute("inline_header") or "true", child.getAttribute("inline_alias") or "false" ) except IndexError: raise ValidationError('Menuname cannot be empty', "") elif child.tagName == "Separator": self.parseSeparator() elif child.tagName == "Filename": try: self.parseFilename(child.childNodes[0].nodeValue) except IndexError: raise ValidationError('Filename cannot be empty', "") elif child.tagName == "Merge": self.parseMerge(child.getAttribute("type") or "all") def parseMenuname(self, value, empty="false", inline="false", inline_limit=4, inline_header="true", inline_alias="false"): self.order.append(["Menuname", value, empty, inline, inline_limit, inline_header, inline_alias]) self.order[-1][4] = int(self.order[-1][4]) def parseSeparator(self): self.order.append(["Separator"]) def parseFilename(self, value): self.order.append(["Filename", value]) def parseMerge(self, type="all"): self.order.append(["Merge", type]) class Rule: "Inlcude / Exclude Rules Class" def __init__(self, type, node=None): # Type is Include or Exclude self.Type = type # Rule is a python expression self.Rule = "" # Private attributes, only needed for parsing self.Depth = 0 self.Expr = [ "or" ] self.New = True # Begin parsing if node: self.parseNode(node) def __str__(self): return self.Rule def do(self, menuentries, type, run): for menuentry in menuentries: if run == 2 and ( menuentry.MatchedInclude == True \ or menuentry.Allocated == True ): continue elif eval(self.Rule): if type == "Include": menuentry.Add = True menuentry.MatchedInclude = True else: menuentry.Add = False return menuentries def parseNode(self, node): for child in node.childNodes: if child.nodeType == ELEMENT_NODE: if child.tagName == 'Filename': try: self.parseFilename(child.childNodes[0].nodeValue) except IndexError: raise ValidationError('Filename cannot be empty', "???") elif child.tagName == 'Category': try: self.parseCategory(child.childNodes[0].nodeValue) except IndexError: raise ValidationError('Category cannot be empty', "???") elif child.tagName == 'All': self.parseAll() elif child.tagName == 'And': self.parseAnd(child) elif child.tagName == 'Or': self.parseOr(child) elif child.tagName == 'Not': self.parseNot(child) def parseNew(self, set=True): if not self.New: self.Rule += " " + self.Expr[self.Depth] + " " if not set: self.New = True elif set: self.New = False def parseFilename(self, value): self.parseNew() self.Rule += "menuentry.DesktopFileID == '%s'" % value.strip().replace("\\", r"\\").replace("'", r"\'") def parseCategory(self, value): self.parseNew() self.Rule += "'%s' in menuentry.Categories" % value.strip() def parseAll(self): self.parseNew() self.Rule += "True" def parseAnd(self, node): self.parseNew(False) self.Rule += "(" self.Depth += 1 self.Expr.append("and") self.parseNode(node) self.Depth -= 1 self.Expr.pop() self.Rule += ")" def parseOr(self, node): self.parseNew(False) self.Rule += "(" self.Depth += 1 self.Expr.append("or") self.parseNode(node) self.Depth -= 1 self.Expr.pop() self.Rule += ")" def parseNot(self, node): self.parseNew(False) self.Rule += "not (" self.Depth += 1 self.Expr.append("or") self.parseNode(node) self.Depth -= 1 self.Expr.pop() self.Rule += ")" class MenuEntry: "Wrapper for 'Menu Style' Desktop Entries" def __init__(self, filename, dir="", prefix=""): # Create entry self.DesktopEntry = DesktopEntry(os.path.join(dir,filename)) self.setAttributes(filename, dir, prefix) # Can be one of Deleted/Hidden/Empty/NotShowIn/NoExec or True self.Show = True # Semi-Private self.Original = None self.Parents = [] # Private Stuff self.Allocated = False self.Add = False self.MatchedInclude = False # Caching self.Categories = self.DesktopEntry.getCategories() def save(self): """Save any changes to the desktop entry.""" if self.DesktopEntry.tainted == True: self.DesktopEntry.write() def getDir(self): """Return the directory containing the desktop entry file.""" return self.DesktopEntry.filename.replace(self.Filename, '') def getType(self): """Return the type of MenuEntry, System/User/Both""" if xdg.Config.root_mode == False: if self.Original: return "Both" elif xdg_data_dirs[0] in self.DesktopEntry.filename: return "User" else: return "System" else: return "User" def setAttributes(self, filename, dir="", prefix=""): self.Filename = filename self.Prefix = prefix self.DesktopFileID = os.path.join(prefix,filename).replace("/", "-") if not os.path.isabs(self.DesktopEntry.filename): self.__setFilename() def updateAttributes(self): if self.getType() == "System": self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) self.__setFilename() def __setFilename(self): if xdg.Config.root_mode == False: path = xdg_data_dirs[0] else: path= xdg_data_dirs[1] if self.DesktopEntry.getType() == "Application": dir = os.path.join(path, "applications") else: dir = os.path.join(path, "desktop-directories") self.DesktopEntry.filename = os.path.join(dir, self.Filename) def __cmp__(self, other): return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) def _key(self): """Key function for locale-aware sorting.""" return _strxfrm(self.DesktopEntry.getName()) def __lt__(self, other): try: other = other._key() except AttributeError: pass return self._key() < other def __eq__(self, other): if self.DesktopFileID == str(other): return True else: return False def __repr__(self): return self.DesktopFileID class Separator: "Just a dummy class for Separators" def __init__(self, parent): self.Parent = parent self.Show = True class Header: "Class for Inline Headers" def __init__(self, name, generic_name, comment): self.Name = name self.GenericName = generic_name self.Comment = comment def __str__(self): return self.Name tmp = {} def __getFileName(filename): dirs = xdg_config_dirs[:] if xdg.Config.root_mode == True: dirs.pop(0) for dir in dirs: menuname = os.path.join (dir, "menus" , filename) if os.path.isdir(dir) and os.path.isfile(menuname): return menuname def parse(filename=None): """Load an applications.menu file. filename : str, optional The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. """ # convert to absolute path if filename and not os.path.isabs(filename): filename = __getFileName(filename) # use default if no filename given if not filename: candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" filename = __getFileName(candidate) if not filename: raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) # check if it is a .menu file if not os.path.splitext(filename)[1] == ".menu": raise ParsingError('Not a .menu file', filename) # create xml parser try: doc = xml.dom.minidom.parse(filename) except xml.parsers.expat.ExpatError: raise ParsingError('Not a valid .menu file', filename) # parse menufile tmp["Root"] = "" tmp["mergeFiles"] = [] tmp["DirectoryDirs"] = [] tmp["cache"] = MenuEntryCache() __parse(doc, filename, tmp["Root"]) __parsemove(tmp["Root"]) __postparse(tmp["Root"]) tmp["Root"].Doc = doc tmp["Root"].Filename = filename # generate the menu __genmenuNotOnlyAllocated(tmp["Root"]) __genmenuOnlyAllocated(tmp["Root"]) # and finally sort sort(tmp["Root"]) return tmp["Root"] def __parse(node, filename, parent=None): for child in node.childNodes: if child.nodeType == ELEMENT_NODE: if child.tagName == 'Menu': __parseMenu(child, filename, parent) elif child.tagName == 'AppDir': try: __parseAppDir(child.childNodes[0].nodeValue, filename, parent) except IndexError: raise ValidationError('AppDir cannot be empty', filename) elif child.tagName == 'DefaultAppDirs': __parseDefaultAppDir(filename, parent) elif child.tagName == 'DirectoryDir': try: __parseDirectoryDir(child.childNodes[0].nodeValue, filename, parent) except IndexError: raise ValidationError('DirectoryDir cannot be empty', filename) elif child.tagName == 'DefaultDirectoryDirs': __parseDefaultDirectoryDir(filename, parent) elif child.tagName == 'Name' : try: parent.Name = child.childNodes[0].nodeValue except IndexError: raise ValidationError('Name cannot be empty', filename) elif child.tagName == 'Directory' : try: parent.Directories.append(child.childNodes[0].nodeValue) except IndexError: raise ValidationError('Directory cannot be empty', filename) elif child.tagName == 'OnlyUnallocated': parent.OnlyUnallocated = True elif child.tagName == 'NotOnlyUnallocated': parent.OnlyUnallocated = False elif child.tagName == 'Deleted': parent.Deleted = True elif child.tagName == 'NotDeleted': parent.Deleted = False elif child.tagName == 'Include' or child.tagName == 'Exclude': parent.Rules.append(Rule(child.tagName, child)) elif child.tagName == 'MergeFile': try: if child.getAttribute("type") == "parent": __parseMergeFile("applications.menu", child, filename, parent) else: __parseMergeFile(child.childNodes[0].nodeValue, child, filename, parent) except IndexError: raise ValidationError('MergeFile cannot be empty', filename) elif child.tagName == 'MergeDir': try: __parseMergeDir(child.childNodes[0].nodeValue, child, filename, parent) except IndexError: raise ValidationError('MergeDir cannot be empty', filename) elif child.tagName == 'DefaultMergeDirs': __parseDefaultMergeDirs(child, filename, parent) elif child.tagName == 'Move': parent.Moves.append(Move(child)) elif child.tagName == 'Layout': if len(child.childNodes) > 1: parent.Layout = Layout(child) elif child.tagName == 'DefaultLayout': if len(child.childNodes) > 1: parent.DefaultLayout = Layout(child) elif child.tagName == 'LegacyDir': try: __parseLegacyDir(child.childNodes[0].nodeValue, child.getAttribute("prefix"), filename, parent) except IndexError: raise ValidationError('LegacyDir cannot be empty', filename) elif child.tagName == 'KDELegacyDirs': __parseKDELegacyDirs(filename, parent) def __parsemove(menu): for submenu in menu.Submenus: __parsemove(submenu) # parse move operations for move in menu.Moves: move_from_menu = menu.getMenu(move.Old) if move_from_menu: move_to_menu = menu.getMenu(move.New) menus = move.New.split("/") oldparent = None while len(menus) > 0: if not oldparent: oldparent = menu newmenu = oldparent.getMenu(menus[0]) if not newmenu: newmenu = Menu() newmenu.Name = menus[0] if len(menus) > 1: newmenu.NotInXml = True oldparent.addSubmenu(newmenu) oldparent = newmenu menus.pop(0) newmenu += move_from_menu move_from_menu.Parent.Submenus.remove(move_from_menu) def __postparse(menu): # unallocated / deleted if menu.Deleted == "notset": menu.Deleted = False if menu.OnlyUnallocated == "notset": menu.OnlyUnallocated = False # Layout Tags if not menu.Layout or not menu.DefaultLayout: if menu.DefaultLayout: menu.Layout = menu.DefaultLayout elif menu.Layout: if menu.Depth > 0: menu.DefaultLayout = menu.Parent.DefaultLayout else: menu.DefaultLayout = Layout() else: if menu.Depth > 0: menu.Layout = menu.Parent.DefaultLayout menu.DefaultLayout = menu.Parent.DefaultLayout else: menu.Layout = Layout() menu.DefaultLayout = Layout() # add parent's app/directory dirs if menu.Depth > 0: menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs # remove duplicates menu.Directories = __removeDuplicates(menu.Directories) menu.DirectoryDirs = __removeDuplicates(menu.DirectoryDirs) menu.AppDirs = __removeDuplicates(menu.AppDirs) # go recursive through all menus for submenu in menu.Submenus: __postparse(submenu) # reverse so handling is easier menu.Directories.reverse() menu.DirectoryDirs.reverse() menu.AppDirs.reverse() # get the valid .directory file out of the list for directory in menu.Directories: for dir in menu.DirectoryDirs: if os.path.isfile(os.path.join(dir, directory)): menuentry = MenuEntry(directory, dir) if not menu.Directory: menu.Directory = menuentry elif menuentry.getType() == "System": if menu.Directory.getType() == "User": menu.Directory.Original = menuentry if menu.Directory: break # Menu parsing stuff def __parseMenu(child, filename, parent): m = Menu() __parse(child, filename, m) if parent: parent.addSubmenu(m) else: tmp["Root"] = m # helper function def __check(value, filename, type): path = os.path.dirname(filename) if not os.path.isabs(value): value = os.path.join(path, value) value = os.path.abspath(value) if type == "dir" and os.path.exists(value) and os.path.isdir(value): return value elif type == "file" and os.path.exists(value) and os.path.isfile(value): return value else: return False # App/Directory Dir Stuff def __parseAppDir(value, filename, parent): value = __check(value, filename, "dir") if value: parent.AppDirs.append(value) def __parseDefaultAppDir(filename, parent): for dir in reversed(xdg_data_dirs): __parseAppDir(os.path.join(dir, "applications"), filename, parent) def __parseDirectoryDir(value, filename, parent): value = __check(value, filename, "dir") if value: parent.DirectoryDirs.append(value) def __parseDefaultDirectoryDir(filename, parent): for dir in reversed(xdg_data_dirs): __parseDirectoryDir(os.path.join(dir, "desktop-directories"), filename, parent) # Merge Stuff def __parseMergeFile(value, child, filename, parent): if child.getAttribute("type") == "parent": for dir in xdg_config_dirs: rel_file = filename.replace(dir, "").strip("/") if rel_file != filename: for p in xdg_config_dirs: if dir == p: continue if os.path.isfile(os.path.join(p,rel_file)): __mergeFile(os.path.join(p,rel_file),child,parent) break else: value = __check(value, filename, "file") if value: __mergeFile(value, child, parent) def __parseMergeDir(value, child, filename, parent): value = __check(value, filename, "dir") if value: for item in os.listdir(value): try: if os.path.splitext(item)[1] == ".menu": __mergeFile(os.path.join(value, item), child, parent) except UnicodeDecodeError: continue def __parseDefaultMergeDirs(child, filename, parent): basename = os.path.splitext(os.path.basename(filename))[0] for dir in reversed(xdg_config_dirs): __parseMergeDir(os.path.join(dir, "menus", basename + "-merged"), child, filename, parent) def __mergeFile(filename, child, parent): # check for infinite loops if filename in tmp["mergeFiles"]: if debug: raise ParsingError('Infinite MergeFile loop detected', filename) else: return tmp["mergeFiles"].append(filename) # load file try: doc = xml.dom.minidom.parse(filename) except IOError: if debug: raise ParsingError('File not found', filename) else: return except xml.parsers.expat.ExpatError: if debug: raise ParsingError('Not a valid .menu file', filename) else: return # append file for child in doc.childNodes: if child.nodeType == ELEMENT_NODE: __parse(child,filename,parent) break # Legacy Dir Stuff def __parseLegacyDir(dir, prefix, filename, parent): m = __mergeLegacyDir(dir,prefix,filename,parent) if m: parent += m def __mergeLegacyDir(dir, prefix, filename, parent): dir = __check(dir,filename,"dir") if dir and dir not in tmp["DirectoryDirs"]: tmp["DirectoryDirs"].append(dir) m = Menu() m.AppDirs.append(dir) m.DirectoryDirs.append(dir) m.Name = os.path.basename(dir) m.NotInXml = True for item in os.listdir(dir): try: if item == ".directory": m.Directories.append(item) elif os.path.isdir(os.path.join(dir,item)): m.addSubmenu(__mergeLegacyDir(os.path.join(dir,item), prefix, filename, parent)) except UnicodeDecodeError: continue tmp["cache"].addMenuEntries([dir],prefix, True) menuentries = tmp["cache"].getMenuEntries([dir], False) for menuentry in menuentries: categories = menuentry.Categories if len(categories) == 0: r = Rule("Include") r.parseFilename(menuentry.DesktopFileID) m.Rules.append(r) if not dir in parent.AppDirs: categories.append("Legacy") menuentry.Categories = categories return m def __parseKDELegacyDirs(filename, parent): try: proc = subprocess.Popen(['kde-config', '--path', 'apps'], stdout=subprocess.PIPE, universal_newlines=True) output = proc.communicate()[0].splitlines() except OSError: # If kde-config doesn't exist, ignore this. return try: for dir in output[0].split(":"): __parseLegacyDir(dir,"kde", filename, parent) except IndexError: pass # remove duplicate entries from a list def __removeDuplicates(list): set = {} list.reverse() list = [set.setdefault(e,e) for e in list if e not in set] list.reverse() return list # Finally generate the menu def __genmenuNotOnlyAllocated(menu): for submenu in menu.Submenus: __genmenuNotOnlyAllocated(submenu) if menu.OnlyUnallocated == False: tmp["cache"].addMenuEntries(menu.AppDirs) menuentries = [] for rule in menu.Rules: menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 1) for menuentry in menuentries: if menuentry.Add == True: menuentry.Parents.append(menu) menuentry.Add = False menuentry.Allocated = True menu.MenuEntries.append(menuentry) def __genmenuOnlyAllocated(menu): for submenu in menu.Submenus: __genmenuOnlyAllocated(submenu) if menu.OnlyUnallocated == True: tmp["cache"].addMenuEntries(menu.AppDirs) menuentries = [] for rule in menu.Rules: menuentries = rule.do(tmp["cache"].getMenuEntries(menu.AppDirs), rule.Type, 2) for menuentry in menuentries: if menuentry.Add == True: menuentry.Parents.append(menu) # menuentry.Add = False # menuentry.Allocated = True menu.MenuEntries.append(menuentry) # And sorting ... def sort(menu): menu.Entries = [] menu.Visible = 0 for submenu in menu.Submenus: sort(submenu) tmp_s = [] tmp_e = [] for order in menu.Layout.order: if order[0] == "Filename": tmp_e.append(order[1]) elif order[0] == "Menuname": tmp_s.append(order[1]) for order in menu.Layout.order: if order[0] == "Separator": separator = Separator(menu) if len(menu.Entries) > 0 and isinstance(menu.Entries[-1], Separator): separator.Show = False menu.Entries.append(separator) elif order[0] == "Filename": menuentry = menu.getMenuEntry(order[1]) if menuentry: menu.Entries.append(menuentry) elif order[0] == "Menuname": submenu = menu.getMenu(order[1]) if submenu: __parse_inline(submenu, menu) elif order[0] == "Merge": if order[1] == "files" or order[1] == "all": menu.MenuEntries.sort() for menuentry in menu.MenuEntries: if menuentry not in tmp_e: menu.Entries.append(menuentry) elif order[1] == "menus" or order[1] == "all": menu.Submenus.sort() for submenu in menu.Submenus: if submenu.Name not in tmp_s: __parse_inline(submenu, menu) # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec for entry in menu.Entries: entry.Show = True menu.Visible += 1 if isinstance(entry, Menu): if entry.Deleted == True: entry.Show = "Deleted" menu.Visible -= 1 elif isinstance(entry.Directory, MenuEntry): if entry.Directory.DesktopEntry.getNoDisplay() == True: entry.Show = "NoDisplay" menu.Visible -= 1 elif entry.Directory.DesktopEntry.getHidden() == True: entry.Show = "Hidden" menu.Visible -= 1 elif isinstance(entry, MenuEntry): if entry.DesktopEntry.getNoDisplay() == True: entry.Show = "NoDisplay" menu.Visible -= 1 elif entry.DesktopEntry.getHidden() == True: entry.Show = "Hidden" menu.Visible -= 1 elif entry.DesktopEntry.getTryExec() and not __try_exec(entry.DesktopEntry.getTryExec()): entry.Show = "NoExec" menu.Visible -= 1 elif xdg.Config.windowmanager: if ( entry.DesktopEntry.getOnlyShowIn() != [] and xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) \ or xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn(): entry.Show = "NotShowIn" menu.Visible -= 1 elif isinstance(entry,Separator): menu.Visible -= 1 # remove separators at the beginning and at the end if len(menu.Entries) > 0: if isinstance(menu.Entries[0], Separator): menu.Entries[0].Show = False if len(menu.Entries) > 1: if isinstance(menu.Entries[-1], Separator): menu.Entries[-1].Show = False # show_empty tag for entry in menu.Entries[:]: if isinstance(entry, Menu) and entry.Layout.show_empty == "false" and entry.Visible == 0: entry.Show = "Empty" menu.Visible -= 1 if entry.NotInXml == True: menu.Entries.remove(entry) def __try_exec(executable): paths = os.environ['PATH'].split(os.pathsep) if not os.path.isfile(executable): for p in paths: f = os.path.join(p, executable) if os.path.isfile(f): if os.access(f, os.X_OK): return True else: if os.access(executable, os.X_OK): return True return False # inline tags def __parse_inline(submenu, menu): if submenu.Layout.inline == "true": if len(submenu.Entries) == 1 and submenu.Layout.inline_alias == "true": menuentry = submenu.Entries[0] menuentry.DesktopEntry.set("Name", submenu.getName(), locale = True) menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale = True) menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale = True) menu.Entries.append(menuentry) elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: if submenu.Layout.inline_header == "true": header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) menu.Entries.append(header) for entry in submenu.Entries: menu.Entries.append(entry) else: menu.Entries.append(submenu) else: menu.Entries.append(submenu) class MenuEntryCache: "Class to cache Desktop Entries" def __init__(self): self.cacheEntries = {} self.cacheEntries['legacy'] = [] self.cache = {} def addMenuEntries(self, dirs, prefix="", legacy=False): for dir in dirs: if not dir in self.cacheEntries: self.cacheEntries[dir] = [] self.__addFiles(dir, "", prefix, legacy) def __addFiles(self, dir, subdir, prefix, legacy): for item in os.listdir(os.path.join(dir,subdir)): if os.path.splitext(item)[1] == ".desktop": try: menuentry = MenuEntry(os.path.join(subdir,item), dir, prefix) except ParsingError: continue self.cacheEntries[dir].append(menuentry) if legacy == True: self.cacheEntries['legacy'].append(menuentry) elif os.path.isdir(os.path.join(dir,subdir,item)) and legacy == False: self.__addFiles(dir, os.path.join(subdir,item), prefix, legacy) def getMenuEntries(self, dirs, legacy=True): list = [] ids = [] # handle legacy items appdirs = dirs[:] if legacy == True: appdirs.append("legacy") # cache the results again key = "".join(appdirs) try: return self.cache[key] except KeyError: pass for dir in appdirs: for menuentry in self.cacheEntries[dir]: try: if menuentry.DesktopFileID not in ids: ids.append(menuentry.DesktopFileID) list.append(menuentry) elif menuentry.getType() == "System": # FIXME: This is only 99% correct, but still... i = list.index(menuentry) e = list[i] if e.getType() == "User": e.Original = menuentry except UnicodeDecodeError: continue self.cache[key] = list return list pyxdg-0.25/xdg/Config.py0000644000175000017500000000133012003334226015734 0ustar thomasthomas00000000000000""" Functions to configure Basic Settings """ language = "C" windowmanager = None icon_theme = "hicolor" icon_size = 48 cache_time = 5 root_mode = False def setWindowManager(wm): global windowmanager windowmanager = wm def setIconTheme(theme): global icon_theme icon_theme = theme import xdg.IconTheme xdg.IconTheme.themes = [] def setIconSize(size): global icon_size icon_size = size def setCacheTime(time): global cache_time cache_time = time def setLocale(lang): import locale lang = locale.normalize(lang) locale.setlocale(locale.LC_ALL, lang) import xdg.Locale xdg.Locale.update(lang) def setRootMode(boolean): global root_mode root_mode = boolean pyxdg-0.25/xdg/BaseDirectory.py0000664000175000017500000001222112060204416017271 0ustar thomasthomas00000000000000""" This module is based on a rox module (LGPL): http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log The freedesktop.org Base Directory specification provides a way for applications to locate shared data and configuration: http://standards.freedesktop.org/basedir-spec/ (based on version 0.6) This module can be used to load and save from and to these directories. Typical usage: from rox import basedir for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): print "Load settings from", dir dir = basedir.save_config_path('mydomain.org', 'MyProg') print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" Note: see the rox.Options module for a higher-level API for managing options. """ import os _home = os.path.expanduser('~') xdg_data_home = os.environ.get('XDG_DATA_HOME') or \ os.path.join(_home, '.local', 'share') xdg_data_dirs = [xdg_data_home] + \ (os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':') xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ os.path.join(_home, '.config') xdg_config_dirs = [xdg_config_home] + \ (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':') xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \ os.path.join(_home, '.cache') xdg_data_dirs = [x for x in xdg_data_dirs if x] xdg_config_dirs = [x for x in xdg_config_dirs if x] def save_config_path(*resource): """Ensure ``$XDG_CONFIG_HOME//`` exists, and return its path. 'resource' should normally be the name of your application. Use this when saving configuration settings. """ resource = os.path.join(*resource) assert not resource.startswith('/') path = os.path.join(xdg_config_home, resource) if not os.path.isdir(path): os.makedirs(path, 0o700) return path def save_data_path(*resource): """Ensure ``$XDG_DATA_HOME//`` exists, and return its path. 'resource' should normally be the name of your application or a shared resource. Use this when saving or updating application data. """ resource = os.path.join(*resource) assert not resource.startswith('/') path = os.path.join(xdg_data_home, resource) if not os.path.isdir(path): os.makedirs(path) return path def save_cache_path(*resource): """Ensure ``$XDG_CACHE_HOME//`` exists, and return its path. 'resource' should normally be the name of your application or a shared resource.""" resource = os.path.join(*resource) assert not resource.startswith('/') path = os.path.join(xdg_cache_home, resource) if not os.path.isdir(path): os.makedirs(path) return path def load_config_paths(*resource): """Returns an iterator which gives each directory named 'resource' in the configuration search path. Information provided by earlier directories should take precedence over later ones, and the user-specific config dir comes first.""" resource = os.path.join(*resource) for config_dir in xdg_config_dirs: path = os.path.join(config_dir, resource) if os.path.exists(path): yield path def load_first_config(*resource): """Returns the first result from load_config_paths, or None if there is nothing to load.""" for x in load_config_paths(*resource): return x return None def load_data_paths(*resource): """Returns an iterator which gives each directory named 'resource' in the application data search path. Information provided by earlier directories should take precedence over later ones.""" resource = os.path.join(*resource) for data_dir in xdg_data_dirs: path = os.path.join(data_dir, resource) if os.path.exists(path): yield path def get_runtime_dir(strict=True): """Returns the value of $XDG_RUNTIME_DIR, a directory path. This directory is intended for 'user-specific non-essential runtime files and other file objects (such as sockets, named pipes, ...)', and 'communication and synchronization purposes'. As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not set, with ``strict=True`` (the default), a KeyError is raised. With ``strict=False``, PyXDG will create a fallback under /tmp for the current user. This fallback does *not* provide the same guarantees as the specification requires for the runtime directory. The strict default is deliberately conservative, so that application developers can make a conscious decision to allow the fallback. """ try: return os.environ['XDG_RUNTIME_DIR'] except KeyError: if strict: raise import getpass fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser() try: os.mkdir(fallback, 0o700) except OSError as e: import errno if e.errno == errno.EEXIST: # Already exists - set 700 permissions again. import stat os.chmod(fallback, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) else: # pragma: no cover raise return fallback pyxdg-0.25/xdg/util.py0000664000175000017500000000024412004055672015517 0ustar thomasthomas00000000000000import sys PY3 = sys.version_info[0] >= 3 if PY3: def u(s): return s else: # Unicode-like literals def u(s): return s.decode('utf-8') pyxdg-0.25/xdg/DesktopEntry.py0000664000175000017500000004064112046047251017202 0ustar thomasthomas00000000000000""" Complete implementation of the XDG Desktop Entry Specification Version 0.9.4 http://standards.freedesktop.org/desktop-entry-spec/ Not supported: - Encoding: Legacy Mixed - Does not check exec parameters - Does not check URL's - Does not completly validate deprecated/kde items - Does not completly check categories """ from xdg.IniFile import IniFile, is_ascii import xdg.Locale from xdg.Exceptions import ParsingError import os.path import re import warnings class DesktopEntry(IniFile): "Class to parse and validate Desktop Entries" defaultGroup = 'Desktop Entry' def __init__(self, filename=None): """Create a new DesktopEntry If filename exists, it will be parsed as a desktop entry file. If not, or if filename is None, a blank DesktopEntry is created. """ self.content = dict() if filename and os.path.exists(filename): self.parse(filename) elif filename: self.new(filename) def __str__(self): return self.getName() def parse(self, file): """Parse a desktop entry file.""" IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) # start standard keys def getType(self): return self.get('Type') def getVersion(self): """deprecated, use getVersionString instead """ return self.get('Version', type="numeric") def getVersionString(self): return self.get('Version') def getName(self): return self.get('Name', locale=True) def getGenericName(self): return self.get('GenericName', locale=True) def getNoDisplay(self): return self.get('NoDisplay', type="boolean") def getComment(self): return self.get('Comment', locale=True) def getIcon(self): return self.get('Icon', locale=True) def getHidden(self): return self.get('Hidden', type="boolean") def getOnlyShowIn(self): return self.get('OnlyShowIn', list=True) def getNotShowIn(self): return self.get('NotShowIn', list=True) def getTryExec(self): return self.get('TryExec') def getExec(self): return self.get('Exec') def getPath(self): return self.get('Path') def getTerminal(self): return self.get('Terminal', type="boolean") def getMimeType(self): """deprecated, use getMimeTypes instead """ return self.get('MimeType', list=True, type="regex") def getMimeTypes(self): return self.get('MimeType', list=True) def getCategories(self): return self.get('Categories', list=True) def getStartupNotify(self): return self.get('StartupNotify', type="boolean") def getStartupWMClass(self): return self.get('StartupWMClass') def getURL(self): return self.get('URL') # end standard keys # start kde keys def getServiceTypes(self): return self.get('ServiceTypes', list=True) def getDocPath(self): return self.get('DocPath') def getKeywords(self): return self.get('Keywords', list=True, locale=True) def getInitialPreference(self): return self.get('InitialPreference') def getDev(self): return self.get('Dev') def getFSType(self): return self.get('FSType') def getMountPoint(self): return self.get('MountPoint') def getReadonly(self): return self.get('ReadOnly', type="boolean") def getUnmountIcon(self): return self.get('UnmountIcon', locale=True) # end kde keys # start deprecated keys def getMiniIcon(self): return self.get('MiniIcon', locale=True) def getTerminalOptions(self): return self.get('TerminalOptions') def getDefaultApp(self): return self.get('DefaultApp') def getProtocols(self): return self.get('Protocols', list=True) def getExtensions(self): return self.get('Extensions', list=True) def getBinaryPattern(self): return self.get('BinaryPattern') def getMapNotify(self): return self.get('MapNotify') def getEncoding(self): return self.get('Encoding') def getSwallowTitle(self): return self.get('SwallowTitle', locale=True) def getSwallowExec(self): return self.get('SwallowExec') def getSortOrder(self): return self.get('SortOrder', list=True) def getFilePattern(self): return self.get('FilePattern', type="regex") def getActions(self): return self.get('Actions', list=True) # end deprecated keys # desktop entry edit stuff def new(self, filename): """Make this instance into a new desktop entry. If filename has a .desktop extension, Type is set to Application. If it has a .directory extension, Type is Directory. """ if os.path.splitext(filename)[1] == ".desktop": type = "Application" elif os.path.splitext(filename)[1] == ".directory": type = "Directory" else: raise ParsingError("Unknown extension", filename) self.content = dict() self.addGroup(self.defaultGroup) self.set("Type", type) self.filename = filename # end desktop entry edit stuff # validation stuff def checkExtras(self): # header if self.defaultGroup == "KDE Desktop Entry": self.warnings.append('[KDE Desktop Entry]-Header is deprecated') # file extension if self.fileExtension == ".kdelnk": self.warnings.append("File extension .kdelnk is deprecated") elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": self.warnings.append('Unknown File extension') # Type try: self.type = self.content[self.defaultGroup]["Type"] except KeyError: self.errors.append("Key 'Type' is missing") # Name try: self.name = self.content[self.defaultGroup]["Name"] except KeyError: self.errors.append("Key 'Name' is missing") def checkGroup(self, group): # check if group header is valid if not (group == self.defaultGroup \ or re.match("^Desktop Action [a-zA-Z0-9\-]+$", group) \ or (re.match("^X-", group) and is_ascii(group))): self.errors.append("Invalid Group name: %s" % group) else: #OnlyShowIn and NotShowIn if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]): self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") def checkKey(self, key, value, group): # standard keys if key == "Type": if value == "ServiceType" or value == "Service" or value == "FSDevice": self.warnings.append("Type=%s is a KDE extension" % key) elif value == "MimeType": self.warnings.append("Type=MimeType is deprecated") elif not (value == "Application" or value == "Link" or value == "Directory"): self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) if self.fileExtension == ".directory" and not value == "Directory": self.warnings.append("File extension is .directory, but Type is '%s'" % value) elif self.fileExtension == ".desktop" and value == "Directory": self.warnings.append("Files with Type=Directory should have the extension .directory") if value == "Application": if "Exec" not in self.content[group]: self.warnings.append("Type=Application needs 'Exec' key") if value == "Link": if "URL" not in self.content[group]: self.warnings.append("Type=Link needs 'URL' key") elif key == "Version": self.checkValue(key, value) elif re.match("^Name"+xdg.Locale.regex+"$", key): pass # locale string elif re.match("^GenericName"+xdg.Locale.regex+"$", key): pass # locale string elif key == "NoDisplay": self.checkValue(key, value, type="boolean") elif re.match("^Comment"+xdg.Locale.regex+"$", key): pass # locale string elif re.match("^Icon"+xdg.Locale.regex+"$", key): self.checkValue(key, value) elif key == "Hidden": self.checkValue(key, value, type="boolean") elif key == "OnlyShowIn": self.checkValue(key, value, list=True) self.checkOnlyShowIn(value) elif key == "NotShowIn": self.checkValue(key, value, list=True) self.checkOnlyShowIn(value) elif key == "TryExec": self.checkValue(key, value) self.checkType(key, "Application") elif key == "Exec": self.checkValue(key, value) self.checkType(key, "Application") elif key == "Path": self.checkValue(key, value) self.checkType(key, "Application") elif key == "Terminal": self.checkValue(key, value, type="boolean") self.checkType(key, "Application") elif key == "Actions": self.checkValue(key, value, list=True) self.checkType(key, "Application") elif key == "MimeType": self.checkValue(key, value, list=True) self.checkType(key, "Application") elif key == "Categories": self.checkValue(key, value) self.checkType(key, "Application") self.checkCategories(value) elif re.match("^Keywords"+xdg.Locale.regex+"$", key): self.checkValue(key, value, type="localestring", list=True) self.checkType(key, "Application") elif key == "StartupNotify": self.checkValue(key, value, type="boolean") self.checkType(key, "Application") elif key == "StartupWMClass": self.checkType(key, "Application") elif key == "URL": self.checkValue(key, value) self.checkType(key, "URL") # kde extensions elif key == "ServiceTypes": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "DocPath": self.checkValue(key, value) self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "InitialPreference": self.checkValue(key, value, type="numeric") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "Dev": self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "FSType": self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "MountPoint": self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "ReadOnly": self.checkValue(key, value, type="boolean") self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) # deprecated keys elif key == "Encoding": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "TerminalOptions": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "DefaultApp": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "Protocols": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is deprecated" % key) elif key == "Extensions": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is deprecated" % key) elif key == "BinaryPattern": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "MapNotify": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): self.warnings.append("Key '%s' is deprecated" % key) elif key == "SwallowExec": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "FilePattern": self.checkValue(key, value, type="regex", list=True) self.warnings.append("Key '%s' is deprecated" % key) elif key == "SortOrder": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is deprecated" % key) # "X-" extensions elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) def checkType(self, key, type): if not self.getType() == type: self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) def checkOnlyShowIn(self, value): values = self.getList(value) valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity", "XFCE", "Old"] for item in values: if item not in valid and item[0:2] != "X-": self.errors.append("'%s' is not a registered OnlyShowIn value" % item); def checkCategories(self, value): values = self.getList(value) main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] if not any(item in main for item in values): self.errors.append("Missing main category") additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly'] allcategories = additional + main for item in values: if item not in allcategories and not item.startswith("X-"): self.errors.append("'%s' is not a registered Category" % item); def checkCategorie(self, value): """Deprecated alias for checkCategories - only exists for backwards compatibility. """ warnings.warn("checkCategorie is deprecated, use checkCategories", DeprecationWarning) return self.checkCategories(value) pyxdg-0.25/xdg/Exceptions.py0000644000175000017500000000276110762531024016666 0ustar thomasthomas00000000000000""" Exception Classes for the xdg package """ debug = False class Error(Exception): def __init__(self, msg): self.msg = msg Exception.__init__(self, msg) def __str__(self): return self.msg class ValidationError(Error): def __init__(self, msg, file): self.msg = msg self.file = file Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) class ParsingError(Error): def __init__(self, msg, file): self.msg = msg self.file = file Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) class NoKeyError(Error): def __init__(self, key, group, file): Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) self.key = key self.group = group class DuplicateKeyError(Error): def __init__(self, key, group, file): Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) self.key = key self.group = group class NoGroupError(Error): def __init__(self, group, file): Error.__init__(self, "No group: %s in file %s" % (group, file)) self.group = group class DuplicateGroupError(Error): def __init__(self, group, file): Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) self.group = group class NoThemeError(Error): def __init__(self, theme): Error.__init__(self, "No such icon-theme: %s" % theme) self.theme = theme pyxdg-0.25/xdg/IconTheme.py0000664000175000017500000003650312025321321016412 0ustar thomasthomas00000000000000""" Complete implementation of the XDG Icon Spec Version 0.8 http://standards.freedesktop.org/icon-theme-spec/ """ import os, time import re from xdg.IniFile import IniFile, is_ascii from xdg.BaseDirectory import xdg_data_dirs from xdg.Exceptions import NoThemeError, debug import xdg.Config class IconTheme(IniFile): "Class to parse and validate IconThemes" def __init__(self): IniFile.__init__(self) def __repr__(self): return self.name def parse(self, file): IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) self.dir = os.path.dirname(file) (nil, self.name) = os.path.split(self.dir) def getDir(self): return self.dir # Standard Keys def getName(self): return self.get('Name', locale=True) def getComment(self): return self.get('Comment', locale=True) def getInherits(self): return self.get('Inherits', list=True) def getDirectories(self): return self.get('Directories', list=True) def getHidden(self): return self.get('Hidden', type="boolean") def getExample(self): return self.get('Example') # Per Directory Keys def getSize(self, directory): return self.get('Size', type="integer", group=directory) def getContext(self, directory): return self.get('Context', group=directory) def getType(self, directory): value = self.get('Type', group=directory) if value: return value else: return "Threshold" def getMaxSize(self, directory): value = self.get('MaxSize', type="integer", group=directory) if value or value == 0: return value else: return self.getSize(directory) def getMinSize(self, directory): value = self.get('MinSize', type="integer", group=directory) if value or value == 0: return value else: return self.getSize(directory) def getThreshold(self, directory): value = self.get('Threshold', type="integer", group=directory) if value or value == 0: return value else: return 2 # validation stuff def checkExtras(self): # header if self.defaultGroup == "KDE Icon Theme": self.warnings.append('[KDE Icon Theme]-Header is deprecated') # file extension if self.fileExtension == ".theme": pass elif self.fileExtension == ".desktop": self.warnings.append('.desktop fileExtension is deprecated') else: self.warnings.append('Unknown File extension') # Check required keys # Name try: self.name = self.content[self.defaultGroup]["Name"] except KeyError: self.errors.append("Key 'Name' is missing") # Comment try: self.comment = self.content[self.defaultGroup]["Comment"] except KeyError: self.errors.append("Key 'Comment' is missing") # Directories try: self.directories = self.content[self.defaultGroup]["Directories"] except KeyError: self.errors.append("Key 'Directories' is missing") def checkGroup(self, group): # check if group header is valid if group == self.defaultGroup: try: self.name = self.content[group]["Name"] except KeyError: self.errors.append("Key 'Name' in Group '%s' is missing" % group) try: self.name = self.content[group]["Comment"] except KeyError: self.errors.append("Key 'Comment' in Group '%s' is missing" % group) elif group in self.getDirectories(): try: self.type = self.content[group]["Type"] except KeyError: self.type = "Threshold" try: self.name = self.content[group]["Size"] except KeyError: self.errors.append("Key 'Size' in Group '%s' is missing" % group) elif not (re.match("^\[X-", group) and is_ascii(group)): self.errors.append("Invalid Group name: %s" % group) def checkKey(self, key, value, group): # standard keys if group == self.defaultGroup: if re.match("^Name"+xdg.Locale.regex+"$", key): pass elif re.match("^Comment"+xdg.Locale.regex+"$", key): pass elif key == "Inherits": self.checkValue(key, value, list=True) elif key == "Directories": self.checkValue(key, value, list=True) elif key == "Hidden": self.checkValue(key, value, type="boolean") elif key == "Example": self.checkValue(key, value) elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) elif group in self.getDirectories(): if key == "Size": self.checkValue(key, value, type="integer") elif key == "Context": self.checkValue(key, value) elif key == "Type": self.checkValue(key, value) if value not in ["Fixed", "Scalable", "Threshold"]: self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) elif key == "MaxSize": self.checkValue(key, value, type="integer") if self.type != "Scalable": self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) elif key == "MinSize": self.checkValue(key, value, type="integer") if self.type != "Scalable": self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) elif key == "Threshold": self.checkValue(key, value, type="integer") if self.type != "Threshold": self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) class IconData(IniFile): "Class to parse and validate IconData Files" def __init__(self): IniFile.__init__(self) def __repr__(self): displayname = self.getDisplayName() if displayname: return "" % displayname else: return "" def parse(self, file): IniFile.parse(self, file, ["Icon Data"]) # Standard Keys def getDisplayName(self): """Retrieve the display name from the icon data, if one is specified.""" return self.get('DisplayName', locale=True) def getEmbeddedTextRectangle(self): """Retrieve the embedded text rectangle from the icon data as a list of numbers (x0, y0, x1, y1), if it is specified.""" return self.get('EmbeddedTextRectangle', type="integer", list=True) def getAttachPoints(self): """Retrieve the anchor points for overlays & emblems from the icon data, as a list of co-ordinate pairs, if they are specified.""" return self.get('AttachPoints', type="point", list=True) # validation stuff def checkExtras(self): # file extension if self.fileExtension != ".icon": self.warnings.append('Unknown File extension') def checkGroup(self, group): # check if group header is valid if not (group == self.defaultGroup \ or (re.match("^\[X-", group) and is_ascii(group))): self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) def checkKey(self, key, value, group): # standard keys if re.match("^DisplayName"+xdg.Locale.regex+"$", key): pass elif key == "EmbeddedTextRectangle": self.checkValue(key, value, type="integer", list=True) elif key == "AttachPoints": self.checkValue(key, value, type="point", list=True) elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) icondirs = [] for basedir in xdg_data_dirs: icondirs.append(os.path.join(basedir, "icons")) icondirs.append(os.path.join(basedir, "pixmaps")) icondirs.append(os.path.expanduser("~/.icons")) # just cache variables, they give a 10x speed improvement themes = [] theme_cache = {} dir_cache = {} icon_cache = {} def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): """Get the path to a specified icon. size : Icon size in pixels. Defaults to ``xdg.Config.icon_size``. theme : Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't found in the specified theme, it will be looked up in the basic 'hicolor' theme. extensions : List of preferred file extensions. Example:: >>> getIconPath("inkscape", 32) '/usr/share/icons/hicolor/32x32/apps/inkscape.png' """ global themes if size == None: size = xdg.Config.icon_size if theme == None: theme = xdg.Config.icon_theme # if we have an absolute path, just return it if os.path.isabs(iconname): return iconname # check if it has an extension and strip it if os.path.splitext(iconname)[1][1:] in extensions: iconname = os.path.splitext(iconname)[0] # parse theme files if (themes == []) or (themes[0].name != theme): themes = list(__get_themes(theme)) # more caching (icon looked up in the last 5 seconds?) tmp = (iconname, size, theme, tuple(extensions)) try: timestamp, icon = icon_cache[tmp] except KeyError: pass else: if (time.time() - timestamp) >= xdg.Config.cache_time: del icon_cache[tmp] else: return icon for thme in themes: icon = LookupIcon(iconname, size, thme, extensions) if icon: icon_cache[tmp] = (time.time(), icon) return icon # cache stuff again (directories looked up in the last 5 seconds?) for directory in icondirs: if (directory not in dir_cache \ or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \ and dir_cache[directory][2] < os.path.getmtime(directory))) \ and os.path.isdir(directory): dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory)) for dir, values in dir_cache.items(): for extension in extensions: try: if iconname + "." + extension in values[0]: icon = os.path.join(dir, iconname + "." + extension) icon_cache[tmp] = [time.time(), icon] return icon except UnicodeDecodeError as e: if debug: raise e else: pass # we haven't found anything? "hicolor" is our fallback if theme != "hicolor": icon = getIconPath(iconname, size, "hicolor") icon_cache[tmp] = [time.time(), icon] return icon def getIconData(path): """Retrieve the data from the .icon file corresponding to the given file. If there is no .icon file, it returns None. Example:: getIconData("/usr/share/icons/Tango/scalable/places/folder.svg") """ if os.path.isfile(path): icon_file = os.path.splitext(path)[0] + ".icon" if os.path.isfile(icon_file): data = IconData() data.parse(icon_file) return data def __get_themes(themename): """Generator yielding IconTheme objects for a specified theme and any themes from which it inherits. """ for dir in icondirs: theme_file = os.path.join(dir, themename, "index.theme") if os.path.isfile(theme_file): break theme_file = os.path.join(dir, themename, "index.desktop") if os.path.isfile(theme_file): break else: if debug: raise NoThemeError(themename) return theme = IconTheme() theme.parse(theme_file) yield theme for subtheme in theme.getInherits(): for t in __get_themes(subtheme): yield t def LookupIcon(iconname, size, theme, extensions): # look for the cache if theme.name not in theme_cache: theme_cache[theme.name] = [] theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup theme_cache[theme.name].append(0) # [1] mtime theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] # cache stuff (directory lookuped up the in the last 5 seconds?) if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time: theme_cache[theme.name][0] = time.time() for subdir in theme.getDirectories(): for directory in icondirs: dir = os.path.join(directory,theme.name,subdir) if (dir not in theme_cache[theme.name][2] \ or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ and subdir != "" \ and os.path.isdir(dir): theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)] theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) for dir, values in theme_cache[theme.name][2].items(): if DirectoryMatchesSize(values[0], size, theme): for extension in extensions: if iconname + "." + extension in values[1]: return os.path.join(dir, iconname + "." + extension) minimal_size = 2**31 closest_filename = "" for dir, values in theme_cache[theme.name][2].items(): distance = DirectorySizeDistance(values[0], size, theme) if distance < minimal_size: for extension in extensions: if iconname + "." + extension in values[1]: closest_filename = os.path.join(dir, iconname + "." + extension) minimal_size = distance return closest_filename def DirectoryMatchesSize(subdir, iconsize, theme): Type = theme.getType(subdir) Size = theme.getSize(subdir) Threshold = theme.getThreshold(subdir) MinSize = theme.getMinSize(subdir) MaxSize = theme.getMaxSize(subdir) if Type == "Fixed": return Size == iconsize elif Type == "Scaleable": return MinSize <= iconsize <= MaxSize elif Type == "Threshold": return Size - Threshold <= iconsize <= Size + Threshold def DirectorySizeDistance(subdir, iconsize, theme): Type = theme.getType(subdir) Size = theme.getSize(subdir) Threshold = theme.getThreshold(subdir) MinSize = theme.getMinSize(subdir) MaxSize = theme.getMaxSize(subdir) if Type == "Fixed": return abs(Size - iconsize) elif Type == "Scalable": if iconsize < MinSize: return MinSize - iconsize elif iconsize > MaxSize: return MaxSize - iconsize return 0 elif Type == "Threshold": if iconsize < Size - Threshold: return MinSize - iconsize elif iconsize > Size + Threshold: return iconsize - MaxSize return 0 pyxdg-0.25/xdg/RecentFiles.py0000664000175000017500000001377112025321321016744 0ustar thomasthomas00000000000000""" Implementation of the XDG Recent File Storage Specification Version 0.2 http://standards.freedesktop.org/recent-file-spec """ import xml.dom.minidom, xml.sax.saxutils import os, time, fcntl from xdg.Exceptions import ParsingError class RecentFiles: def __init__(self): self.RecentFiles = [] self.filename = "" def parse(self, filename=None): """Parse a list of recently used files. filename defaults to ``~/.recently-used``. """ if not filename: filename = os.path.join(os.getenv("HOME"), ".recently-used") try: doc = xml.dom.minidom.parse(filename) except IOError: raise ParsingError('File not found', filename) except xml.parsers.expat.ExpatError: raise ParsingError('Not a valid .menu file', filename) self.filename = filename for child in doc.childNodes: if child.nodeType == xml.dom.Node.ELEMENT_NODE: if child.tagName == "RecentFiles": for recent in child.childNodes: if recent.nodeType == xml.dom.Node.ELEMENT_NODE: if recent.tagName == "RecentItem": self.__parseRecentItem(recent) self.sort() def __parseRecentItem(self, item): recent = RecentFile() self.RecentFiles.append(recent) for attribute in item.childNodes: if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: if attribute.tagName == "URI": recent.URI = attribute.childNodes[0].nodeValue elif attribute.tagName == "Mime-Type": recent.MimeType = attribute.childNodes[0].nodeValue elif attribute.tagName == "Timestamp": recent.Timestamp = int(attribute.childNodes[0].nodeValue) elif attribute.tagName == "Private": recent.Prviate = True elif attribute.tagName == "Groups": for group in attribute.childNodes: if group.nodeType == xml.dom.Node.ELEMENT_NODE: if group.tagName == "Group": recent.Groups.append(group.childNodes[0].nodeValue) def write(self, filename=None): """Write the list of recently used files to disk. If the instance is already associated with a file, filename can be omitted to save it there again. """ if not filename and not self.filename: raise ParsingError('File not found', filename) elif not filename: filename = self.filename f = open(filename, "w") fcntl.lockf(f, fcntl.LOCK_EX) f.write('\n') f.write("\n") for r in self.RecentFiles: f.write(" \n") f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) f.write(" %s\n" % r.MimeType) f.write(" %s\n" % r.Timestamp) if r.Private == True: f.write(" \n") if len(r.Groups) > 0: f.write(" \n") for group in r.Groups: f.write(" %s\n" % group) f.write(" \n") f.write(" \n") f.write("\n") fcntl.lockf(f, fcntl.LOCK_UN) f.close() def getFiles(self, mimetypes=None, groups=None, limit=0): """Get a list of recently used files. The parameters can be used to filter by mime types, by group, or to limit the number of items returned. By default, the entire list is returned, except for items marked private. """ tmp = [] i = 0 for item in self.RecentFiles: if groups: for group in groups: if group in item.Groups: tmp.append(item) i += 1 elif mimetypes: for mimetype in mimetypes: if mimetype == item.MimeType: tmp.append(item) i += 1 else: if item.Private == False: tmp.append(item) i += 1 if limit != 0 and i == limit: break return tmp def addFile(self, item, mimetype, groups=None, private=False): """Add a recently used file. item should be the URI of the file, typically starting with ``file:///``. """ # check if entry already there if item in self.RecentFiles: index = self.RecentFiles.index(item) recent = self.RecentFiles[index] else: # delete if more then 500 files if len(self.RecentFiles) == 500: self.RecentFiles.pop() # add entry recent = RecentFile() self.RecentFiles.append(recent) recent.URI = item recent.MimeType = mimetype recent.Timestamp = int(time.time()) recent.Private = private if groups: recent.Groups = groups self.sort() def deleteFile(self, item): """Remove a recently used file, by URI, from the list. """ if item in self.RecentFiles: self.RecentFiles.remove(item) def sort(self): self.RecentFiles.sort() self.RecentFiles.reverse() class RecentFile: def __init__(self): self.URI = "" self.MimeType = "" self.Timestamp = "" self.Private = False self.Groups = [] def __cmp__(self, other): return cmp(self.Timestamp, other.Timestamp) def __lt__ (self, other): return self.Timestamp < other.Timestamp def __eq__(self, other): return self.URI == str(other) def __str__(self): return self.URI pyxdg-0.25/xdg/MenuEditor.py0000644000175000017500000004374110762531060016623 0ustar thomasthomas00000000000000""" CLass to edit XDG Menus """ from xdg.Menu import * from xdg.BaseDirectory import * from xdg.Exceptions import * from xdg.DesktopEntry import * from xdg.Config import * import xml.dom.minidom import os import re # XML-Cleanups: Move / Exclude # FIXME: proper reverte/delete # FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions # FIXME: catch Exceptions # FIXME: copy functions # FIXME: More Layout stuff # FIXME: unod/redo function / remove menu... # FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile # Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs class MenuEditor: def __init__(self, menu=None, filename=None, root=False): self.menu = None self.filename = None self.doc = None self.parse(menu, filename, root) # fix for creating two menus with the same name on the fly self.filenames = [] def parse(self, menu=None, filename=None, root=False): if root == True: setRootMode(True) if isinstance(menu, Menu): self.menu = menu elif menu: self.menu = parse(menu) else: self.menu = parse() if root == True: self.filename = self.menu.Filename elif filename: self.filename = filename else: self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) try: self.doc = xml.dom.minidom.parse(self.filename) except IOError: self.doc = xml.dom.minidom.parseString('Applications'+self.menu.Filename+'') except xml.parsers.expat.ExpatError: raise ParsingError('Not a valid .menu file', self.filename) self.__remove_whilespace_nodes(self.doc) def save(self): self.__saveEntries(self.menu) self.__saveMenu() def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): menuentry = MenuEntry(self.__getFileName(name, ".desktop")) menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) self.__addEntry(parent, menuentry, after, before) sort(self.menu) return menuentry def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): menu = Menu() menu.Parent = parent menu.Depth = parent.Depth + 1 menu.Layout = parent.DefaultLayout menu.DefaultLayout = parent.DefaultLayout menu = self.editMenu(menu, name, genericname, comment, icon) self.__addEntry(parent, menu, after, before) sort(self.menu) return menu def createSeparator(self, parent, after=None, before=None): separator = Separator(parent) self.__addEntry(parent, separator, after, before) sort(self.menu) return separator def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): self.__deleteEntry(oldparent, menuentry, after, before) self.__addEntry(newparent, menuentry, after, before) sort(self.menu) return menuentry def moveMenu(self, menu, oldparent, newparent, after=None, before=None): self.__deleteEntry(oldparent, menu, after, before) self.__addEntry(newparent, menu, after, before) root_menu = self.__getXmlMenu(self.menu.Name) if oldparent.getPath(True) != newparent.getPath(True): self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) sort(self.menu) return menu def moveSeparator(self, separator, parent, after=None, before=None): self.__deleteEntry(parent, separator, after, before) self.__addEntry(parent, separator, after, before) sort(self.menu) return separator def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): self.__addEntry(newparent, menuentry, after, before) sort(self.menu) return menuentry def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): deskentry = menuentry.DesktopEntry if name: if not deskentry.hasKey("Name"): deskentry.set("Name", name) deskentry.set("Name", name, locale = True) if comment: if not deskentry.hasKey("Comment"): deskentry.set("Comment", comment) deskentry.set("Comment", comment, locale = True) if genericname: if not deskentry.hasKey("GnericNe"): deskentry.set("GenericName", genericname) deskentry.set("GenericName", genericname, locale = True) if command: deskentry.set("Exec", command) if icon: deskentry.set("Icon", icon) if terminal == True: deskentry.set("Terminal", "true") elif terminal == False: deskentry.set("Terminal", "false") if nodisplay == True: deskentry.set("NoDisplay", "true") elif nodisplay == False: deskentry.set("NoDisplay", "false") if hidden == True: deskentry.set("Hidden", "true") elif hidden == False: deskentry.set("Hidden", "false") menuentry.updateAttributes() if len(menuentry.Parents) > 0: sort(self.menu) return menuentry def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): # Hack for legacy dirs if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": xml_menu = self.__getXmlMenu(menu.getPath(True, True)) self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") menu.Directory.setAttributes(menu.Name + ".directory") # Hack for New Entries elif not isinstance(menu.Directory, MenuEntry): if not name: name = menu.Name filename = self.__getFileName(name, ".directory").replace("/", "") if not menu.Name: menu.Name = filename.replace(".directory", "") xml_menu = self.__getXmlMenu(menu.getPath(True, True)) self.__addXmlTextElement(xml_menu, 'Directory', filename) menu.Directory = MenuEntry(filename) deskentry = menu.Directory.DesktopEntry if name: if not deskentry.hasKey("Name"): deskentry.set("Name", name) deskentry.set("Name", name, locale = True) if genericname: if not deskentry.hasKey("GenericName"): deskentry.set("GenericName", genericname) deskentry.set("GenericName", genericname, locale = True) if comment: if not deskentry.hasKey("Comment"): deskentry.set("Comment", comment) deskentry.set("Comment", comment, locale = True) if icon: deskentry.set("Icon", icon) if nodisplay == True: deskentry.set("NoDisplay", "true") elif nodisplay == False: deskentry.set("NoDisplay", "false") if hidden == True: deskentry.set("Hidden", "true") elif hidden == False: deskentry.set("Hidden", "false") menu.Directory.updateAttributes() if isinstance(menu.Parent, Menu): sort(self.menu) return menu def hideMenuEntry(self, menuentry): self.editMenuEntry(menuentry, nodisplay = True) def unhideMenuEntry(self, menuentry): self.editMenuEntry(menuentry, nodisplay = False, hidden = False) def hideMenu(self, menu): self.editMenu(menu, nodisplay = True) def unhideMenu(self, menu): self.editMenu(menu, nodisplay = False, hidden = False) xml_menu = self.__getXmlMenu(menu.getPath(True,True), False) for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu): node.parentNode.removeChild(node) def deleteMenuEntry(self, menuentry): if self.getAction(menuentry) == "delete": self.__deleteFile(menuentry.DesktopEntry.filename) for parent in menuentry.Parents: self.__deleteEntry(parent, menuentry) sort(self.menu) return menuentry def revertMenuEntry(self, menuentry): if self.getAction(menuentry) == "revert": self.__deleteFile(menuentry.DesktopEntry.filename) menuentry.Original.Parents = [] for parent in menuentry.Parents: index = parent.Entries.index(menuentry) parent.Entries[index] = menuentry.Original index = parent.MenuEntries.index(menuentry) parent.MenuEntries[index] = menuentry.Original menuentry.Original.Parents.append(parent) sort(self.menu) return menuentry def deleteMenu(self, menu): if self.getAction(menu) == "delete": self.__deleteFile(menu.Directory.DesktopEntry.filename) self.__deleteEntry(menu.Parent, menu) xml_menu = self.__getXmlMenu(menu.getPath(True, True)) xml_menu.parentNode.removeChild(xml_menu) sort(self.menu) return menu def revertMenu(self, menu): if self.getAction(menu) == "revert": self.__deleteFile(menu.Directory.DesktopEntry.filename) menu.Directory = menu.Directory.Original sort(self.menu) return menu def deleteSeparator(self, separator): self.__deleteEntry(separator.Parent, separator, after=True) sort(self.menu) return separator """ Private Stuff """ def getAction(self, entry): if isinstance(entry, Menu): if not isinstance(entry.Directory, MenuEntry): return "none" elif entry.Directory.getType() == "Both": return "revert" elif entry.Directory.getType() == "User" \ and (len(entry.Submenus) + len(entry.MenuEntries)) == 0: return "delete" elif isinstance(entry, MenuEntry): if entry.getType() == "Both": return "revert" elif entry.getType() == "User": return "delete" else: return "none" return "none" def __saveEntries(self, menu): if not menu: menu = self.menu if isinstance(menu.Directory, MenuEntry): menu.Directory.save() for entry in menu.getEntries(hidden=True): if isinstance(entry, MenuEntry): entry.save() elif isinstance(entry, Menu): self.__saveEntries(entry) def __saveMenu(self): if not os.path.isdir(os.path.dirname(self.filename)): os.makedirs(os.path.dirname(self.filename)) fd = open(self.filename, 'w') fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*\n', ''))) fd.close() def __getFileName(self, name, extension): postfix = 0 while 1: if postfix == 0: filename = name + extension else: filename = name + "-" + str(postfix) + extension if extension == ".desktop": dir = "applications" elif extension == ".directory": dir = "desktop-directories" if not filename in self.filenames and not \ os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)): self.filenames.append(filename) break else: postfix += 1 return filename def __getXmlMenu(self, path, create=True, element=None): if not element: element = self.doc if "/" in path: (name, path) = path.split("/", 1) else: name = path path = "" found = None for node in self.__getXmlNodesByName("Menu", element): for child in self.__getXmlNodesByName("Name", node): if child.childNodes[0].nodeValue == name: if path: found = self.__getXmlMenu(path, create, node) else: found = node break if found: break if not found and create == True: node = self.__addXmlMenuElement(element, name) if path: found = self.__getXmlMenu(path, create, node) else: found = node return found def __addXmlMenuElement(self, element, name): node = self.doc.createElement('Menu') self.__addXmlTextElement(node, 'Name', name) return element.appendChild(node) def __addXmlTextElement(self, element, name, text): node = self.doc.createElement(name) text = self.doc.createTextNode(text) node.appendChild(text) return element.appendChild(node) def __addXmlFilename(self, element, filename, type = "Include"): # remove old filenames for node in self.__getXmlNodesByName(["Include", "Exclude"], element): if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename: element.removeChild(node) # add new filename node = self.doc.createElement(type) node.appendChild(self.__addXmlTextElement(node, 'Filename', filename)) return element.appendChild(node) def __addXmlMove(self, element, old, new): node = self.doc.createElement("Move") node.appendChild(self.__addXmlTextElement(node, 'Old', old)) node.appendChild(self.__addXmlTextElement(node, 'New', new)) return element.appendChild(node) def __addXmlLayout(self, element, layout): # remove old layout for node in self.__getXmlNodesByName("Layout", element): element.removeChild(node) # add new layout node = self.doc.createElement("Layout") for order in layout.order: if order[0] == "Separator": child = self.doc.createElement("Separator") node.appendChild(child) elif order[0] == "Filename": child = self.__addXmlTextElement(node, "Filename", order[1]) elif order[0] == "Menuname": child = self.__addXmlTextElement(node, "Menuname", order[1]) elif order[0] == "Merge": child = self.doc.createElement("Merge") child.setAttribute("type", order[1]) node.appendChild(child) return element.appendChild(node) def __getXmlNodesByName(self, name, element): for child in element.childNodes: if child.nodeType == xml.dom.Node.ELEMENT_NODE and child.nodeName in name: yield child def __addLayout(self, parent): layout = Layout() layout.order = [] layout.show_empty = parent.Layout.show_empty layout.inline = parent.Layout.inline layout.inline_header = parent.Layout.inline_header layout.inline_alias = parent.Layout.inline_alias layout.inline_limit = parent.Layout.inline_limit layout.order.append(["Merge", "menus"]) for entry in parent.Entries: if isinstance(entry, Menu): layout.parseMenuname(entry.Name) elif isinstance(entry, MenuEntry): layout.parseFilename(entry.DesktopFileID) elif isinstance(entry, Separator): layout.parseSeparator() layout.order.append(["Merge", "files"]) parent.Layout = layout return layout def __addEntry(self, parent, entry, after=None, before=None): if after or before: if after: index = parent.Entries.index(after) + 1 elif before: index = parent.Entries.index(before) parent.Entries.insert(index, entry) else: parent.Entries.append(entry) xml_parent = self.__getXmlMenu(parent.getPath(True, True)) if isinstance(entry, MenuEntry): parent.MenuEntries.append(entry) entry.Parents.append(parent) self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") elif isinstance(entry, Menu): parent.addSubmenu(entry) if after or before: self.__addLayout(parent) self.__addXmlLayout(xml_parent, parent.Layout) def __deleteEntry(self, parent, entry, after=None, before=None): parent.Entries.remove(entry) xml_parent = self.__getXmlMenu(parent.getPath(True, True)) if isinstance(entry, MenuEntry): entry.Parents.remove(parent) parent.MenuEntries.remove(entry) self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") elif isinstance(entry, Menu): parent.Submenus.remove(entry) if after or before: self.__addLayout(parent) self.__addXmlLayout(xml_parent, parent.Layout) def __deleteFile(self, filename): try: os.remove(filename) except OSError: pass try: self.filenames.remove(filename) except ValueError: pass def __remove_whilespace_nodes(self, node): remove_list = [] for child in node.childNodes: if child.nodeType == xml.dom.minidom.Node.TEXT_NODE: child.data = child.data.strip() if not child.data.strip(): remove_list.append(child) elif child.hasChildNodes(): self.__remove_whilespace_nodes(child) for node in remove_list: node.parentNode.removeChild(node) pyxdg-0.25/xdg/Locale.py0000644000175000017500000000415710762531045015750 0ustar thomasthomas00000000000000""" Helper Module for Locale settings This module is based on a ROX module (LGPL): http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log """ import os from locale import normalize regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?" def _expand_lang(locale): locale = normalize(locale) COMPONENT_CODESET = 1 << 0 COMPONENT_MODIFIER = 1 << 1 COMPONENT_TERRITORY = 1 << 2 # split up the locale into its base components mask = 0 pos = locale.find('@') if pos >= 0: modifier = locale[pos:] locale = locale[:pos] mask |= COMPONENT_MODIFIER else: modifier = '' pos = locale.find('.') codeset = '' if pos >= 0: locale = locale[:pos] pos = locale.find('_') if pos >= 0: territory = locale[pos:] locale = locale[:pos] mask |= COMPONENT_TERRITORY else: territory = '' language = locale ret = [] for i in range(mask+1): if not (i & ~mask): # if all components for this combo exist ... val = language if i & COMPONENT_TERRITORY: val += territory if i & COMPONENT_CODESET: val += codeset if i & COMPONENT_MODIFIER: val += modifier ret.append(val) ret.reverse() return ret def expand_languages(languages=None): # Get some reasonable defaults for arguments that were not supplied if languages is None: languages = [] for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): val = os.environ.get(envar) if val: languages = val.split(':') break #if 'C' not in languages: # languages.append('C') # now normalize and expand the languages nelangs = [] for lang in languages: for nelang in _expand_lang(lang): if nelang not in nelangs: nelangs.append(nelang) return nelangs def update(language=None): global langs if language: langs = expand_languages([language]) else: langs = expand_languages() langs = [] update() pyxdg-0.25/xdg/__init__.py0000644000175000017500000000025312060213662016274 0ustar thomasthomas00000000000000__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] __version__ = "0.25" pyxdg-0.25/test/0000775000175000017500000000000012060214733014362 5ustar thomasthomas00000000000000pyxdg-0.25/test/test-icon.py0000664000175000017500000000304512025321321016634 0ustar thomasthomas00000000000000#!/usr/bin/python from xdg.IconTheme import IconTheme, getIconPath, getIconData import tempfile, shutil, os import unittest import resources class IconThemeTest(unittest.TestCase): def test_find_icon_exists(self): print("Finding an icon that probably exists:") print (getIconPath("firefox")) def test_find_icon_nonexistant(self): icon = getIconPath("oijeorjewrjkngjhbqefew") assert icon is None, "%r is not None" % icon def test_validate_icon_theme(self): theme = IconTheme() theme.parse("/usr/share/icons/hicolor/index.theme") theme.validate() class IconDataTest(unittest.TestCase): def test_read_icon_data(self): tmpdir = tempfile.mkdtemp() try: png_file = os.path.join(tmpdir, "test.png") with open(png_file, "wb") as f: f.write(resources.png_data) icon_file = os.path.join(tmpdir, "test.icon") with open(icon_file, "w") as f: f.write(resources.icon_data) icondata = getIconData(png_file) icondata.validate() self.assertEqual(icondata.getDisplayName(), 'Mime text/plain') self.assertEqual(icondata.getAttachPoints(), [(200,200), (800,200), (500,500), (200,800), (800,800)]) self.assertEqual(icondata.getEmbeddedTextRectangle(), [100,100,900,900]) assert " file:///home/thomas/foo/bar.ods application/vnd.oasis.opendocument.spreadsheet 1272385187 openoffice.org staroffice starsuite file:///tmp/2.ppt application/vnd.ms-powerpoint 1272378716 openoffice.org staroffice starsuite """ applications_menu = """ Applications X-GNOME-Menu-Applications.directory /etc/X11/applnk /usr/share/gnome/apps Accessories Utility.directory Utility Accessibility System Universal Access Utility-Accessibility.directory Accessibility Settings Development Development.directory Development emacs.desktop Education Education.directory Education Science Science GnomeScience.directory Education Science Games Game.directory Game ActionGame AdventureGame ArcadeGame BoardGame BlocksGame CardGame KidsGame LogicGame Simulation SportsGame StrategyGame Action ActionGames.directory ActionGame Adventure AdventureGames.directory AdventureGame Arcade ArcadeGames.directory ArcadeGame Board BoardGames.directory BoardGame Blocks BlocksGames.directory BlocksGame Cards CardGames.directory CardGame Kids KidsGames.directory KidsGame Logic LogicGames.directory LogicGame Role Playing RolePlayingGames.directory RolePlaying Simulation SimulationGames.directory Simulation Sports SportsGames.directory SportsGame Strategy StrategyGames.directory StrategyGame Graphics Graphics.directory Graphics Internet Network.directory Network Multimedia AudioVideo.directory AudioVideo Office Office.directory Office System System-Tools.directory System Settings Game Preferences Settings.directory Settings System X-GNOME-Settings-Panel Administration Settings-System.directory Settings System X-GNOME-Settings-Panel Other X-GNOME-Other.directory Core Screensaver X-GNOME-Settings-Panel Other Debian debian-menu.menu Debian.directory ubuntu-software-center.desktop ubuntu-software-center.desktop """ legacy_menu = """ Legacy legacy_dir """ kde_legacy_menu = """ KDE Legacy """ png_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\rIDAT\x08\x99c\xf8\x7f\x83\xe1?\x00\x07\x88\x02\xd7\xd9\n\xd8\xdc\x00\x00\x00\x00IEND\xaeB`\x82' icon_data = """[Icon Data] DisplayName=Mime text/plain EmbeddedTextRectangle=100,100,900,900 AttachPoints=200,200|800,200|500,500|200,800|800,800 """ pyxdg-0.25/test/test-recentfiles.py0000664000175000017500000000265312003334202020211 0ustar thomasthomas00000000000000from xdg import RecentFiles import resources import unittest import os.path import tempfile, shutil class RecentFilesTest(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.test_file = os.path.join(self.tmpdir, ".recently-used") with open(self.test_file, "w") as f: f.write(resources.recently_used) def tearDown(self): shutil.rmtree(self.tmpdir) def test_get_files(self): rf = RecentFiles.RecentFiles() rf.parse(self.test_file) last_file = rf.getFiles()[0] self.assertEqual(last_file.URI, "file:///home/thomas/foo/bar.ods") self.assertEqual(last_file.MimeType, "application/vnd.oasis.opendocument.spreadsheet") def test_modify(self): rf = RecentFiles.RecentFiles() rf.parse(self.test_file) rf.deleteFile("file:///home/thomas/foo/bar.ods") self.assertEqual(len(rf.RecentFiles), 1) rf.addFile("file:///home/thomas/foo/baz.png", "image/png") self.assertEqual(len(rf.RecentFiles), 2) new_file = os.path.join(self.tmpdir, ".new-recently-used") rf.write(new_file) rf2 = RecentFiles.RecentFiles() rf2.parse(new_file) last_file = rf.getFiles()[0] self.assertEqual(last_file.URI, "file:///home/thomas/foo/baz.png") self.assertEqual(last_file.MimeType, "image/png") pyxdg-0.25/test/test-locale.py0000664000175000017500000000030312002071111017127 0ustar thomasthomas00000000000000from xdg import Locale import unittest class LocaleTest(unittest.TestCase): def test_expand_languages(self): langs = Locale.expand_languages() assert isinstance(langs, list) pyxdg-0.25/test/test-inifile.py0000664000175000017500000000512212004064336017330 0ustar thomasthomas00000000000000#!/usr/bin/python # coding: utf-8 from xdg import IniFile from xdg.util import u import unittest class IniFileTest(unittest.TestCase): def test_check_string(self): i = IniFile.IniFile() self.assertEqual(i.checkString(u('abc')), 0) self.assertEqual(i.checkString('abc'), 0) self.assertEqual(i.checkString(u('abcö')), 1) self.assertEqual(i.checkString('abcö'), 1) def test_modify(self): i = IniFile.IniFile() i.addGroup('foo') i.set('bar', u('wallöby'), group='foo') self.assertEqual(i.get('bar', group='foo'), u('wallöby')) self.assertEqual(list(i.groups()), ['foo']) i.removeKey('bar', group='foo') i.removeGroup('foo') def test_value_types(self): i = IniFile.IniFile() i.addGroup('foo') i.defaultGroup = 'foo' # Numeric i.errors = [] i.set('num', '12.3') i.checkValue('num', '12.3', type='numeric') self.assertEqual(i.errors, []) i.checkValue('num', '12.a', type='numeric') self.assertEqual(len(i.errors), 1) self.assertEqual(i.get('num', type='numeric'), 12.3) # Regex i.errors = [] i.set('re', '[1-9]+') i.checkValue('re', '[1-9]+', type='regex') self.assertEqual(i.errors, []) i.checkValue('re', '[1-9+', type='regex') self.assertEqual(len(i.errors), 1) r = i.get('re', type='regex') assert r.match('123') # Point i.errors = [] i.set('pt', '3,12') i.checkValue('pt', '3,12', type='point') self.assertEqual(i.errors, []) i.checkValue('pt', '3,12,5', type='point') self.assertEqual(len(i.errors), 1) x,y = i.get('pt', type='point') # Boolean i.errors = [] i.warnings = [] i.set('boo', 'true') i.checkValue('boo', 'true', type='boolean') self.assertEqual(i.errors, []) i.checkValue('boo', '1', type='boolean') self.assertEqual(len(i.warnings), 1) self.assertEqual(i.errors, []) i.checkValue('boo', 'verily', type='boolean') self.assertEqual(len(i.errors), 1) boo = i.get('boo', type='boolean') assert boo is True, boo # Integer i.errors = [] i.set('int', '44') i.checkValue('int', '44', type='integer') self.assertEqual(i.errors, []) i.checkValue('int', 'A4', type='integer') self.assertEqual(len(i.errors), 1) self.assertEqual(i.get('int', type='integer'), 44) pyxdg-0.25/README0000644000175000017500000000136512003625062014264 0ustar thomasthomas00000000000000The XDG Package contains: - Implementation of the XDG-Base-Directory Standard http://standards.freedesktop.org/basedir-spec/ - Implementation of the XDG-Desktop Standard http://standards.freedesktop.org/desktop-entry-spec/ - Implementation of the XDG-Menu Standard http://standards.freedesktop.org/menu-spec/ - Implementation of the XDG-Icon-Theme Standard http://standards.freedesktop.org/icon-theme-spec/ - Implementation of the XDG-Shared MIME-info Database http://standards.freedesktop.org/shared-mime-info-spec/ - Implementation of the XDG-Recent File Storage Specification http://standards.freedesktop.org/recent-file-spec/ To run the tests, run nosetests in the top level directory. pyxdg-0.25/INSTALL0000644000175000017500000000005710315253674014444 0ustar thomasthomas00000000000000Quite easy, just run: python setup.py install pyxdg-0.25/AUTHORS0000664000175000017500000000022212025321321014440 0ustar thomasthomas00000000000000Current Maintainer: Thomas Kluyver Inactive: Heinrich Wendel Sergey Kuleshov pyxdg-0.25/COPYING0000644000175000017500000006130307763445223014455 0ustar thomasthomas00000000000000 GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! pyxdg-0.25/ChangeLog0000664000175000017500000002002612060214064015152 0ustar thomasthomas00000000000000Version 0.25 (December 2012) * Add support for $XDG_RUNTIME_DIR, Debian bug #656338. * Allow desktop entry files that are not encoded in UTF-8, Debian bug #693855. * Mime: Add support for subclasses and aliases. Version 0.24 (October 2012) * Update allowed DesktopEntry categories following changes to the specification. * Fix removal of empty submenu, freedesktop bug #54747. * Documentation is now available on RTD: http://pyxdg.readthedocs.org/ * A few more tests, and some code cleanup. * Fix failure to parse some menu files when kde-config is missing, freedesktop bug #56426. Version 0.23 (July 2012) * Fix a test for non-UTF-8 locales. Version 0.22 (July 2012) * Better unicode handling in several modules. * Fix for sorting non-ASCII menu entries, freedesktop bug #52492. * More tests. Version 0.21 (July 2012) * Tests can now be run conveniently using nosetests, and cover more of the code. * BaseDirectory: New save_cache_path() function, freedesktop bug #26458. * Config: Default icon theme is 'hicolor', not 'highcolor'. * Menu: Obsolete Rule.compile() method removed. * DesktopEntry: Corrected spelling of checkCategories() method, freedesktop bug #24974. * DesktopEntry: Consider Actions and Keywords keys standard. * DesktopEntry: Accept non-ASCII Keywords. * DesktopEntry: Update list of environments valid for OnlyShowIn. * Mime: Fix get_type_by_contents() in Python 3. * RecentFiles: Minor bug fixes. Version 0.20 (June 2012) * Compatible with Python 3; requires Python 2.6 or later * Clean up accidental GPL license notice in Menu.py * Add test scripts for xdg.Mime, xdg.Locale and xdg.RecentFiles * Fixes for icon theme validation * Fix exception in xdg.Mime * Replace invalid string exceptions * Fall back to default base directories if $XDG* environment variables are set but empty. * Remove use of deprecated os.popen3 in Menu.py * Correct URLs in README Version 0.19 * IniFile.py: add support for trusted desktop files (thanks to karl mikaelsson) * DesktopEntry.py: Support spec version 1.0, Debian bug #563660 * MimeType.py: Fix parsing of in memory data, Debian bug #563718 * DesktopEntry.py: Fix constructor, Debian bug #551297, #562951, #562952 Version 0.18 * DesktopEntry.py: Add getMimeTypes() method, correctly returning strings * DesktopEntry.py: Deprecated getMimeType() returning list of regex * Menu.py: Add support for XDG_MENU_PREFIX * Mime.py: Add get_type_by_contents() Version 0.17 2008-10-30 Heinrich Wendel * Menu.py: Python <= 2.3 compatibility fix * DesktopEntry.py: Fix wrong indention Version 0.16 2008-08-07 Heinrich Wendel * IconTheme.py: Add more directories to the pixmap path 2008-03-02 Heinrich Wendel * IniFile.py: Fix saving of relative filenames * IniFile.py, DesktopEntry.py: Fix __cmp__ method * IniFile.py, IconTheme.py: Better error handling Version 0.15 2005-08-10 Heinrich Wendel * Menu.py: Add support for TryExec 2005-08-09 Heinrich Wendel * Menu.py: Unicode bug fixed! * IconTheme.py: small speedup 2005-08-04 Heinrich Wendel * Menu.py, IconTheme.py: Bugfixes... * MenuEditor.py: Make xml nice; add hide/unhide functions Versinon 0.14 2005-06-02 Heinrich Wendel * Menu.py, MenuEditor.py: Bugfixes... version 0.13 2005-06-01 Heinrich Wendel * Menu.py, MenuEditor.py: Bugfixes... * Config.py: Add root_mode Version 0.12 2005-05-30 Heinrich Wendel * MenuEditor.py: New in this release, use to edit Menus thx to Travis Watkins and Matt Kynaston for their help * Menu.py, IniFile.py, DesktopEntry.py: Lot of bugfixing... * BaseDirectory.py: Add xdg_cache_home * IconTheme.py, Config.py: More caching stuff, make cachetime configurable Version 0.11 2005-05-23 Heinrich Wendel * DesktopEntry.p, Menu.py: A lot of bugfixes, thx to Travis Watkins 2005-05-02 Heinrich Wendel * Config.py: Module to configure Basic Settings, currently available: - Locale, IconTheme, IconSize, WindowManager * Locale.py: Internal Module to support Locales * Mime.py: Implementation of the Mime Specification * Menu.py: Now supports LegacyDirs * RecentFiles.py: Implementation of the Recent Files Specification Version 0.10 2005-04-26 Heinrich Wendel * Menu.py: various bug fixing to support version 1.0.draft-1 2005-04-13 Heinrich Wendel * IniFily.py: Detect if a .desktop file was edited * Menu.py Fix bug caused by excluding NoDisplay/Hidden Items to early Version 0.9 2005-03-23 Heinrich Wendel * IniFile.py: various speedups * Menu.py: add support for , menu-spec-0.91 2005-03-21 Heinrich Wendel * IniFily.py: Small fixes * Menu.py: remove __preparse and don't edit the parsed document, so menu editing is possible store parsed document in Menu.Doc store document name in Menu.Filename 2005-03-18 Heinrich Wendel * Menu.py: fix basename argument, thx to Matt Kynaston ; make it comply to menu-spec-0.9 2004-30-11 Heinrich Wendel * Update BaseDirectory.py to the current ROX version Version 0.8 2004-10-18 Ross Burton * xdg/DesktopEntry.py, xdg/IconTheme.py: Add . to the literal FileExtensions so that the checks work. * xdg/Menu.py: Don't read .desktop-* files, only .desktop 2004-10-18 Martin Grimme * xdg/IconTheme.py (getIconPath): The "hicolor" theme has to be used as the fallback. * xdg/IniFile.py (IniFile.getList): Fixed bug in splitting up strings. Version 0.7 2004-09-04 Heinrich Wendel * Add 'import codecs' to IniFile, needed by write support * Fix parsing of lists with only one entry Version 0.6 2004-08-04 Heinrich Wendel * Performance Improvements Version 0.5 2004-03-29 Heinrich Wendel * Finished Support for menu-spec 0.7 2004-03-27 Heinrich Wendel * 5* speed improvement in Menu.py parsing code 2004-03-20 Heinrich Wendel * check values of Categories/OnlyShowIn keys * various misc changes 2004-03-17 Martin Grimme * xdg/Menu.py (__preparse): * xdg/IconTheme.py (IconTheme.parse): Made compatible with Python 2.3 (None is a keyword). (__parseTheme): Prepend new icon themes to make sure that they have priority when looking up icons. (icondirs): Add "~/.icons" to the paths where to look for icons. Users may have icon themes installed in their home directory. 2003-10-08 Heinrich Wendel * Completed write-support in IniFile 2003-10-05 Heinrich Wendel * Added support for Hidden and NoDisplay in menu-spec * inital write-support in IniFile 2003-10-04 Heinrich Wendel * License change to LGPL-2 * initial support for menu-spec 0.7 Version 0.4 2003-09-30 Heinrich Wendel * Bugfix release Version 0.3 2003-09-12 Heinrich Wendel * Complete IconSpec implementation, including cache and validation 2003-09-07 Heinrich Wendel * Basedir spec converted to version 0.6 * First part of separating DesktopEntry backend in IniFile * added getPath(...) function to Menu.py Version 0.2 2003-09-05 Heinrich Wendel * Rewrite of menu-spec code * Taken basedir-spec code from ROX Version 0.1 2003-08-08 Heinrich Wendel * initial public release pyxdg-0.25/PKG-INFO0000664000175000017500000000110012060214733014470 0ustar thomasthomas00000000000000Metadata-Version: 1.1 Name: pyxdg Version: 0.25 Summary: PyXDG contains implementations of freedesktop.org standards in python. Home-page: http://freedesktop.org/wiki/Software/pyxdg Author: Freedesktop.org Author-email: xdg@lists.freedesktop.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2) Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Desktop Environment