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. Note: see the rox.Options module for a higher-level API for managing options.
""" """
import os import os, stat
_home = os.path.expanduser('~') _home = os.path.expanduser('~')
xdg_data_home = os.environ.get('XDG_DATA_HOME') or \ xdg_data_home = os.environ.get('XDG_DATA_HOME') or \
@ -131,15 +131,30 @@ def get_runtime_dir(strict=True):
import getpass import getpass
fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser() fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser()
create = False
try: 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: except OSError as e:
import errno import errno
if e.errno == errno.EEXIST: if e.errno == errno.ENOENT:
# Already exists - set 700 permissions again. create = True
import stat else:
os.chmod(fallback, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
else: # pragma: no cover
raise 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 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/ http://standards.freedesktop.org/desktop-entry-spec/
Not supported: Not supported:
@ -13,6 +13,7 @@ Not supported:
from xdg.IniFile import IniFile, is_ascii from xdg.IniFile import IniFile, is_ascii
import xdg.Locale import xdg.Locale
from xdg.Exceptions import ParsingError from xdg.Exceptions import ParsingError
from xdg.util import which
import os.path import os.path
import re import re
import warnings import warnings
@ -23,7 +24,7 @@ class DesktopEntry(IniFile):
defaultGroup = 'Desktop Entry' defaultGroup = 'Desktop Entry'
def __init__(self, filename=None): 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, If filename exists, it will be parsed as a desktop entry file. If not,
or if filename is None, a blank DesktopEntry is created. or if filename is None, a blank DesktopEntry is created.
@ -38,9 +39,23 @@ class DesktopEntry(IniFile):
return self.getName() return self.getName()
def parse(self, file): 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"]) 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 # start standard keys
def getType(self): def getType(self):
return self.get('Type') return self.get('Type')
@ -140,10 +155,11 @@ class DesktopEntry(IniFile):
# desktop entry edit stuff # desktop entry edit stuff
def new(self, filename): 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 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": if os.path.splitext(filename)[1] == ".desktop":
type = "Application" type = "Application"
@ -185,7 +201,7 @@ class DesktopEntry(IniFile):
def checkGroup(self, group): def checkGroup(self, group):
# check if group header is valid # check if group header is valid
if not (group == self.defaultGroup \ 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))): or (re.match("^X-", group) and is_ascii(group))):
self.errors.append("Invalid Group name: %s" % group) self.errors.append("Invalid Group name: %s" % group)
else: else:

View file

