mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-08-19 21:03:14 -07:00
Update pyxdg to 0.26
This commit is contained in:
parent
41ccbfdede
commit
79011dbbc1
12 changed files with 1568 additions and 1144 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
1530
libs/xdg/Menu.py
1530
libs/xdg/Menu.py
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
700
libs/xdg/Mime.py
700
libs/xdg/Mime.py
|
@ -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.``
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ]
|
||||
|
||||
__version__ = "0.25"
|
||||
__version__ = "0.26"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue