Source code for shell

# -*- coding: utf-8 -*-
ur"""Wrappers around standard functionality from the semi-independent Windows Shell
subsystem which powers the desktop, shortcuts, special folders, property sheets &c.

Implemented so far:

* Shortcuts: use the :func:`shortcut` function to edit or create desktop shortcuts
* [EXPERIMENTAL] Properties: use the :func:`properties` function to expose property sheet data
* Standard folders: commonly-accessed shell folders are exposed at module level, eg :func:`desktop`,
    :func:`startup`, :func:`recent`
"""
from __future__ import unicode_literals

import os, sys
import binascii

from win32com import storagecon
from win32com.shell import shell, shellcon
from win32com import storagecon
import win32api
import pythoncom
import pywintypes

from winsys import core, constants, exc, fs, utils

CSIDL = constants.Constants.from_pattern("CSIDL_*", namespace=shellcon)
STGM = constants.Constants.from_pattern("STGM_*", namespace=storagecon)
STGFMT = constants.Constants.from_pattern("STGFMT_*", namespace=storagecon)
FMTID = constants.Constants.from_pattern("FMTID_*", namespace=pythoncom)
FMTID.update(constants.Constants.from_pattern("FMTID_*", namespace=shell))
PIDSI = constants.Constants.from_pattern("PIDSI_*", namespace=storagecon)
PIDDSI = constants.Constants.from_pattern("PIDDSI_*", namespace=storagecon)
PIDMSI = constants.Constants.from_pattern("PIDMSI_*", namespace=shellcon)
PIDASI = constants.Constants.from_pattern("PIDASI_*", namespace=shellcon)
PID_VOLUME = constants.Constants.from_pattern("PID_VOLUME_*", namespace=shellcon)
SHCONTF = constants.Constants.from_pattern("SHCONTF_*", namespace=shellcon)
SHGDN = constants.Constants.from_pattern("SHGDN_*", namespace=shellcon)
SLGP = constants.Constants.from_pattern("SLGP_*", namespace=shell)
SFGAO = constants.Constants.from_pattern("SFGAO_*", namespace=shellcon)

PROPERTIES = {
    FMTID.SummaryInformation : PIDSI,
    FMTID.DocSummaryInformation : PIDDSI,
    FMTID.MediaFileSummaryInformation : PIDMSI,
    FMTID.AudioSummaryInformation : PIDASI,
    FMTID.Volume : PID_VOLUME,
}