@ -5,6 +5,7 @@ Exception Classes for the xdg package
debug = False debug = False
class Error(Exception): class Error(Exception):
"""Base class for exceptions defined here."""
def __init__(self, msg): def __init__(self, msg):
self.msg = msg self.msg = msg
Exception.__init__(self, msg) Exception.__init__(self, msg)
@ -12,40 +13,72 @@ class Error(Exception):
return self.msg return self.msg
class ValidationError(Error): class ValidationError(Error):
"""Raised when a file fails to validate.
The filename is the .file attribute.
"""
def __init__(self, msg, file): def __init__(self, msg, file):
self.msg = msg self.msg = msg
self.file = file self.file = file
Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg))
class ParsingError(Error): class ParsingError(Error):
"""Raised when a file cannot be parsed.
The filename is the .file attribute.
"""
def __init__(self, msg, file): def __init__(self, msg, file):
self.msg = msg self.msg = msg
self.file = file self.file = file
Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg))
class NoKeyError(Error): 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): def __init__(self, key, group, file):
Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file))
self.key = key self.key = key
self.group = group self.group = group
self.file = file
class DuplicateKeyError(Error): 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): def __init__(self, key, group, file):
Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file))
self.key = key self.key = key
self.group = group self.group = group
self.file = file
class NoGroupError(Error): 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): def __init__(self, group, file):
Error.__init__(self, "No group: %s in file %s" % (group, file)) Error.__init__(self, "No group: %s in file %s" % (group, file))
self.group = group self.group = group
self.file = file
class DuplicateGroupError(Error): 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): def __init__(self, group, file):
Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) Error.__init__(self, "Duplicate group: %s in file %s" % (group, file))
self.group = group self.group = group
self.file = file
class NoThemeError(Error): 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): def __init__(self, theme):
Error.__init__(self, "No such icon-theme: %s" % theme) Error.__init__(self, "No such icon-theme: %s" % theme)
self.theme = 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/ http://standards.freedesktop.org/icon-theme-spec/
""" """
@ -37,6 +37,8 @@ class IconTheme(IniFile):
return self.get('Inherits', list=True) return self.get('Inherits', list=True)
def getDirectories(self): def getDirectories(self):
return self.get('Directories', list=True) return self.get('Directories', list=True)
def getScaledDirectories(self):
return self.get('ScaledDirectories', list=True)
def getHidden(self): def getHidden(self):
return self.get('Hidden', type="boolean") return self.get('Hidden', type="boolean")
def getExample(self): def getExample(self):
@ -72,6 +74,10 @@ class IconTheme(IniFile):
else: else:
return 2 return 2
def getScale(self, directory):
value = self.get('Scale', type="integer", group=directory)
return value or 1
# validation stuff # validation stuff
def checkExtras(self): def checkExtras(self):
# header # header
@ -125,7 +131,7 @@ class IconTheme(IniFile):
self.name = self.content[group]["Size"] self.name = self.content[group]["Size"]
except KeyError: except KeyError:
self.errors.append("Key 'Size' in Group '%s' is missing" % group) 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) self.errors.append("Invalid Group name: %s" % group)
def checkKey(self, key, value, group): def checkKey(self, key, value, group):
@ -139,6 +145,8 @@ class IconTheme(IniFile):
self.checkValue(key, value, list=True) self.checkValue(key, value, list=True)
elif key == "Directories": elif key == "Directories":
self.checkValue(key, value, list=True) self.checkValue(key, value, list=True)
elif key == "ScaledDirectories":
self.checkValue(key, value, list=True)
elif key == "Hidden": elif key == "Hidden":
self.checkValue(key, value, type="boolean") self.checkValue(key, value, type="boolean")
elif key == "Example": elif key == "Example":
@ -168,6 +176,8 @@ class IconTheme(IniFile):
self.checkValue(key, value, type="integer") self.checkValue(key, value, type="integer")
if self.type != "Threshold": if self.type != "Threshold":
self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) 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): elif re.match("^X-[a-zA-Z0-9-]+", key):
pass pass
else: else:
@ -211,7 +221,7 @@ class IconData(IniFile):
def checkGroup(self, group): def checkGroup(self, group):
# check if group header is valid # check if group header is valid
if not (group == self.defaultGroup \ 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")) self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace"))
def checkKey(self, key, value, group): def checkKey(self, key, value, group):

View file

