Update pyxdg to 0.26

This commit is contained in:
Labrys of Knossos 2018-12-16 11:29:16 -05:00
commit 79011dbbc1
12 changed files with 1568 additions and 1144 deletions

View file

@ -25,7 +25,7 @@ Typical usage:
Note: see the rox.Options module for a higher-level API for managing options.
"""
import os
import os, stat
_home = os.path.expanduser('~')
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
@ -131,15 +131,30 @@ def get_runtime_dir(strict=True):
import getpass
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
create = False
try:
os.mkdir(fallback, 0o700)
# This must be a real directory, not a symlink, so attackers can't
# point it elsewhere. So we use lstat to check it.
st = os.lstat(fallback)
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
if e.errno == errno.ENOENT:
create = True
else:
raise
else:
# The fallback must be a directory
if not stat.S_ISDIR(st.st_mode):
os.unlink(fallback)
create = True
# Must be owned by the user and not accessible by anyone else
elif (st.st_uid != os.getuid()) \
or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)):
os.rmdir(fallback)
create = True
if create:
os.mkdir(fallback, 0o700)
return fallback

View file

@ -1,5 +1,5 @@
"""
Complete implementation of the XDG Desktop Entry Specification Version 0.9.4
Complete implementation of the XDG Desktop Entry Specification
http://standards.freedesktop.org/desktop-entry-spec/
Not supported:
@ -13,6 +13,7 @@ Not supported:
from xdg.IniFile import IniFile, is_ascii
import xdg.Locale
from xdg.Exceptions import ParsingError
from xdg.util import which
import os.path
import re
import warnings
@ -23,7 +24,7 @@ class DesktopEntry(IniFile):
defaultGroup = 'Desktop Entry'
def __init__(self, filename=None):
"""Create a new DesktopEntry
"""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.
@ -38,9 +39,23 @@ class DesktopEntry(IniFile):
return self.getName()
def parse(self, file):
"""Parse a desktop entry file."""
"""Parse a desktop entry file.
This can raise :class:`~xdg.Exceptions.ParsingError`,
:class:`~xdg.Exceptions.DuplicateGroupError` or
:class:`~xdg.Exceptions.DuplicateKeyError`.
"""
IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"])
def findTryExec(self):
"""Looks in the PATH for the executable given in the TryExec field.
Returns the full path to the executable if it is found, None if not.
Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present.
"""
tryexec = self.get('TryExec', strict=True)
return which(tryexec)
# start standard keys
def getType(self):
return self.get('Type')
@ -140,10 +155,11 @@ class DesktopEntry(IniFile):
# desktop entry edit stuff
def new(self, filename):
"""Make this instance into a new desktop entry.
"""Make this instance into a new, blank desktop entry.
If filename has a .desktop extension, Type is set to Application. If it
has a .directory extension, Type is Directory.
has a .directory extension, Type is Directory. Other extensions will
cause :class:`~xdg.Exceptions.ParsingError` to be raised.
"""
if os.path.splitext(filename)[1] == ".desktop":
type = "Application"
@ -185,7 +201,7 @@ class DesktopEntry(IniFile):
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("^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:

View file

@ -5,6 +5,7 @@ Exception Classes for the xdg package
debug = False
class Error(Exception):
"""Base class for exceptions defined here."""
def __init__(self, msg):
self.msg = msg
Exception.__init__(self, msg)
@ -12,40 +13,72 @@ class Error(Exception):
return self.msg
class ValidationError(Error):
"""Raised when a file fails to validate.
The filename is the .file attribute.
"""
def __init__(self, msg, file):
self.msg = msg
self.file = file
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
class ParsingError(Error):
"""Raised when a file cannot be parsed.
The filename is the .file attribute.
"""
def __init__(self, msg, file):
self.msg = msg
self.file = file
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
class NoKeyError(Error):
"""Raised when trying to access a nonexistant key in an INI-style file.
Attributes are .key, .group and .file.
"""
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
self.file = file
class DuplicateKeyError(Error):
"""Raised when the same key occurs twice in an INI-style file.
Attributes are .key, .group and .file.
"""
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
self.file = file
class NoGroupError(Error):
"""Raised when trying to access a nonexistant group in an INI-style file.
Attributes are .group and .file.
"""
def __init__(self, group, file):
Error.__init__(self, "No group: %s in file %s" % (group, file))
self.group = group
self.file = file
class DuplicateGroupError(Error):
"""Raised when the same key occurs twice in an INI-style file.
Attributes are .group and .file.
"""
def __init__(self, group, file):
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
self.group = group
self.file = file
class NoThemeError(Error):
"""Raised when trying to access a nonexistant icon theme.
The name of the theme is the .theme attribute.
"""
def __init__(self, theme):
Error.__init__(self, "No such icon-theme: %s" % theme)
self.theme = theme

View file

@ -1,5 +1,5 @@
"""
Complete implementation of the XDG Icon Spec Version 0.8
Complete implementation of the XDG Icon Spec
http://standards.freedesktop.org/icon-theme-spec/
"""
@ -37,6 +37,8 @@ class IconTheme(IniFile):
return self.get('Inherits', list=True)
def getDirectories(self):
return self.get('Directories', list=True)
def getScaledDirectories(self):
return self.get('ScaledDirectories', list=True)
def getHidden(self):
return self.get('Hidden', type="boolean")
def getExample(self):
@ -72,6 +74,10 @@ class IconTheme(IniFile):
else:
return 2
def getScale(self, directory):
value = self.get('Scale', type="integer", group=directory)
return value or 1
# validation stuff
def checkExtras(self):
# header
@ -125,7 +131,7 @@ class IconTheme(IniFile):
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)):
elif not (re.match(r"^\[X-", group) and is_ascii(group)):
self.errors.append("Invalid Group name: %s" % group)
def checkKey(self, key, value, group):
@ -139,6 +145,8 @@ class IconTheme(IniFile):
self.checkValue(key, value, list=True)
elif key == "Directories":
self.checkValue(key, value, list=True)
elif key == "ScaledDirectories":
self.checkValue(key, value, list=True)
elif key == "Hidden":
self.checkValue(key, value, type="boolean")
elif key == "Example":
@ -168,6 +176,8 @@ class IconTheme(IniFile):
self.checkValue(key, value, type="integer")
if self.type != "Threshold":
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type)
elif key == "Scale":
self.checkValue(key, value, type="integer")
elif re.match("^X-[a-zA-Z0-9-]+", key):
pass
else:
@ -211,7 +221,7 @@ class IconData(IniFile):
def checkGroup(self, group):
# check if group header is valid
if not (group == self.defaultGroup \
or (re.match("^\[X-", group) and is_ascii(group))):
or (re.match(r"^\[X-", group) and is_ascii(group))):
self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
def checkKey(self, key, value, group):

View file

@ -102,7 +102,7 @@ class IniFile:
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):
def get(self, key, group=None, locale=False, type="string", list=False, strict=False):
# set default group
if not group:
group = self.defaultGroup
@ -114,7 +114,7 @@ class IniFile:
else:
value = self.content[group][key]
else:
if debug:
if strict or debug:
if group not in self.content:
raise NoGroupError(group, self.filename)
elif key not in self.content[group]:
@ -192,8 +192,8 @@ class IniFile:
# start validation stuff
def validate(self, report="All"):
"""Validate the contents, raising ``ValidationError`` if there
is anything amiss.
"""Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
if there is anything amiss.
report can be 'All' / 'Warnings' / 'Errors'
"""