[docs]class x_shell(exc.x_winsys): pass
class x_not_a_shortcut(x_shell): pass WINERROR_MAP = { } wrapped = exc.wrapper(WINERROR_MAP, x_shell) def _rpidl(parent, child): l = len(parent) if child[:l] != parent: raise x_shell("Parent %s is not related to child %s" % (parent, child)) else: return child[l:] _desktop = shell.SHGetDesktopFolder() PyIShellFolder = type(_desktop) # # Although this can be done in one call, Win9x didn't # support it, so I added this workaround. # def get_path(folder_id): return fs.entry(shell.SHGetPathFromIDList(shell.SHGetSpecialFolderLocation(0, folder_id))) def desktop_folder(common=0): "What folder is equivalent to the current desktop?" return get_path((shellcon.CSIDL_DESKTOP, shellcon.CSIDL_COMMON_DESKTOPDIRECTORY)[common]) def special_folder(folder_id): return shell.SHGetSpecialFolderPath(None, CSIDL.constant(folder_id), 0) def application_data(common=0): "What folder holds application configuration files?" return get_path((CSIDL.APPDATA, CSIDL.COMMON_APPDATA)[common]) def favourites(common=0): "What folder holds the Explorer favourites shortcuts?" return get_path((CSIDL.FAVORITES, CSIDL.COMMON_FAVORITES)[common]) bookmarks = favourites def start_menu(common=0): "What folder holds the Start Menu shortcuts?" return get_path((CSIDL.STARTMENU, CSIDL.COMMON_STARTMENU)[common]) def programs(common=0): "What folder holds the Programs shortcuts (from the Start Menu)?" return get_path((CSIDL.PROGRAMS, CSIDL.COMMON_PROGRAMS)[common]) def startup(common=0): "What folder holds the Startup shortcuts (from the Start Menu)?" return get_path((shellcon.CSIDL_STARTUP, shellcon.CSIDL_COMMON_STARTUP)[common]) def personal_folder(): "What folder holds the My Documents files?" return get_path(shellcon.CSIDL_PERSONAL) my_documents = personal_folder def recent(): "What folder holds the Documents shortcuts (from the Start Menu)?" return get_path(shellcon.CSIDL_RECENT) def sendto(): "What folder holds the SendTo shortcuts (from the Context Menu)?" return get_path(shellcon.CSIDL_SENDTO) # # Internally abstracted function to handle one # of several shell-based file manipulation # routines. Not all the possible parameters # are covered which might be passed to the # underlying SHFileOperation API call, but # only those which seemed useful to me at # the time. # def _file_operation( operation, source_path, target_path=None, allow_undo=True, no_confirm=False, rename_on_collision=True, silent=False, hWnd=None ): # # At present the Python wrapper around SHFileOperation doesn't # allow lists of files. Hopefully it will at some point, so # take account of it here. # If you pass this shell function a "/"-separated path with # a wildcard, eg c:/temp/*.tmp, it gets confused. It's ok # with a backslash, so convert here. # source_path = source_path or "" if isinstance(source_path, basestring): source_path = os.path.abspath(source_path) else: source_path = [os.path.abspath(i) for i in source_path] target_path = target_path or "" if isinstance(target_path, basestring): target_path = os.path.abspath(target_path) else: target_path = [os.path.abspath(i) for i in target_path] flags = 0 if allow_undo: flags |= shellcon.FOF_ALLOWUNDO if no_confirm: flags |= shellcon.FOF_NOCONFIRMATION if rename_on_collision: flags |= shellcon.FOF_RENAMEONCOLLISION if silent: flags |= shellcon.FOF_SILENT result, n_aborted = shell.SHFileOperation( (hWnd or 0, operation, source_path, target_path, flags, None, None) ) if result <> 0: raise x_winshell, result elif n_aborted: raise x_winshell, "%d operations were aborted by the user" % n_aborted def copy_file( source_path, target_path, allow_undo=True, no_confirm=False, rename_on_collision=True, silent=False, hWnd=None ): """Perform a shell-based file copy. Copying in this way allows the possibility of undo, auto-renaming, and showing the "flying file" animation during the copy. The default options allow for undo, don't automatically clobber on a name clash, automatically rename on collision and display the animation. """ _file_operation( shellcon.FO_COPY, source_path, target_path, allow_undo, no_confirm, rename_on_collision, silent, hWnd ) def move_file( source_path, target_path, allow_undo=True, no_confirm=False, rename_on_collision=True, silent=False, hWnd=None ): """Perform a shell-based file move. Moving in this way allows the possibility of undo, auto-renaming, and showing the "flying file" animation during the copy. The default options allow for undo, don't automatically clobber on a name clash, automatically rename on collision and display the animation. """ _file_operation( shellcon.FO_MOVE, source_path, target_path, allow_undo, no_confirm, rename_on_collision, silent, hWnd ) def rename_file( source_path, target_path, allow_undo=True, no_confirm=False, rename_on_collision=True, silent=False, hWnd=None ): """Perform a shell-based file rename. Renaming in this way allows the possibility of undo, auto-renaming, and showing the "flying file" animation during the copy. The default options allow for undo, don't automatically clobber on a name clash, automatically rename on collision and display the animation. """ _file_operation( shellcon.FO_RENAME, source_path, target_path, allow_undo, no_confirm, rename_on_collision, silent, hWnd ) def delete_file( source_path, allow_undo=True, no_confirm=False, rename_on_collision=True, silent=False, hWnd=None ): """Perform a shell-based file delete. Deleting in this way uses the system recycle bin, allows the possibility of undo, and showing the "flying file" animation during the delete. The default options allow for undo, don't automatically clobber on a name clash, automatically rename on collision and display the animation. """ _file_operation( shellcon.FO_DELETE, source_path, None, allow_undo, no_confirm, rename_on_collision, silent, hWnd )
[docs]class Shortcut(core._WinSysObject): def __init__(self, filepath=core.UNSET, **kwargs): self._shell_link = wrapped( pythoncom.CoCreateInstance, shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink ) self.filepath = filepath if self.filepath and os.path.exists(self.filepath): wrapped( self._shell_link.QueryInterface, pythoncom.IID_IPersistFile ).Load( self.filepath ) for k, v in kwargs.iteritems(): setattr(self, k, v) def as_string(self): return ("-> %s" % self.path) or "-unsaved-" def dumped(self, level=0): output = [] output.append(self.as_string()) output.append("") for attribute in ["arguments", "description", "hotkey", "icon_location", "path", "show_cmd", "working_directory"]: output.append("%s: %s" % (attribute, getattr(self, attribute))) return utils.dumped("\n".join(output), level) @classmethod def from_lnk(cls, lnk_filepath): return cls(lnk_filepath) @classmethod def from_target(cls, target_filepath, lnk_filepath=core.UNSET, **kwargs): target_filepath = os.path.abspath(target_filepath) if lnk_filepath is core.UNSET: lnk_filepath = os.path.join(os.getcwd(), os.path.basename(target_filepath) + ".lnk") return cls( lnk_filepath, path=target_filepath, **kwargs ) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.write() def _get_arguments(self): return self._shell_link.GetArguments() def _set_arguments(self, arguments): self._shell_link.SetArguments(arguments) arguments = property(_get_arguments, _set_arguments) def _get_description(self): return self._shell_link.GetDescription() def _set_description(self, description): self._shell_link.SetDescription(description) description = property(_get_description, _set_description) def _get_hotkey(self): return self._shell_link.GetHotkey() def _set_hotkey(self, hotkey): self._shell_link.SetHotkey(hotkey) hotkey = property(_get_hotkey, _set_hotkey) def _get_icon_location(self): path, index = self._shell_link.GetIconLocation() return fs.entry(path), index def _set_icon_location(self, icon_location): self._shell_link.SetIconLocation(*icon_location) icon_location = property(_get_icon_location, _set_icon_location) def _get_path(self): filepath, data = self._shell_link.GetPath(SLGP.UNCPRIORITY) return fs.entry(filepath) def _set_path(self, path): self._shell_link.SetPath(path) path = property(_get_path, _set_path) def _get_show_cmd(self): return self._shell_link.GetShowCmd() def _set_show_cmd(self, show_cmd): self._shell_link.SetShowCmd(show_cmd) show_cmd = property(_get_show_cmd, _set_show_cmd) def _get_working_directory(self): return fs.dir(self._shell_link.GetWorkingDirectory()) def _set_working_directory(self, working_directory): self._shell_link.SetWorkingDirectory(working_directory) working_directory = property(_get_working_directory, _set_working_directory) def write(self, filepath=core.UNSET): if not filepath: filepath = self.filepath if filepath is None: raise x_shell(errmsg="Must specify a filepath for an unsaved shortcut") wrapped( self._shell_link.QueryInterface, pythoncom.IID_IPersistFile ).Save( self.filepath, filepath == self.filepath ) self.filepath = filepath return self
[docs]def shortcut(source=core.UNSET): if source is None: return None elif source is core.UNSET: return Shortcut() elif isinstance(source, Shortcut): return source elif source.endswith(".lnk"): return Shortcut.from_lnk(source) else: return Shortcut.from_target(source)
class PropertySet(core._WinSysObject): def __init__(self, property_set_storage, fmtid): self.property_set_storage = property_set_storage self.fmtid = fmtid def as_string(self): return FMTID.name_from_value(self.fmtid) def as_dict(self): try: property_storage = self.property_set_storage.Open(self.fmtid, STGM.READ | STGM.SHARE_EXCLUSIVE) except pythoncom.com_error, error: if error.strerror == 'STG_E_FILENOTFOUND': return {} else: raise properties = {} for name, property_id, vartype in property_storage: if name is None: property_names = PROPERTIES.get(self.fmtid, constants.Constants()) name = property_names.name_from_value(property_id, unicode(hex(property_id))) try: for value in property_storage.ReadMultiple([property_id]): properties[name] = value # # There are certain values we can't read; they # raise type errors from within the pythoncom # implementation, thumbnail # except TypeError: properties[name] = None return properties def __getattr__(self, attr): return self.as_dict()[attr] def keys(self): return list(self.as_dict().iterkeys()) def values(self): return list(self.as_dict().itervalues()) def items(self): return list(self.as_dict().iteritems()) class Properties(core._WinSysObject): def __init__(self, filepath): self._pidl, _ = shell.SHILCreateFromPath(os.path.abspath(filepath), 0) self._pss = _desktop.BindToStorage(self._pidl, None, pythoncom.IID_IPropertySetStorage) def property_set(self, fmtid): return PropertySet(self._pss, FMTID.constant(fmtid)) __getattr__ = property_set __getitem__ = property_set def __iter__(self): for fmtid, clsid, flags, ctime, mtime, atime in self._pss: yield self.property_set(fmtid) if fmtid == FMTID.DocSummaryInformation: fmtid = pythoncom.FMTID_UserDefinedProperties yield self.property_set(fmtid) def dumped(self, level=0): output = [] for ps in self: output.append("%s:\n%s" % (FMTID.name_from_value(ps.fmtid), utils.dumped_dict(ps.as_dict(), level))) return utils.dumped("\n".join(output), level)
[docs]def properties(source): if source is None: return None elif isinstance(source, Properties): return source else: return Properties(source)
PyIID = type(pywintypes.IID("{00000000-0000-0000-0000-000000000000}")) # # Shell functions all work on the basis of querying a parent # object for details of its children. With the exception of # the root desktop object, all other shell items have a parent # and child. Iterating over a shell parent will yield the relative # pidls of its child items. # class ShellEntry(core._WinSysObject): def __init__(self, parent_obj, rpidl): self._parent_obj = parent_obj self._rpidl = rpidl def as_string(self): return self._parent_obj.GetDisplayNameOf(self._rpidl, SHGDN.NORMAL) @classmethod def from_pidl(cls, pidl, parent_obj=None): if parent_obj is None: # # pidl is absolute # parent_obj = _desktop.BindToObject(pidl[:-1], None, shell.IID_IShellFolder) rpidl = pidl[-1:] else: # # pidl is relative # rpidl = pidl return cls(parent_obj, rpidl) @classmethod def from_path(cls, path): _, pidl, flags = _desktop.ParseDisplayName(0, None, path, SFGAO.FOLDER) if flags & SFGAO.FOLDER: return ShellFolder.from_pidl(pidl) else: return ShellItem.from_pidl(pidl) @classmethod def factory(cls, shell_entry=core.UNSET, parent_obj=None): if shell_entry is None: return None elif shell_entry is core.UNSET: return ShellFolder(_desktop, []) elif isinstance(shell_entry, ShellEntry): return shell_entry elif isinstance(shell_entry, int): return ShellFolder.from_pidl(shell.SHGetSpecialFolderLocation(0, shell_entry)) else: if isinstance(shell_entry, list): return cls.from_pidl(shell_entry, parent_obj) elif isinstance(shell_entry,basestring): return cls.from_path(shell_entry) @property def name(self): return self.as_string() @property def attributes(self): return constants.Attributes(self._parent_obj.GetAttributesOf([self._rpidl], -1), SFGAO) def attribute(self, attr): """Determine whether this entry has this attribute set :param attr: one of :const:`SFGAO` :returns: `True` if set otherwise `False` """ value = SFGAO.constant(attr) return bool(self._parent_obj.GetAttributesOf([self._rpidl], value) & value) class ShellItem(ShellEntry): pass class ShellFolder(ShellEntry): def __init__(self, parent_obj, rpidl): ShellEntry.__init__(self, parent_obj, rpidl) if self._rpidl: self._folder = self._parent_obj.BindToObject(self._rpidl, None, shell.IID_IShellFolder) else: self._folder = self._parent_obj #~ def __getattr__(self, attr): #~ return getattr(self.shell_folder, attr) def __iter__(self): for folder in self._folder.EnumObjects(None, SHCONTF.FOLDERS): yield ShellFolder(self._folder, folder) for item in self._folder.EnumObjects(None, SHCONTF.NONFOLDERS): yield ShellItem(self._folder, item) def walk(self, depthfirst=False): top = self._folder folders = [self.factory(f, self._folder) for f in self._folder.EnumObjects(None, SHCONTF.FOLDERS)] non_folders = [ShellItem(f, self._folder) for f in self._folder.EnumObjects(None, SHCONTF.NONFOLDERS)] if not depthfirst: yield top, folders, non_folders shell_entry = ShellEntry.factory def shell_folder(shell_folder=core.UNSET, parent=core.UNSET): if shell_folder is None: return None elif shell_folder is core.UNSET: return ShellFolder([], _desktop) elif isinstance(shell_folder, PyIShellFolder): return ShellFolder(shell_folder) elif isinstance(shell_folder, basestring): pidl, flags = shell.SHILCreateFromPath(os.path.abspath(shell_folder), 0) if pidl is None: pidl = shell.SHGetFolderLocation(None, CSIDL.constant(shell_folder), None, 0) return ShellFolder(_desktop.BindToObject(pidl, None, shell.IID_IShellFolder)) elif isinstance(shell_folder, list): if parent is core.UNSET: raise x_shell(errctx="shell_folder", errmsg="Cannot bind to PIDL without parent") return ShellFolder(parent.BindToObject(shell_folder, None, shell.IID_IShellFolder)) else: raise x_shell(errctx="shell_folder") desktop = ShellFolder(_desktop, [])