@ -102,7 +102,7 @@ class IniFile:
raise ParsingError("[%s]-Header missing" % headers[0], filename) raise ParsingError("[%s]-Header missing" % headers[0], filename)
# start stuff to access the keys # 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 # set default group
if not group: if not group:
group = self.defaultGroup group = self.defaultGroup
@ -114,7 +114,7 @@ class IniFile:
else: else:
value = self.content[group][key] value = self.content[group][key]
else: else:
if debug: if strict or debug:
if group not in self.content: if group not in self.content:
raise NoGroupError(group, self.filename) raise NoGroupError(group, self.filename)
elif key not in self.content[group]: elif key not in self.content[group]:
@ -192,8 +192,8 @@ class IniFile:
# start validation stuff # start validation stuff
def validate(self, report="All"): def validate(self, report="All"):
"""Validate the contents, raising ``ValidationError`` if there """Validate the contents, raising :class:`~xdg.Exceptions.ValidationError`
is anything amiss. if there is anything amiss.
report can be 'All' / 'Warnings' / 'Errors' 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 import os
from locale import normalize 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): def _expand_lang(locale):
locale = normalize(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 """ """ 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 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 # XML-Cleanups: Move / Exclude
# FIXME: proper reverte/delete # FIXME: proper reverte/delete
@ -20,28 +20,31 @@ import re
# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile # FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile
# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs # Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs
class MenuEditor:
class MenuEditor(object):
def __init__(self, menu=None, filename=None, root=False): def __init__(self, menu=None, filename=None, root=False):
self.menu = None self.menu = None
self.filename = None self.filename = None
self.doc = None self.tree = None
self.parser = XMLMenuBuilder()
self.parse(menu, filename, root) self.parse(menu, filename, root)
# fix for creating two menus with the same name on the fly # fix for creating two menus with the same name on the fly
self.filenames = [] self.filenames = []
def parse(self, menu=None, filename=None, root=False): def parse(self, menu=None, filename=None, root=False):
if root == True: if root:
setRootMode(True) setRootMode(True)
if isinstance(menu, Menu): if isinstance(menu, Menu):
self.menu = menu self.menu = menu
elif menu: elif menu:
self.menu = parse(menu) self.menu = self.parser.parse(menu)
else: else:
self.menu = parse() self.menu = self.parser.parse()
if root == True: if root:
self.filename = self.menu.Filename self.filename = self.menu.Filename
elif filename: elif filename:
self.filename = 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]) self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1])
try: try:
self.doc = xml.dom.minidom.parse(self.filename) self.tree = etree.parse(self.filename)
except IOError: 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>') root = etree.fromtring("""
except xml.parsers.expat.ExpatError: <!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) 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): def save(self):
self.__saveEntries(self.menu) self.__saveEntries(self.menu)
@ -67,7 +78,7 @@ class MenuEditor:
self.__addEntry(parent, menuentry, after, before) self.__addEntry(parent, menuentry, after, before)
sort(self.menu) self.menu.sort()
return menuentry return menuentry
@ -83,7 +94,7 @@ class MenuEditor:
self.__addEntry(parent, menu, after, before) self.__addEntry(parent, menu, after, before)
sort(self.menu) self.menu.sort()
return menu return menu
@ -92,7 +103,7 @@ class MenuEditor:
self.__addEntry(parent, separator, after, before) self.__addEntry(parent, separator, after, before)
sort(self.menu) self.menu.sort()
return separator return separator
@ -100,7 +111,7 @@ class MenuEditor:
self.__deleteEntry(oldparent, menuentry, after, before) self.__deleteEntry(oldparent, menuentry, after, before)
self.__addEntry(newparent, menuentry, after, before) self.__addEntry(newparent, menuentry, after, before)
sort(self.menu) self.menu.sort()
return menuentry return menuentry
@ -112,7 +123,7 @@ class MenuEditor:
if oldparent.getPath(True) != newparent.getPath(True): 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)) 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 return menu
@ -120,14 +131,14 @@ class MenuEditor:
self.__deleteEntry(parent, separator, after, before) self.__deleteEntry(parent, separator, after, before)
self.__addEntry(parent, separator, after, before) self.__addEntry(parent, separator, after, before)
sort(self.menu) self.menu.sort()
return separator return separator
def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None):
self.__addEntry(newparent, menuentry, after, before) self.__addEntry(newparent, menuentry, after, before)
sort(self.menu) self.menu.sort()
return menuentry return menuentry
@ -137,39 +148,39 @@ class MenuEditor:
if name: if name:
if not deskentry.hasKey("Name"): if not deskentry.hasKey("Name"):
deskentry.set("Name", name) deskentry.set("Name", name)
deskentry.set("Name", name, locale = True) deskentry.set("Name", name, locale=True)
if comment: if comment:
if not deskentry.hasKey("Comment"): if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment) deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale = True) deskentry.set("Comment", comment, locale=True)
if genericname: if genericname:
if not deskentry.hasKey("GnericNe"): if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname) deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale = True) deskentry.set("GenericName", genericname, locale=True)
if command: if command:
deskentry.set("Exec", command) deskentry.set("Exec", command)
if icon: if icon:
deskentry.set("Icon", icon) deskentry.set("Icon", icon)
if terminal == True: if terminal:
deskentry.set("Terminal", "true") deskentry.set("Terminal", "true")
elif terminal == False: elif not terminal:
deskentry.set("Terminal", "false") deskentry.set("Terminal", "false")
if nodisplay == True: if nodisplay is True:
deskentry.set("NoDisplay", "true") deskentry.set("NoDisplay", "true")
elif nodisplay == False: elif nodisplay is False:
deskentry.set("NoDisplay", "false") deskentry.set("NoDisplay", "false")
if hidden == True: if hidden is True:
deskentry.set("Hidden", "true") deskentry.set("Hidden", "true")
elif hidden == False: elif hidden is False:
deskentry.set("Hidden", "false") deskentry.set("Hidden", "false")
menuentry.updateAttributes() menuentry.updateAttributes()
if len(menuentry.Parents) > 0: if len(menuentry.Parents) > 0:
sort(self.menu) self.menu.sort()
return menuentry return menuentry
@ -195,56 +206,58 @@ class MenuEditor:
if name: if name:
if not deskentry.hasKey("Name"): if not deskentry.hasKey("Name"):
deskentry.set("Name", name) deskentry.set("Name", name)
deskentry.set("Name", name, locale = True) deskentry.set("Name", name, locale=True)
if genericname: if genericname:
if not deskentry.hasKey("GenericName"): if not deskentry.hasKey("GenericName"):
deskentry.set("GenericName", genericname) deskentry.set("GenericName", genericname)
deskentry.set("GenericName", genericname, locale = True) deskentry.set("GenericName", genericname, locale=True)
if comment: if comment:
if not deskentry.hasKey("Comment"): if not deskentry.hasKey("Comment"):
deskentry.set("Comment", comment) deskentry.set("Comment", comment)
deskentry.set("Comment", comment, locale = True) deskentry.set("Comment", comment, locale=True)
if icon: if icon:
deskentry.set("Icon", icon) deskentry.set("Icon", icon)
if nodisplay == True: if nodisplay is True:
deskentry.set("NoDisplay", "true") deskentry.set("NoDisplay", "true")
elif nodisplay == False: elif nodisplay is False:
deskentry.set("NoDisplay", "false") deskentry.set("NoDisplay", "false")
if hidden == True: if hidden is True:
deskentry.set("Hidden", "true") deskentry.set("Hidden", "true")
elif hidden == False: elif hidden is False:
deskentry.set("Hidden", "false") deskentry.set("Hidden", "false")
menu.Directory.updateAttributes() menu.Directory.updateAttributes()
if isinstance(menu.Parent, Menu): if isinstance(menu.Parent, Menu):
sort(self.menu) self.menu.sort()
return menu return menu
def hideMenuEntry(self, menuentry): def hideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay = True) self.editMenuEntry(menuentry, nodisplay=True)
def unhideMenuEntry(self, menuentry): def unhideMenuEntry(self, menuentry):
self.editMenuEntry(menuentry, nodisplay = False, hidden = False) self.editMenuEntry(menuentry, nodisplay=False, hidden=False)
def hideMenu(self, menu): def hideMenu(self, menu):
self.editMenu(menu, nodisplay = True) self.editMenu(menu, nodisplay=True)
def unhideMenu(self, menu): def unhideMenu(self, menu):
self.editMenu(menu, nodisplay = False, hidden = False) self.editMenu(menu, nodisplay=False, hidden=False)
xml_menu = self.__getXmlMenu(menu.getPath(True,True), False) xml_menu = self.__getXmlMenu(menu.getPath(True, True), False)
for node in self.__getXmlNodesByName(["Deleted", "NotDeleted"], xml_menu): deleted = xml_menu.findall('Deleted')
node.parentNode.removeChild(node) not_deleted = xml_menu.findall('NotDeleted')
for node in deleted + not_deleted:
xml_menu.remove(node)
def deleteMenuEntry(self, menuentry): def deleteMenuEntry(self, menuentry):
if self.getAction(menuentry) == "delete": if self.getAction(menuentry) == "delete":
self.__deleteFile(menuentry.DesktopEntry.filename) self.__deleteFile(menuentry.DesktopEntry.filename)
for parent in menuentry.Parents: for parent in menuentry.Parents:
self.__deleteEntry(parent, menuentry) self.__deleteEntry(parent, menuentry)
sort(self.menu) self.menu.sort()
return menuentry return menuentry
def revertMenuEntry(self, menuentry): def revertMenuEntry(self, menuentry):
@ -257,7 +270,7 @@ class MenuEditor:
index = parent.MenuEntries.index(menuentry) index = parent.MenuEntries.index(menuentry)
parent.MenuEntries[index] = menuentry.Original parent.MenuEntries[index] = menuentry.Original
menuentry.Original.Parents.append(parent) menuentry.Original.Parents.append(parent)
sort(self.menu) self.menu.sort()
return menuentry return menuentry
def deleteMenu(self, menu): def deleteMenu(self, menu):
@ -265,21 +278,22 @@ class MenuEditor:
self.__deleteFile(menu.Directory.DesktopEntry.filename) self.__deleteFile(menu.Directory.DesktopEntry.filename)
self.__deleteEntry(menu.Parent, menu) self.__deleteEntry(menu.Parent, menu)
xml_menu = self.__getXmlMenu(menu.getPath(True, True)) xml_menu = self.__getXmlMenu(menu.getPath(True, True))
xml_menu.parentNode.removeChild(xml_menu) parent = self.__get_parent_node(xml_menu)
sort(self.menu) parent.remove(xml_menu)
self.menu.sort()
return menu return menu
def revertMenu(self, menu): def revertMenu(self, menu):
if self.getAction(menu) == "revert": if self.getAction(menu) == "revert":
self.__deleteFile(menu.Directory.DesktopEntry.filename) self.__deleteFile(menu.Directory.DesktopEntry.filename)
menu.Directory = menu.Directory.Original menu.Directory = menu.Directory.Original
sort(self.menu) self.menu.sort()
return menu return menu
def deleteSeparator(self, separator): def deleteSeparator(self, separator):
self.__deleteEntry(separator.Parent, separator, after=True) self.__deleteEntry(separator.Parent, separator, after=True)
sort(self.menu) self.menu.sort()
return separator return separator
@ -290,8 +304,9 @@ class MenuEditor:
return "none" return "none"
elif entry.Directory.getType() == "Both": elif entry.Directory.getType() == "Both":
return "revert" return "revert"
elif entry.Directory.getType() == "User" \ elif entry.Directory.getType() == "User" and (
and (len(entry.Submenus) + len(entry.MenuEntries)) == 0: len(entry.Submenus) + len(entry.MenuEntries)
) == 0:
return "delete" return "delete"
elif isinstance(entry, MenuEntry): elif isinstance(entry, MenuEntry):
@ -318,9 +333,7 @@ class MenuEditor:
def __saveMenu(self): def __saveMenu(self):
if not os.path.isdir(os.path.dirname(self.filename)): if not os.path.isdir(os.path.dirname(self.filename)):
os.makedirs(os.path.dirname(self.filename)) os.makedirs(os.path.dirname(self.filename))
fd = open(self.filename, 'w') self.tree.write(self.filename, encoding='utf-8')
fd.write(re.sub("\n[\s]*([^\n<]*)\n[\s]*</", "\\1</", self.doc.toprettyxml().replace('<?xml version="1.0" ?>\n', '')))
fd.close()
def __getFileName(self, name, extension): def __getFileName(self, name, extension):
postfix = 0 postfix = 0
@ -333,8 +346,9 @@ class MenuEditor:
dir = "applications" dir = "applications"
elif extension == ".directory": elif extension == ".directory":
dir = "desktop-directories" dir = "desktop-directories"
if not filename in self.filenames and not \ if not filename in self.filenames and not os.path.isfile(
os.path.isfile(os.path.join(xdg_data_dirs[0], dir, filename)): os.path.join(xdg_data_dirs[0], dir, filename)
):
self.filenames.append(filename) self.filenames.append(filename)
break break
else: else:
@ -343,8 +357,11 @@ class MenuEditor:
return filename return filename
def __getXmlMenu(self, path, create=True, element=None): 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: if not element:
element = self.doc element = self.tree
if "/" in path: if "/" in path:
(name, path) = path.split("/", 1) (name, path) = path.split("/", 1)
@ -353,17 +370,16 @@ class MenuEditor:
path = "" path = ""
found = None found = None
for node in self.__getXmlNodesByName("Menu", element): for node in element.findall("Menu"):
for child in self.__getXmlNodesByName("Name", node): name_node = node.find('Name')
if child.childNodes[0].nodeValue == name: if name_node.text == name:
if path: if path:
found = self.__getXmlMenu(path, create, node) found = self.__getXmlMenu(path, create, node)
else: else:
found = node found = node
break
if found: if found:
break break
if not found and create == True: if not found and create:
node = self.__addXmlMenuElement(element, name) node = self.__addXmlMenuElement(element, name)
if path: if path:
found = self.__getXmlMenu(path, create, node) found = self.__getXmlMenu(path, create, node)
@ -373,58 +389,62 @@ class MenuEditor:
return found return found
def __addXmlMenuElement(self, element, name): def __addXmlMenuElement(self, element, name):
node = self.doc.createElement('Menu') menu_node = etree.SubElement('Menu', element)
self.__addXmlTextElement(node, 'Name', name) name_node = etree.SubElement('Name', menu_node)
return element.appendChild(node) name_node.text = name
return menu_node
def __addXmlTextElement(self, element, name, text): def __addXmlTextElement(self, element, name, text):
node = self.doc.createElement(name) node = etree.SubElement(name, element)
text = self.doc.createTextNode(text) node.text = text
node.appendChild(text) return node
return element.appendChild(node)
def __addXmlFilename(self, element, filename, type = "Include"): def __addXmlFilename(self, element, filename, type_="Include"):
# remove old filenames # remove old filenames
for node in self.__getXmlNodesByName(["Include", "Exclude"], element): includes = element.findall('Include')
if node.childNodes[0].nodeName == "Filename" and node.childNodes[0].childNodes[0].nodeValue == filename: excludes = element.findall('Exclude')
element.removeChild(node) 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 # add new filename
node = self.doc.createElement(type) node = etree.SubElement(type_, element)
node.appendChild(self.__addXmlTextElement(node, 'Filename', filename)) self.__addXmlTextElement(node, 'Filename', filename)
return element.appendChild(node) return node
def __addXmlMove(self, element, old, new): def __addXmlMove(self, element, old, new):
node = self.doc.createElement("Move") node = etree.SubElement("Move", element)
node.appendChild(self.__addXmlTextElement(node, 'Old', old)) self.__addXmlTextElement(node, 'Old', old)
node.appendChild(self.__addXmlTextElement(node, 'New', new)) self.__addXmlTextElement(node, 'New', new)
return element.appendChild(node) return node
def __addXmlLayout(self, element, layout): def __addXmlLayout(self, element, layout):
# remove old layout # remove old layout
for node in self.__getXmlNodesByName("Layout", element): for node in element.findall("Layout"):
element.removeChild(node) element.remove(node)
# add new layout # add new layout
node = self.doc.createElement("Layout") node = etree.SubElement("Layout", element)
for order in layout.order: for order in layout.order:
if order[0] == "Separator": if order[0] == "Separator":
child = self.doc.createElement("Separator") child = etree.SubElement("Separator", node)
node.appendChild(child)
elif order[0] == "Filename": elif order[0] == "Filename":
child = self.__addXmlTextElement(node, "Filename", order[1]) child = self.__addXmlTextElement(node, "Filename", order[1])
elif order[0] == "Menuname": elif order[0] == "Menuname":
child = self.__addXmlTextElement(node, "Menuname", order[1]) child = self.__addXmlTextElement(node, "Menuname", order[1])
elif order[0] == "Merge": elif order[0] == "Merge":
child = self.doc.createElement("Merge") child = etree.SubElement("Merge", node)
child.setAttribute("type", order[1]) child.attrib["type"] = order[1]
node.appendChild(child) return node
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): def __addLayout(self, parent):
layout = Layout() layout = Layout()
@ -498,14 +518,24 @@ class MenuEditor:
except ValueError: except ValueError:
pass pass
def __remove_whilespace_nodes(self, node): def __remove_whitespace_nodes(self, node):
remove_list = [] for child in node:
for child in node.childNodes: text = child.text.strip()
if child.nodeType == xml.dom.minidom.Node.TEXT_NODE: if not text:
child.data = child.data.strip() child.text = ''
if not child.data.strip(): tail = child.tail.strip()
remove_list.append(child) if not tail:
elif child.hasChildNodes(): child.tail = ''
if len(child):
self.__remove_whilespace_nodes(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 os
import re
import stat import stat
import sys import sys
import fnmatch import fnmatch
@ -46,25 +47,42 @@ def _get_node_data(node):
return ''.join([n.nodeValue for n in node.childNodes]).strip() return ''.join([n.nodeValue for n in node.childNodes]).strip()
def lookup(media, subtype = None): 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 The name can either be passed as one part ('text/plain'), or as two
('text', 'plain'). ('text', 'plain').
""" """
if subtype is None and '/' in media: return MIMEtype(media, subtype)
media, subtype = media.split('/', 1)
if (media, subtype) not in types:
types[(media, subtype)] = MIMEtype(media, subtype)
return types[(media, subtype)]
class MIMEtype: class MIMEtype(object):
"""Type holding data about a MIME type""" """Class 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
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.media = media
self.subtype = subtype self.subtype = subtype
self._comment = None self._comment = None
@ -109,100 +127,106 @@ class MIMEtype:
return self.media + '/' + self.subtype return self.media + '/' + self.subtype
def __repr__(self): 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: class MagicRule:
def __init__(self, f): also = None
self.next=None
self.prev=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 #print line
ind=b''
while True: # [indent] '>'
c=f.read(1) nest_depth, line = line.split(b'>', 1)
if c == b'>': nest_depth = int(nest_depth) if nest_depth else 0
break
ind+=c # start-offset '='
if not ind: start, line = line.split(b'=', 1)
self.nest=0 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: else:
self.nest=int(ind.decode('ascii')) lenvalue = (ord(line[0])<<8)+ord(line[1])
line = line[2:]
start = b'' # value
while True: # This can contain newlines, so we may need to read more lines
c = f.read(1) while len(line) <= lenvalue:
if c == b'=': line += f.readline()
break value, line = line[:lenvalue], line[lenvalue:]
start += c
self.start = int(start.decode('ascii'))
hb=f.read(1) # ['&' mask]
lb=f.read(1) if line.startswith(b'&'):
self.lenvalue = ord(lb)+(ord(hb)<<8) # This can contain newlines, so we may need to read more lines
while len(line) <= lenvalue:
self.value = f.read(self.lenvalue) line += f.readline()
mask, line = line[1:lenvalue+1], line[lenvalue+1:]
c = f.read(1)
if c == b'&':
self.mask = f.read(self.lenvalue)
c = f.read(1)
else: else:
self.mask=None mask = None
if c == b'~': # ['~' word-size] ['+' range-length]
w = b'' ending = cls.rule_ending_re.match(line)
while c!=b'+' and c!=b'\n': if not ending:
c=f.read(1) # Per the spec, this will be caught and ignored, to allow
if c==b'+' or c==b'\n': # for future extensions.
break raise UnknownMagicRuleFormat(repr(line))
w+=c
self.word=int(w.decode('ascii')) word, range = ending.groups()
else: word = int(word) if (word is not None) else 1
self.word=1 range = int(range) if (range is not None) else 1
if c==b'+': return nest_depth, cls(start, value, mask, word, range)
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': def maxlen(self):
raise ValueError('Malformed MIME magic line') l = self.start + len(self.value) + self.range
if self.also:
def getLength(self): return max(l, self.also.maxlen())
return self.start+self.lenvalue+self.range return l
def appendRule(self, rule):
if self.nest<rule.nest:
self.next=rule
rule.prev=self
elif self.prev:
self.prev.appendRule(rule)
def match(self, buffer): def match(self, buffer):
if self.match0(buffer): if self.match0(buffer):
if self.next: if self.also:
return self.next.match(buffer) return self.also.match(buffer)
return True return True
def match0(self, buffer): def match0(self, buffer):
l=len(buffer) l=len(buffer)
lenvalue = len(self.value)
for o in range(self.range): for o in range(self.range):
s=self.start+o s=self.start+o
e=s+self.lenvalue e=s+lenvalue
if l<e: if l<e:
return False return False
if self.mask: if self.mask:
test='' test=''
for i in range(self.lenvalue): for i in range(lenvalue):
if PY3: if PY3:
c = buffer[s+i] & self.mask[i] c = buffer[s+i] & self.mask[i]
else: else:
@ -215,46 +239,81 @@ class MagicRule:
return True return True
def __repr__(self): 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.start,
self.lenvalue,
self.value, self.value,
self.mask, self.mask,
self.word, self.word,
self.range) self.range)
class MagicType:
def __init__(self, mtype):
self.mtype=mtype
self.top_rules=[]
self.last_rule=None
def getLine(self, f): class MagicMatchAny(object):
nrule=MagicRule(f) """Match any of a set of magic rules.
if nrule.nest and self.last_rule: This has a similar interface to MagicRule objects (i.e. its match() and
self.last_rule.appendRule(nrule) maxlen() methods), to allow for duck typing.
else: """
self.top_rules.append(nrule) def __init__(self, rules):
self.rules = rules
self.last_rule=nrule
return nrule
def match(self, buffer): def match(self, buffer):
for rule in self.top_rules: return any(r.match(buffer) for r in self.rules)
if rule.match(buffer):
return self.mtype
def __repr__(self): def maxlen(self):
return '<MagicType %s>' % self.mtype 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: class MagicDB:
def __init__(self): def __init__(self):
self.types={} # Indexed by priority, each entry is a list of type rules self.bytype = defaultdict(list) # mimetype -> [(priority, rule), ...]
self.maxlen=0
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: with open(fname, 'rb') as f:
line = f.readline() line = f.readline()
if line != b'MIME-Magic\0\n': if line != b'MIME-Magic\0\n':
@ -262,68 +321,210 @@ class MagicDB:
while True: while True:
shead = f.readline().decode('ascii') shead = f.readline().decode('ascii')
#print shead #print(shead)
if not shead: if not shead:
break break
if shead[0] != '[' or shead[-2:] != ']\n': if shead[0] != '[' or shead[-2:] != ']\n':
raise ValueError('Malformed section heading') raise ValueError('Malformed section heading', shead)
pri, tname = shead[1:-2].split(':') pri, tname = shead[1:-2].split(':')
#print shead[1:-2] #print shead[1:-2]
pri = int(pri) pri = int(pri)
mtype = lookup(tname) mtype = lookup(tname)
try: try:
ents = self.types[pri] rule = MagicMatchAny.from_file(f)
except: except DiscardMagicRules:
ents = [] self.bytype.pop(mtype, None)
self.types[pri] = ents rule = MagicMatchAny.from_file(f)
if rule is None:
continue
#print rule
magictype = MagicType(mtype) self.bytype[mtype].append((pri, rule))
#print tname
#rline=f.readline() def finalise(self):
c=f.read(1) """Prepare the MagicDB for matching.
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) This should be called after all rules have been merged into it.
f.seek(-1, 1) """
maxlen = 0
self.alltypes = [] # (priority, mimetype, rule)
ents.append(magictype) for mtype, rules in self.bytype.items():
#self.types[pri]=ents for pri, rule in rules:
if not c: self.alltypes.append((pri, mtype, rule))
break maxlen = max(maxlen, rule.maxlen())
def match_data(self, data, max_pri=100, min_pri=0): self.maxlen = maxlen # Number of bytes to read from files
for priority in sorted(self.types.keys(), reverse=True): 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 #print priority, max_pri, min_pri
if priority > max_pri: if priority > max_pri:
continue continue
if priority < min_pri: if priority < min_pri:
break 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): if rule.match(data):
try: return mimetype
with open(path, 'rb') as f:
buf = f.read(self.maxlen) def match(self, path, max_pri=100, min_pri=0, possible=None):
return self.match_data(buf, max_pri, min_pri) """Read data from the file and do magic sniffing on it.
except:
pass 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): 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 # Some well-known types
text = lookup('text', 'plain') text = lookup('text', 'plain')
octet_stream = lookup('application', 'octet-stream')
inode_block = lookup('inode', 'blockdevice') inode_block = lookup('inode', 'blockdevice')
inode_char = lookup('inode', 'chardevice') inode_char = lookup('inode', 'chardevice')
inode_dir = lookup('inode', 'directory') inode_dir = lookup('inode', 'directory')
@ -336,44 +537,12 @@ app_exe = lookup('application', 'executable')
_cache_uptodate = False _cache_uptodate = False
def _cache_database(): def _cache_database():
global exts, globs, literals, magic, aliases, inheritance, _cache_uptodate global globs, magic, aliases, inheritance, _cache_uptodate
_cache_uptodate = True _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 aliases = {} # Maps alias Mime types to canonical names
inheritance = defaultdict(set) # Maps to sets of parent mime types. 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 # Load aliases
for path in BaseDirectory.load_data_paths(os.path.join('mime', '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) alias, canonical = line.strip().split(None, 1)
aliases[alias] = canonical 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 # Load subclasses
for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')): for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')):
with open(path, 'r') as f: with open(path, 'r') as f:
@ -396,35 +577,7 @@ def update_cache():
def get_type_by_name(path): def get_type_by_name(path):
"""Returns type of file by its name, or None if not known""" """Returns type of file by its name, or None if not known"""
update_cache() update_cache()
return globs.first_match(path)
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): def get_type_by_contents(path, max_pri=100, min_pri=0):
"""Returns type of file by its contents, or None if not known""" """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) 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): def get_type(path, follow=True, name_pri=100):
"""Returns type of file indicated by path. """Returns type of file indicated by path.
path : This function is *deprecated* - :func:`get_type2` is more accurate.
pathname to check (need not exist)
follow : :param path: pathname to check (need not exist)
when reading file, follow symbolic links :param follow: when reading file, follow symbolic links
name_pri : :param name_pri: Priority to do name matches. 100=override magic
Priority to do name matches. 100=override magic
This tries to use the contents of the file, and falls back to the name. It 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. 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 return t or text
if stat.S_ISREG(st.st_mode): if stat.S_ISREG(st.st_mode):
# Regular file
t = get_type_by_contents(path, min_pri=name_pri) 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_name(path)
if not t: t = get_type_by_contents(path, max_pri=name_pri) 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: else:
return text return text
return t return t
elif stat.S_ISDIR(st.st_mode): return inode_dir else:
elif stat.S_ISCHR(st.st_mode): return inode_char return _get_type_by_stat(st.st_mode)
elif stat.S_ISBLK(st.st_mode): return inode_block
elif stat.S_ISFIFO(st.st_mode): return inode_fifo def get_type2(path, follow=True):
elif stat.S_ISLNK(st.st_mode): return inode_symlink """Find the MIMEtype of a file using the XDG recommended checking order.
elif stat.S_ISSOCK(st.st_mode): return inode_socket
return inode_door 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): def install_mime_info(application, package_file):
"""Copy 'package_file' as ``~/.local/share/mime/packages/<application>.xml.`` """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 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" ] __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 # Unicode-like literals
def u(s): def u(s):
return s.decode('utf-8') 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