View file

@ -9,7 +9,7 @@ http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&vi
import os
from locale import normalize
regex = "(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z\-0-9]+)?(@[a-zA-Z]+)?\])?"
regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?"
def _expand_lang(locale):
locale = normalize(locale)

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
""" 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
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
from xdg.Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder
from xdg.BaseDirectory import xdg_config_dirs, xdg_data_dirs
from xdg.Exceptions import ParsingError
from xdg.Config import setRootMode
# XML-Cleanups: Move / Exclude
# FIXME: proper reverte/delete
@ -20,28 +20,31 @@ import re
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
class MenuEditor:
class MenuEditor(object):
def __init__(self, menu=None, filename=None, root=False):
self.menu = None
self.filename = None
self.doc = None
self.tree = None
self.parser = XMLMenuBuilder()
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:
if root:
setRootMode(True)
if isinstance(menu, Menu):
self.menu = menu
elif menu:
self.menu = parse(menu)
self.menu = self.parser.parse(menu)
else:
self.menu = parse()
self.menu = self.parser.parse()
if root == True:
if root:
self.filename = self.menu.Filename
elif filename:
self.filename = filename
@ -49,13 +52,21 @@ class MenuEditor:
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)
self.tree = etree.parse(self.filename)
except IOError:
self.doc = xml.dom.minidom.parseString('<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd"><Menu><Name>Applications</Name><MergeFile type="parent">'+self.menu.Filename+'</MergeFile></Menu>')
except xml.parsers.expat.ExpatError:
root = etree.fromtring("""
<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://standards.freedesktop.org/menu-spec/menu-1.0.dtd">
<Menu>
<Name>Applications</Name>
<MergeFile type="parent">%s</MergeFile>
</Menu>
""" % self.menu.Filename)
self.tree = etree.ElementTree(root)
except ParsingError:
raise ParsingError('Not a valid .menu file', self.filename)
self.__remove_whilespace_nodes(self.doc)
#FIXME: is this needed with etree ?
self.__remove_whitespace_nodes(self.tree)
def save(self):
self.__saveEntries(self.menu)
@ -67,7 +78,7 @@ class MenuEditor:
self.__addEntry(parent, menuentry, after, before)
sort(self.menu)
self.menu.sort()
return menuentry
@ -83,7 +94,7 @@ class MenuEditor:
self.__addEntry(parent, menu, after, before)
sort(self.menu)
self.menu.sort()
return menu
@ -92,7 +103,7 @@ class MenuEditor:
self.__addEntry(parent, separator, after, before)
sort(self.menu)
self.menu.sort()
return separator
@ -100,7 +111,7 @@ class MenuEditor:
self.__deleteEntry(oldparent, menuentry, after, before)
self.__addEntry(newparent, menuentry, after, before)
sort(self.menu)
self.menu.sort()
return menuentry
@ -112,7 +123,7 @@ class MenuEditor:
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)
self.menu.sort()
return menu
@ -120,14 +131,14 @@ class MenuEditor:
self.__deleteEntry(parent, separator, after, before)
self.__addEntry(parent, separator, after, before)
sort(self.menu)
self.menu.sort()
return separator
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__addEntry(newparent, menuentry, after, before)
sort(self.menu)
self.menu.sort()
return menuentry
@ -137,39 +148,39 @@ class MenuEditor:
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
deskentry.set("Name", name, locale = True)
deskentry.set("Name", name, locale=True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale = True)
deskentry.set("Comment", comment, locale=True)
if genericname:
if not deskentry.hasKey("GnericNe"):
if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale = True)
deskentry.set("GenericName", genericname, locale=True)
if command:
deskentry.set("Exec", command)
if icon:
deskentry.set("Icon", icon)
if terminal == True:
if terminal:
deskentry.set("Terminal", "true")
elif terminal == False:
elif not terminal:
deskentry.set("Terminal", "false")
if nodisplay == True:
if nodisplay is True:
deskentry.set("NoDisplay", "true")
elif nodisplay == False:
elif nodisplay is False:
deskentry.set("NoDisplay", "false")
if hidden == True:
if hidden is True:
deskentry.set("Hidden", "true")
elif hidden == False:
elif hidden is False:
deskentry.set("Hidden", "false")
menuentry.updateAttributes()
if len(menuentry.Parents) > 0:
sort(self.menu)
self.menu.sort()
return menuentry
@ -195,56 +206,58 @@ class MenuEditor:
if name:
if not deskentry.hasKey("Name"):
deskentry.set("Name", name)
deskentry.set("Name", name, locale = True)
deskentry.set("Name", name, locale=True)
if genericname:
if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale = True)
deskentry.set("GenericName", genericname, locale=True)
if comment:
if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale = True)
deskentry.set("Comment", comment, locale=True)
if icon:
deskentry.set("Icon", icon)
if nodisplay == True:
if nodisplay is True:
deskentry.set("NoDisplay", "true")
elif nodisplay == False:
elif nodisplay is False:
deskentry.set("NoDisplay", "false")
if hidden == True:
if hidden is True:
deskentry.set("Hidden", "true")
elif hidden == False:
elif hidden is False:
deskentry.set("Hidden", "false")
menu.Directory.updateAttributes()
if isinstance(menu.Parent, Menu):
sort(self.menu)
self.menu.sort()
return menu
def hideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay = True)
self.editMenuEntry(menuentry, nodisplay=True)
def unhideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay = False, hidden = False)
self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
def hideMenu(self, menu):
self.editMenu(menu, nodisplay = True)
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)
self.editMenu(menu, nodisplay=False, hidden=False)
xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
deleted = xml_menu.findall('Deleted')
not_deleted = xml_menu.findall('NotDeleted')
for node in deleted + not_deleted:
xml_menu.remove(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)
self.menu.sort()
return menuentry
def revertMenuEntry(self, menuentry):
@ -257,7 +270,7 @@ class MenuEditor:
index = parent.MenuEntries.index(menuentry)
parent.MenuEntries[index] = menuentry.Original
menuentry.Original.Parents.append(parent)
sort(self.menu)
self.menu.sort()
return menuentry
def deleteMenu(self, menu):
@ -265,21 +278,22 @@ class MenuEditor:
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)
parent = self.__get_parent_node(xml_menu)
parent.remove(xml_menu)
self.menu.sort()
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)
self.menu.sort()
return menu
def deleteSeparator(self, separator):
self.__deleteEntry(separator.Parent, separator, after=True)
sort(self.menu)
self.menu.sort()
return separator
@ -290,8 +304,9 @@ class MenuEditor:
return "none"
elif entry.Directory.getType() == "Both":
return "revert"
elif entry.Directory.getType() == "User" \
and (len(entry.Submenus) + len(entry.MenuEntries)) == 0:
elif entry.Directory.getType() == "User" and (
len(entry.Submenus) + len(entry.MenuEntries)
) == 0:
return "delete"
elif isinstance(entry, MenuEntry):
@ -318,9 +333,7 @@ class MenuEditor:
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]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
fd.close()
self.tree.write(self.filename, encoding='utf-8')
def __getFileName(self, name, extension):
postfix = 0
@ -333,8 +346,9 @@ class MenuEditor:
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)):
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:
@ -343,8 +357,11 @@ class MenuEditor:
return filename
def __getXmlMenu(self, path, create=True, element=None):
# FIXME: we should also return the menu's parent,
# to avoid looking for it later on
# @see Element.getiterator()
if not element:
element = self.doc
element = self.tree
if "/" in path:
(name, path) = path.split("/", 1)
@ -353,17 +370,16 @@ class MenuEditor:
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
for node in element.findall("Menu"):
name_node = node.find('Name')
if name_node.text == name:
if path:
found = self.__getXmlMenu(path, create, node)
else:
found = node
if found:
break
if not found and create == True:
if not found and create:
node = self.__addXmlMenuElement(element, name)
if path:
found = self.__getXmlMenu(path, create, node)
@ -373,58 +389,62 @@ class MenuEditor:
return found
def __addXmlMenuElement(self, element, name):
node = self.doc.createElement('Menu')
self.__addXmlTextElement(node, 'Name', name)
return element.appendChild(node)
menu_node = etree.SubElement('Menu', element)
name_node = etree.SubElement('Name', menu_node)
name_node.text = name
return menu_node
def __addXmlTextElement(self, element, name, text):
node = self.doc.createElement(name)
text = self.doc.createTextNode(text)
node.appendChild(text)
return element.appendChild(node)
node = etree.SubElement(name, element)
node.text = text
return node
def __addXmlFilename(self, element, filename, type = "Include"):
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)
includes = element.findall('Include')
excludes = element.findall('Exclude')
rules = includes + excludes
for rule in rules:
#FIXME: this finds only Rules whose FIRST child is a Filename element
if rule[0].tag == "Filename" and rule[0].text == filename:
element.remove(rule)
# shouldn't it remove all occurences, like the following:
#filename_nodes = rule.findall('.//Filename'):
#for fn in filename_nodes:
#if fn.text == filename:
##element.remove(rule)
#parent = self.__get_parent_node(fn)
#parent.remove(fn)
# add new filename
node = self.doc.createElement(type)
node.appendChild(self.__addXmlTextElement(node, 'Filename', filename))
return element.appendChild(node)
node = etree.SubElement(type_, element)
self.__addXmlTextElement(node, 'Filename', filename)
return 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)
node = etree.SubElement("Move", element)
self.__addXmlTextElement(node, 'Old', old)
self.__addXmlTextElement(node, 'New', new)
return node
def __addXmlLayout(self, element, layout):
# remove old layout
for node in self.__getXmlNodesByName("Layout", element):
element.removeChild(node)
for node in element.findall("Layout"):
element.remove(node)
# add new layout
node = self.doc.createElement("Layout")
node = etree.SubElement("Layout", element)
for order in layout.order:
if order[0] == "Separator":
child = self.doc.createElement("Separator")
node.appendChild(child)
child = etree.SubElement("Separator", node)
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
child = etree.SubElement("Merge", node)
child.attrib["type"] = order[1]
return node
def __addLayout(self, parent):
layout = Layout()
@ -498,14 +518,24 @@ class MenuEditor:
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():
def __remove_whitespace_nodes(self, node):
for child in node:
text = child.text.strip()
if not text:
child.text = ''
tail = child.tail.strip()
if not tail:
child.tail = ''
if len(child):
self.__remove_whilespace_nodes(child)
for node in remove_list:
node.parentNode.removeChild(node)
def __get_parent_node(self, node):
# elements in ElementTree doesn't hold a reference to their parent
for parent, child in self.__iter_parent():
if child is node:
return child
def __iter_parent(self):
for parent in self.tree.getiterator():
for child in parent:
yield parent, child

View file

@ -20,6 +20,7 @@ information about the format of these files.
"""
import os
import re
import stat
import sys
import fnmatch
@ -46,25 +47,42 @@ def _get_node_data(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.
"""Get the MIMEtype object for the given type.
This remains for backwards compatibility; calling MIMEtype now does
the same thing.
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)]
return MIMEtype(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
class MIMEtype(object):
"""Class holding data about a MIME type.
Calling the class will return a cached instance, so there is only one
instance for each MIME type. The name can either be passed as one part
('text/plain'), or as two ('text', 'plain').
"""
def __new__(cls, media, subtype=None):
if subtype is None and '/' in media:
media, subtype = media.split('/', 1)
assert '/' not in subtype
media = media.lower()
subtype = subtype.lower()
try:
return types[(media, subtype)]
except KeyError:
mtype = super(MIMEtype, cls).__new__(cls)
mtype._init(media, subtype)
types[(media, subtype)] = mtype
return mtype
# If this is done in __init__, it is automatically called again each time
# the MIMEtype is returned by __new__, which we don't want. So we call it
# explicitly only when we construct a new instance.
def _init(self, media, subtype):
self.media = media
self.subtype = subtype
self._comment = None
@ -109,100 +127,106 @@ class MIMEtype:
return self.media + '/' + self.subtype
def __repr__(self):
return '<%s: %s>' % (self, self._comment or '(comment not loaded)')
return 'MIMEtype(%r, %r)' % (self.media, self.subtype)
def __hash__(self):
return hash(self.media) ^ hash(self.subtype)
class UnknownMagicRuleFormat(ValueError):
pass
class DiscardMagicRules(Exception):
"Raised when __NOMAGIC__ is found, and caught to discard previous rules."
pass
class MagicRule:
def __init__(self, f):
self.next=None
self.prev=None
also = None
def __init__(self, start, value, mask, word, range):
self.start = start
self.value = value
self.mask = mask
self.word = word
self.range = range
rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$')
@classmethod
def from_file(cls, f):
"""Read a rule from the binary magics file. Returns a 2-tuple of
the nesting depth and the MagicRule."""
line = f.readline()
#print line
ind=b''
while True:
c=f.read(1)
if c == b'>':
break
ind+=c
if not ind:
self.nest=0
# [indent] '>'
nest_depth, line = line.split(b'>', 1)
nest_depth = int(nest_depth) if nest_depth else 0
# start-offset '='
start, line = line.split(b'=', 1)
start = int(start)
if line == b'__NOMAGIC__\n':
raise DiscardMagicRules
# value length (2 bytes, big endian)
if sys.version_info[0] >= 3:
lenvalue = int.from_bytes(line[:2], byteorder='big')
else:
self.nest=int(ind.decode('ascii'))
lenvalue = (ord(line[0])<<8)+ord(line[1])
line = line[2:]
start = b''
while True:
c = f.read(1)
if c == b'=':
break
start += c
self.start = int(start.decode('ascii'))
# value
# This can contain newlines, so we may need to read more lines
while len(line) <= lenvalue:
line += f.readline()
value, line = line[:lenvalue], line[lenvalue:]
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)
# ['&' mask]
if line.startswith(b'&'):
# This can contain newlines, so we may need to read more lines
while len(line) <= lenvalue:
line += f.readline()
mask, line = line[1:lenvalue+1], line[lenvalue+1:]
else:
self.mask=None
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
# ['~' word-size] ['+' range-length]
ending = cls.rule_ending_re.match(line)
if not ending:
# Per the spec, this will be caught and ignored, to allow
# for future extensions.
raise UnknownMagicRuleFormat(repr(line))
self.word=int(w.decode('ascii'))
else:
self.word=1
word, range = ending.groups()
word = int(word) if (word is not None) else 1
range = int(range) if (range is not None) else 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
return nest_depth, cls(start, value, mask, word, range)
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<rule.nest:
self.next=rule
rule.prev=self
elif self.prev:
self.prev.appendRule(rule)
def maxlen(self):
l = self.start + len(self.value) + self.range
if self.also:
return max(l, self.also.maxlen())
return l
def match(self, buffer):
if self.match0(buffer):
if self.next:
return self.next.match(buffer)
if self.also:
return self.also.match(buffer)
return True
def match0(self, buffer):
l=len(buffer)
lenvalue = len(self.value)
for o in range(self.range):
s=self.start+o
e=s+self.lenvalue
e=s+lenvalue
if l<e:
return False
if self.mask:
test=''
for i in range(self.lenvalue):
for i in range(lenvalue):
if PY3:
c = buffer[s+i] & self.mask[i]
else:
@ -215,46 +239,81 @@ class MagicRule:
return True
def __repr__(self):
return '<MagicRule %d>%d=[%d]%r&%r~%d+%d>' % (self.nest,
return 'MagicRule(start=%r, value=%r, mask=%r, word=%r, range=%r)' %(
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)
class MagicMatchAny(object):
"""Match any of a set of magic rules.
if nrule.nest and self.last_rule:
self.last_rule.appendRule(nrule)
else:
self.top_rules.append(nrule)
self.last_rule=nrule
return nrule
This has a similar interface to MagicRule objects (i.e. its match() and
maxlen() methods), to allow for duck typing.
"""
def __init__(self, rules):
self.rules = rules
def match(self, buffer):
for rule in self.top_rules:
if rule.match(buffer):
return self.mtype
return any(r.match(buffer) for r in self.rules)
def __repr__(self):
return '<MagicType %s>' % self.mtype
def maxlen(self):
return max(r.maxlen() for r in self.rules)
@classmethod
def from_file(cls, f):
"""Read a set of rules from the binary magic file."""
c=f.read(1)
f.seek(-1, 1)
depths_rules = []
while c and c != b'[':
try:
depths_rules.append(MagicRule.from_file(f))
except UnknownMagicRuleFormat:
# Ignored to allow for extensions to the rule format.
pass
c=f.read(1)
if c:
f.seek(-1, 1)
# Build the rule tree
tree = [] # (rule, [(subrule,[subsubrule,...]), ...])
insert_points = {0:tree}
for depth, rule in depths_rules:
subrules = []
insert_points[depth].append((rule, subrules))
insert_points[depth+1] = subrules
return cls.from_rule_tree(tree)
@classmethod
def from_rule_tree(cls, tree):
"""From a nested list of (rule, subrules) pairs, build a MagicMatchAny
instance, recursing down the tree.
Where there's only one top-level rule, this is returned directly,
to simplify the nested structure. Returns None if no rules were read.
"""
rules = []
for rule, subrules in tree:
if subrules:
rule.also = cls.from_rule_tree(subrules)
rules.append(rule)
if len(rules)==0:
return None
if len(rules)==1:
return rules[0]
return cls(rules)
class MagicDB:
def __init__(self):
self.types={} # Indexed by priority, each entry is a list of type rules
self.maxlen=0
self.bytype = defaultdict(list) # mimetype -> [(priority, rule), ...]
def mergeFile(self, fname):
def merge_file(self, fname):
"""Read a magic binary file, and add its rules to this MagicDB."""
with open(fname, 'rb') as f:
line = f.readline()
if line != b'MIME-Magic\0\n':
@ -262,68 +321,210 @@ class MagicDB:
while True:
shead = f.readline().decode('ascii')
#print shead
#print(shead)
if not shead:
break
if shead[0] != '[' or shead[-2:] != ']\n':
raise ValueError('Malformed section heading')
raise ValueError('Malformed section heading', shead)
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
rule = MagicMatchAny.from_file(f)
except DiscardMagicRules:
self.bytype.pop(mtype, None)
rule = MagicMatchAny.from_file(f)
if rule is None:
continue
#print rule
magictype = MagicType(mtype)
#print tname
self.bytype[mtype].append((pri, rule))
#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()
def finalise(self):
"""Prepare the MagicDB for matching.
c = f.read(1)
f.seek(-1, 1)
This should be called after all rules have been merged into it.
"""
maxlen = 0
self.alltypes = [] # (priority, mimetype, rule)
ents.append(magictype)
#self.types[pri]=ents
if not c:
break
for mtype, rules in self.bytype.items():
for pri, rule in rules:
self.alltypes.append((pri, mtype, rule))
maxlen = max(maxlen, rule.maxlen())
def match_data(self, data, max_pri=100, min_pri=0):
for priority in sorted(self.types.keys(), reverse=True):
self.maxlen = maxlen # Number of bytes to read from files
self.alltypes.sort(key=lambda x: x[0], reverse=True)
def match_data(self, data, max_pri=100, min_pri=0, possible=None):
"""Do magic sniffing on some bytes.
max_pri & min_pri can be used to specify the maximum & minimum priority
rules to look for. possible can be a list of mimetypes to check, or None
(the default) to check all mimetypes until one matches.
Returns the MIMEtype found, or None if no entries match.
"""
if possible is not None:
types = []
for mt in possible:
for pri, rule in self.bytype[mt]:
types.append((pri, mt, rule))
types.sort(key=lambda x: x[0])
else:
types = self.alltypes
for priority, mimetype, rule in types:
#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
if rule.match(data):
return mimetype
def match(self, path, max_pri=100, min_pri=0, possible=None):
"""Read data from the file and do magic sniffing on it.
max_pri & min_pri can be used to specify the maximum & minimum priority
rules to look for. possible can be a list of mimetypes to check, or None
(the default) to check all mimetypes until one matches.
Returns the MIMEtype found, or None if no entries match. Raises IOError
if the file can't be opened.
"""
with open(path, 'rb') as f:
buf = f.read(self.maxlen)
return self.match_data(buf, max_pri, min_pri, possible)
def __repr__(self):
return '<MagicDB %s>' % self.types
return '<MagicDB (%d types)>' % len(self.alltypes)
class GlobDB(object):
def __init__(self):
"""Prepare the GlobDB. It can't actually be used until .finalise() is
called, but merge_file() can be used to add data before that.
"""
# Maps mimetype to {(weight, glob, flags), ...}
self.allglobs = defaultdict(set)
def merge_file(self, path):
"""Loads name matching information from a globs2 file."""#
allglobs = self.allglobs
with open(path) as f:
for line in f:
if line.startswith('#'): continue # Comment
fields = line[:-1].split(':')
weight, type_name, pattern = fields[:3]
weight = int(weight)
mtype = lookup(type_name)
if len(fields) > 3:
flags = fields[3].split(',')
else:
flags = ()
if pattern == '__NOGLOBS__':
# This signals to discard any previous globs
allglobs.pop(mtype, None)
continue
allglobs[mtype].add((weight, pattern, tuple(flags)))
def finalise(self):
"""Prepare the GlobDB for matching.
This should be called after all files have been merged into it.
"""
self.exts = defaultdict(list) # Maps extensions to [(type, weight),...]
self.cased_exts = defaultdict(list)
self.globs = [] # List of (regex, type, weight) triplets
self.literals = {} # Maps literal names to (type, weight)
self.cased_literals = {}
for mtype, globs in self.allglobs.items():
mtype = mtype.canonical()
for weight, pattern, flags in globs:
cased = 'cs' in flags
if pattern.startswith('*.'):
# *.foo -- extension pattern
rest = pattern[2:]
if not ('*' in rest or '[' in rest or '?' in rest):
if cased:
self.cased_exts[rest].append((mtype, weight))
else:
self.exts[rest.lower()].append((mtype, weight))
continue
if ('*' in pattern or '[' in pattern or '?' in pattern):
# Translate the glob pattern to a regex & compile it
re_flags = 0 if cased else re.I
pattern = re.compile(fnmatch.translate(pattern), flags=re_flags)
self.globs.append((pattern, mtype, weight))
else:
# No wildcards - literal pattern
if cased:
self.cased_literals[pattern] = (mtype, weight)
else:
self.literals[pattern.lower()] = (mtype, weight)
# Sort globs by weight & length
self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) )
def first_match(self, path):
"""Return the first match found for a given path, or None if no match
is found."""
try:
return next(self._match_path(path))[0]
except StopIteration:
return None
def all_matches(self, path):
"""Return a list of (MIMEtype, glob weight) pairs for the path."""
return list(self._match_path(path))
def _match_path(self, path):
"""Yields pairs of (mimetype, glob weight)."""
leaf = os.path.basename(path)
# Literals (no wildcards)
if leaf in self.cased_literals:
yield self.cased_literals[leaf]
lleaf = leaf.lower()
if lleaf in self.literals:
yield self.literals[lleaf]
# Extensions
ext = leaf
while 1:
p = ext.find('.')
if p < 0: break
ext = ext[p + 1:]
if ext in self.cased_exts:
for res in self.cased_exts[ext]:
yield res
ext = lleaf
while 1:
p = ext.find('.')
if p < 0: break
ext = ext[p+1:]
if ext in self.exts:
for res in self.exts[ext]:
yield res
# Other globs
for (regex, mime_type, weight) in self.globs:
if regex.match(leaf):
yield (mime_type, weight)
# Some well-known types
text = lookup('text', 'plain')
octet_stream = lookup('application', 'octet-stream')
inode_block = lookup('inode', 'blockdevice')
inode_char = lookup('inode', 'chardevice')
inode_dir = lookup('inode', 'directory')
@ -336,44 +537,12 @@ app_exe = lookup('application', 'executable')
_cache_uptodate = False
def _cache_database():
global exts, globs, literals, magic, aliases, inheritance, _cache_uptodate
global globs, 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')):
@ -382,6 +551,18 @@ def _cache_database():
alias, canonical = line.strip().split(None, 1)
aliases[alias] = canonical
# Load filename patterns (globs)
globs = GlobDB()
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')):
globs.merge_file(path)
globs.finalise()
# Load magic sniffing data
magic = MagicDB()
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
magic.merge_file(path)
magic.finalise()
# Load subclasses
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
with open(path, 'r') as f:
@ -396,35 +577,7 @@ def update_cache():
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
return globs.first_match(path)
def get_type_by_contents(path, max_pri=100, min_pri=0):
"""Returns type of file by its contents, or None if not known"""
@ -438,15 +591,24 @@ def get_type_by_data(data, max_pri=100, min_pri=0):
return magic.match_data(data, max_pri, min_pri)
def _get_type_by_stat(st_mode):
"""Match special filesystem objects to Mimetypes."""
if stat.S_ISDIR(st_mode): return inode_dir
elif stat.S_ISCHR(st_mode): return inode_char
elif stat.S_ISBLK(st_mode): return inode_block
elif stat.S_ISFIFO(st_mode): return inode_fifo
elif stat.S_ISLNK(st_mode): return inode_symlink
elif stat.S_ISSOCK(st_mode): return inode_socket
return inode_door
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 function is *deprecated* - :func:`get_type2` is more accurate.
:param path: pathname to check (need not exist)
:param follow: when reading file, follow symbolic links
:param 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.
@ -463,6 +625,7 @@ def get_type(path, follow=True, name_pri=100):
return t or text
if stat.S_ISREG(st.st_mode):
# Regular file
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)
@ -472,13 +635,112 @@ def get_type(path, follow=True, name_pri=100):
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
else:
return _get_type_by_stat(st.st_mode)
def get_type2(path, follow=True):
"""Find the MIMEtype of a file using the XDG recommended checking order.
This first checks the filename, then uses file contents if the name doesn't
give an unambiguous MIMEtype. It can also handle special filesystem objects
like directories and sockets.
:param path: file path to examine (need not exist)
:param follow: whether to follow symlinks
:rtype: :class:`MIMEtype`
.. versionadded:: 1.0
"""
update_cache()
try:
st = os.stat(path) if follow else os.lstat(path)
except OSError:
return get_type_by_name(path) or octet_stream
if not stat.S_ISREG(st.st_mode):
# Special filesystem objects
return _get_type_by_stat(st.st_mode)
mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True)
if mtypes:
max_weight = mtypes[0][1]
i = 1
for mt, w in mtypes[1:]:
if w < max_weight:
break
i += 1
mtypes = mtypes[:i]
if len(mtypes) == 1:
return mtypes[0][0]
possible = [mt for mt,w in mtypes]
else:
possible = None # Try all magic matches
try:
t = magic.match(path, possible=possible)
except IOError:
t = None
if t:
return t
elif mtypes:
return mtypes[0][0]
elif stat.S_IMODE(st.st_mode) & 0o111:
return app_exe
else:
return text if is_text_file(path) else octet_stream
def is_text_file(path):
"""Guess whether a file contains text or binary data.
Heuristic: binary if the first 32 bytes include ASCII control characters.
This rule may change in future versions.
.. versionadded:: 1.0
"""
try:
f = open(path, 'rb')
except IOError:
return False
with f:
return _is_text(f.read(32))
if PY3:
def _is_text(data):
return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data)
else:
def _is_text(data):
return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \
for b in data)
_mime2ext_cache = None
_mime2ext_cache_uptodate = False
def get_extensions(mimetype):
"""Retrieve the set of filename extensions matching a given MIMEtype.
Extensions are returned without a leading dot, e.g. 'py'. If no extensions
are registered for the MIMEtype, returns an empty set.
The extensions are stored in a cache the first time this is called.
.. versionadded:: 1.0
"""
global _mime2ext_cache, _mime2ext_cache_uptodate
update_cache()
if not _mime2ext_cache_uptodate:
_mime2ext_cache = defaultdict(set)
for ext, mtypes in globs.exts.items():
for mtype, prio in mtypes:
_mime2ext_cache[mtype].add(ext)
_mime2ext_cache_uptodate = True
return _mime2ext_cache[mimetype]
def install_mime_info(application, package_file):
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.``

View file

@ -1,5 +1,5 @@
"""
Implementation of the XDG Recent File Storage Specification Version 0.2
Implementation of the XDG Recent File Storage Specification
http://standards.freedesktop.org/recent-file-spec
"""

View file

@ -1,3 +1,3 @@
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
__version__ = "0.25"
__version__ = "0.26"

View file

@ -9,3 +9,67 @@ else:
# Unicode-like literals
def u(s):
return s.decode('utf-8')
try:
# which() is available from Python 3.3
from shutil import which
except ImportError:
import os
# This is a copy of which() from Python 3.3
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try
# others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None