#! /usr/bin/env python
"""Module providing GUI capability to prune any directory.
The code presented in this module is for the purposes of: (1) ascertaining
the space taken up by a directory, its files, its sub-directories, and its
sub-files; (2) allowing for the removal of the sub-files, sub-directories,
files, and directory found in the first purpose; (3) giving the user a GUI
to accomplish said purposes in a convenient way that is easily accessible."""
################################################################################
__author__ = 'Stephen "Zero" Chappell '
__date__ = '15 February 2011'
__version__ = '$Revision: 298 $'
################################################################################
# Import several GUI libraries.
import tkinter
import tkinter.ttk
import tkinter.filedialog
import tkinter.messagebox
# Import other needed modules.
import zlib
import base64
import os
import math
################################################################################
ICON = b'eJxjYGAEQgEBBiApwZDBzMAgxsDAoAHEQCEGBQaIOAwkQDE2UOSkiUM\
Gp/rlyd740Ugzf8/uXROxAaA4VvVAqcfYAFCcoHqge4hR/+btWwgCqoez8aj//fs\
XWiAARfCrhyCg+XA2HvV/YACoHs4mRj0ywKWe1PD//p+B4QMOmqGeMAYAAY/2nw=='
################################################################################
class GUISizeTree(tkinter.ttk.Frame):
"Widget for examining size of directory with optional deletion."
WARN = True # Should warnings be made for permanent operations?
MENU = True # Should the (destructive) context menu be enabled?
# Give names to columns.
CLMS = 'total_size', 'file_size', 'path'
TREE = '#0'
########################################################################
# Allow direct execution of GUISizeTree widget.
@classmethod
def main(cls):
"Create an application containing a single GUISizeTree widget."
tkinter.NoDefaultRoot()
root = cls.create_application_root()
cls.attach_window_icon(root, ICON)
view = cls.setup_class_instance(root)
root.mainloop()
@staticmethod
def create_application_root():
"Create and configure the main application window."
root = tkinter.Tk()
root.minsize(430, 215)
root.title('Directory Pruner')
root.option_add('*tearOff', tkinter.FALSE)
return root
@staticmethod
def attach_window_icon(root, icon):
"Generate and use the icon in the window's corner."
with open('tree.ico', 'wb') as file:
file.write(zlib.decompress(base64.b64decode(ICON)))
root.iconbitmap('tree.ico')
os.remove('tree.ico')
@classmethod
def setup_class_instance(cls, root):
"Build GUISizeTree instance that expects resizing."
instance = cls(root)
instance.grid(row=0, column=0, sticky=tkinter.NSEW)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
return instance
########################################################################
# Initialize the GUISizeTree object.
def __init__(self, master=None, **kw):
"Initialize the GUISizeTree instance and configure for operation."
super().__init__(master, **kw)
# Initialize and configure this frame widget.
self.capture_root()
self.create_widgets()
self.create_supports()
self.create_bindings()
self.configure_grid()
self.configure_tree()
self.configure_menu()
# Set focus to path entry.
self.__path.focus_set()
def capture_root(self):
"Capture the root (Tk instance) of this application."
widget = self.master
while not isinstance(widget, tkinter.Tk):
widget = widget.master
self.__tk = widget
def create_widgets(self):
"Create all the widgets that will be placed in this frame."
self.__label = tkinter.ttk.Button(self, text='Path:',
command=self.choose)
self.__path = tkinter.ttk.Entry(self, cursor='xterm')
self.__run = tkinter.ttk.Button(self, text='Search',
command=self.search)
self.__cancel = tkinter.ttk.Button(self, text='Cancel',
command=self.stop_search)
self.__progress = tkinter.ttk.Progressbar(self,
orient=tkinter.HORIZONTAL)
self.__tree = tkinter.ttk.Treeview(self, columns=self.CLMS,
selectmode=tkinter.BROWSE)
self.__scroll_1 = tkinter.ttk.Scrollbar(self, orient=tkinter.VERTICAL,
command=self.__tree.yview)
self.__scroll_2 = tkinter.ttk.Scrollbar(self, orient=tkinter.HORIZONTAL,
command=self.__tree.xview)
self.__grip = tkinter.ttk.Sizegrip(self)
def create_supports(self):
"Create all GUI elements not placed directly in this frame."
self.__menu = tkinter.Menu(self)
self.create_directory_browser()
self.create_error_message()
self.create_warning_message()
def create_directory_browser(self):
"Find root of file system and create directory browser."
head, tail = os.getcwd(), True
while tail:
head, tail = os.path.split(head)
self.__dialog = tkinter.filedialog.Directory(self, initialdir=head)
def create_error_message(self):
"Create error message when trying to search bad path."
options = {'title': 'Path Error',
'icon': tkinter.messagebox.ERROR,
'type': tkinter.messagebox.OK,
'message': 'Directory does not exist.'}
self.__error = tkinter.messagebox.Message(self, **options)
def create_warning_message(self):
"Create warning message for permanent operations."
options = {'title': 'Important Warning',
'icon': tkinter.messagebox.QUESTION,
'type': tkinter.messagebox.YESNO,
'message': '''\
You cannot undo these operations.
Are you sure you want to do this?'''}
self.__warn = tkinter.messagebox.Message(self, **options)
def create_bindings(self):
"Bind the widgets to any events they will need to handle."
self.__label.bind('', self.choose)
self.__path.bind('', self.select_all)
self.__path.bind('', lambda event: 'break')
self.__path.bind('', self.search)
self.__run.bind('', self.search)
self.__cancel.bind('', self.stop_search)
self.bind_right_click(self.__tree, self.open_menu)
@staticmethod
def select_all(event):
"Select all of the contents in this Entry widget."
event.widget.selection_range(0, tkinter.END)
return 'break'
def bind_right_click(self, widget, action):
"Bind action to widget while considering Apple computers."
if self.__tk.tk.call('tk', 'windowingsystem') == 'aqua':
widget.bind('<2>', action)
widget.bind('', action)
else:
widget.bind('<3>', action)
def configure_grid(self):
"Place all widgets on the grid in their respective locations."
self.__label.grid(row=0, column=0)
self.__path.grid(row=0, column=1, sticky=tkinter.EW)
self.__run.grid(row=0, column=2, columnspan=2)
self.__run.grid_remove()
self.__cancel.grid(row=0, column=2, columnspan=2)
self.__cancel.grid_remove()
self.__run.grid()
self.__progress.grid(row=1, column=0, columnspan=4, sticky=tkinter.EW)
self.__tree.grid(row=2, column=0, columnspan=3, sticky=tkinter.NSEW)
self.__scroll_1.grid(row=2, column=3, sticky=tkinter.NS)
self.__scroll_2.grid(row=3, column=0, columnspan=3, sticky=tkinter.EW)
self.__grip.grid(row=3, column=3, sticky=tkinter.SE)
# Configure the grid to automatically resize internal widgets.
self.grid_rowconfigure(2, weight=1)
self.grid_columnconfigure(1, weight=1)
def configure_tree(self):
"Configure the Treeview widget."
# Setup the headings.
self.__tree.heading(self.TREE, text=' Name', anchor=tkinter.W,
command=self.sort_name)
self.__tree.heading(self.CLMS[0], text=' Total Size', anchor=tkinter.W,
command=self.sort_total_size)
self.__tree.heading(self.CLMS[1], text=' File Size', anchor=tkinter.W,
command=self.sort_file_size)
self.__tree.heading(self.CLMS[2], text=' Path', anchor=tkinter.W,
command=self.sort_path)
# Setup the columns.
self.__tree.column(self.TREE, minwidth=100, width=200)
self.__tree.column(self.CLMS[0], minwidth=100, width=200)
self.__tree.column(self.CLMS[1], minwidth=100, width=200)
self.__tree.column(self.CLMS[2], minwidth=100, width=200)
# Connect the Scrollbars.
self.__tree.configure(yscrollcommand=self.__scroll_1.set)
self.__tree.configure(xscrollcommand=self.__scroll_2.set)
def configure_menu(self):
"Configure the (context) Menu widget."
# Shortcut for narrowing the search.
self.__menu.add_command(label='Search Directory',
command=self.search_dir)
self.__menu.add_separator()
# Operations committed on directory.
self.__menu.add_command(label='Remove Directory', command=self.rm_dir)
self.__menu.add_command(label='Remove Files', command=self.rm_files)
self.__menu.add_separator()
# Operations that recurse on sub-directories.
self.__menu.add_command(label='Remove Sub-directories',
command=self.rm_subdirs)
self.__menu.add_command(label='Remove Sub-files',
command=self.rm_subfiles)
# Only add "Open Directory" command on Windows.
if hasattr(os, 'startfile'):
self.__menu.add_separator()
self.__menu.add_command(label='Open Directory',
command=self.open_dir)
########################################################################
# This property is used to control access to operations.
def __get_operations_enabled(self):
"Return if run button is in normal state."
return self.__run['state'].string == tkinter.NORMAL
def __set_operations_enabled(self, value):
"Enable or disable run button's state according to value."
self.__run['state'] = tkinter.NORMAL if value else tkinter.DISABLED
operations_enabled = property(__get_operations_enabled,
__set_operations_enabled,
doc="Flag controlling certain operations")
########################################################################
# Handle path browsing and searching actions.
def choose(self, event=None):
"Show directory browser and set path as needed."
path = self.__dialog.show()
if path:
# Entry is cleared before absolute path is added.
self.__path.delete(0, tkinter.END)
self.__path.insert(0, os.path.abspath(path))
def search(self, event=None):
"Search the path and display the size of the directory."
if self.operations_enabled:
self.operations_enabled = False
# Get absolute path and check existence.
path = os.path.abspath(self.__path.get())
if os.path.isdir(path):
# Enable operations after finishing search.
self.__search(path)
self.operations_enabled = True
else:
self.shake()
def __search(self, path):
"Execute the search procedure and display in Treeview."
self.__run.grid_remove()
self.__cancel.grid()
children = self.start_search()
try:
tree = SizeTree(self.update_search, path)
except StopIteration:
self.handle_stop_search(children)
else:
self.finish_search(children, tree)
self.__cancel.grid_remove()
self.__run.grid()
########################################################################
# Execute various phases of a search.
def start_search(self):
"Edit the GUI in preparation for executing a search."
self.__stop_search = False
children = Apply(TreeviewNode(self.__tree).children)
children.detach()
self.__progress.configure(mode='indeterminate', maximum=100)
self.__progress.start()
return children
def update_search(self):
"Check if search has been stopped and update the GUI."
self.validate_search()
self.update()
def validate_search(self):
"Check that the current search action is valid."
if self.__stop_search:
self.__stop_search = False
raise StopIteration('Search has been canceled!')
def stop_search(self, event=None):
"Cancel a search by setting its stop flag."
self.__stop_search = True
def handle_stop_search(self, children):
"Reset the Treeview and Progressbar on premature termination."
children.reattach()
self.__progress.stop()
self.__progress['mode'] = 'determinate'
def finish_search(self, children, tree):
"Delete old children, update Progressbar, and update Treeview."
children.delete()
self.__progress.stop()
self.__progress.configure(mode='determinate',
maximum=tree.total_nodes+1)
node = TreeviewNode(self.__tree).append(tree.name)
try:
self.build_tree(node, tree)
except StopIteration:
pass
########################################################################
# Handle Treeview column sorting events initiated by user.
def sort_name(self):
"Sort children of selected node by name."
TreeviewNode.current(self.__tree).sort_name()
def sort_total_size(self):
"Sort children of selected node by total size."
TreeviewNode.current(self.__tree).sort_total_size()
def sort_file_size(self):
"Sort children of selected node by file size."
TreeviewNode.current(self.__tree).sort_file_size()
def sort_path(self):
"Sort children of selected node by path."
TreeviewNode.current(self.__tree).sort_path()
########################################################################
# Handle right-click events on the Treeview widget.
def open_menu(self, event):
"Select Treeview row and show context menu if allowed."
item = event.widget.identify_row(event.y)
if item:
event.widget.selection_set(item)
if self.menu_allowed:
self.__menu.post(event.x_root, event.y_root)
@property
def menu_allowed(self):
"Check if menu is enabled along with operations."
return self.MENU and self.operations_enabled
def search_dir(self):
"Search the path of the currently selected row."
path = TreeviewNode.current(self.__tree).path
self.__path.delete(0, tkinter.END)
self.__path.insert(0, path)
self.search()
def rm_dir(self):
"Remove the currently selected directory."
if self.commit_permanent_operation:
self.do_remove_directory()
def rm_files(self):
"Remove the files in the currently selected directory."
if self.commit_permanent_operation:
self.do_remove_files()
def rm_subdirs(self):
"Remove the sub-directories of the currently selected directory."
if self.commit_permanent_operation:
self.do_remove_subdirectories()
def rm_subfiles(self):
"Remove the sub-files of the currently selected directory."
if self.commit_permanent_operation:
self.do_remove_subfiles()
@property
def commit_permanent_operation(self):
"Check if warning should be issued before committing operation."
return not self.WARN or self.__warn.show() == tkinter.messagebox.YES
def open_dir(self):
"Open up the current directory (only available on Windows)."
os.startfile(TreeviewNode.current(self.__tree).path)
########################################################################
# Execute actions requested by context menu.
def do_remove_directory(self):
"Remove a directory and all of its sub-directories."
self.begin_rm()
# Get the current Treeview node and delete it.
node = TreeviewNode.current(self.__tree)
directory_size, path = node.total_size, node.path
position, parent = node.position, node.delete(True)
# Delete the entire directory at path.
self.__rm_dir(self.update, path, True, True)
if os.path.isdir(path):
# Add the directory back to the Treeview.
tree = SizeTree(self.update, path)
self.begin_rm_update(tree.total_nodes + 1)
# Rebuild the Treeview under the parent.
node = parent.insert(position, tree.name)
self.build_tree(node, tree)
# New directory size.
total_size = tree.total_size
else:
self.begin_rm_update()
# New directory size.
total_size = 0
# If the size has changed, update parent nodes.
if directory_size != total_size:
diff = total_size - directory_size
self.update_parents(parent, diff)
self.end_rm()
def do_remove_files(self):
"Remove all of the files in the selected directory."
# Delete files in the directory and get its new size.
node = TreeviewNode.current(self.__tree)
total_size = self.__rm_files(node.path)
# Update current and parent nodes if the size changed.
if node.file_size != total_size:
diff = total_size - node.file_size
node.file_size = total_size
node.total_size += diff
self.update_parents(node.parent, diff)
def do_remove_subdirectories(self):
"Remove all subdirectories in the directory."
self.begin_rm()
# Remove all the children nodes in Viewtree.
node = TreeviewNode.current(self.__tree)
for child in node.children:
child.delete()
# Delete all of the subdirectories and their files.
self.__rm_dir(self.update, node.path, True)
# Find out what subdirectories could not be deteled.
tree = SizeTree(self.update, node.path)
self.begin_rm_update(tree.total_nodes)
if tree.total_nodes:
# Rebuild the Viewtree as needed.
self.build_tree(node, tree, False)
# Fix node and prepare to update parents.
diff = node.total_size - tree.total_size
node.total_size = tree.total_size
else:
# Fix node and prepare to update parents.
diff = node.file_size - node.total_size
node.total_size = node.file_size
# Update parents with new size.
self.update_parents(node.parent, diff)
self.end_rm()
def do_remove_subfiles(self):
"Remove all subfiles while keeping subdirectories in place."
self.begin_rm()
# Delete all subfiles from current directory.
node = TreeviewNode.current(self.__tree)
self.__rm_dir(self.update, node.path)
# Build a new SizeTree to find the result.
tree = SizeTree(self.update, node.path)
self.begin_rm_update(tree.total_nodes)
# Record the difference and patch the Viewtree.
diff = tree.total_size - node.total_size
self.patch_tree(node, tree)
# Fix all parent nodes with the correct size.
self.update_parents(node.parent, diff)
self.end_rm()
########################################################################
# Help update Progressbar in removal process.
def begin_rm(self):
"Start a long-running removal operation."
self.operations_enabled = False
self.__progress.configure(mode='indeterminate', maximum=100)
self.__progress.start()
def begin_rm_update(self, nodes=0):
"Move to determinate mode of updating the Viewtree."
self.__progress.stop()
self.__progress.configure(mode='determinate', maximum=nodes)
def end_rm(self):
"Finish removal process by enabling operations."
self.operations_enabled = True
########################################################################
# Help in removing directories and files.
@staticmethod
def __rm_dir(callback, path, rm_dir=False, rm_root=False):
"Remove directory at path, respecting the flags."
for root, dirs, files in os.walk(path, False):
# Ignore path if rm_root is false.
if rm_root or root != path:
callback()
for name in files:
file_name = os.path.join(root, name)
# Remove file while catching errors.
try:
os.remove(file_name)
except OSError:
pass
# Ignore directory if rm_dir is false.
if rm_dir:
try:
os.rmdir(root)
except OSError:
pass
@staticmethod
def __rm_files(path):
"Remove files in path and get remaining space."
total_size = 0
# Find all files in directory of path.
for name in os.listdir(path):
path_name = os.path.join(path, name)
if os.path.isfile(path_name):
# Try to remove any file that may have been found.
try:
os.remove(path_name)
except OSError:
try:
# If there was an error, try to get the filesize.
total_size += os.path.getsize(path_name)
except OSError:
pass
# Return best guess of space still occupied.
return total_size
########################################################################
# Update the Viewtree nodes after creating a SizeTree object.
def build_tree(self, node, tree, update_node=True):
"Build the Treeview while updating the Progressbar."
self.validate_search()
if update_node:
self.sync_nodes(node, tree)
self.add_children(node, tree)
def sync_nodes(self, node, tree):
"Update attributes on node and refresh GUI."
# Copy the information on the node.
node.total_size = tree.total_size
node.file_size = tree.file_size
node.path = tree.path
# Update the Progressbar and GUI.
self.__progress.step()
self.update()
def patch_tree(self, node, tree):
"Patch differences between node and tree."
node.total_size = tree.total_size
node.file_size = tree.file_size
self.patch_children(node, tree)
self.add_children(node, tree)
def add_children(self, node, tree):
"Build and traverse all child nodes."
for child in tree.children:
subnode = node.append(child.name)
self.build_tree(subnode, child)
def patch_children(self, node, tree):
"Patch Viewtree based on children of SizeTree."
for subnode in node.children:
child = tree.pop_child(subnode.name)
if child is None:
# Directory is gone.
subnode.delete()
else:
# Dig down further in tree.
self.__progress.step()
self.update()
self.patch_tree(subnode, child)
@staticmethod
def update_parents(node, diff):
"Add in difference to node and parents."
while not node.root:
node.total_size += diff
node = node.parent
########################################################################
# Show an error when searching paths that do not exist.
def shake(self, force=False):
"Prepare to shake the application's root window."
if force:
tkinter._tkinter.setbusywaitinterval(20)
elif tkinter._tkinter.getbusywaitinterval() != 20:
# Show error message if not running at 50 FPS.
self.__error.show()
self.operations_enabled = True
return
# Shake window at 50 FPS.
self.after_idle(self.__shake)
def __shake(self, frame=0):
"Animate each step of shaking the root window."
frame += 1
# Get the window's location and update the X position.
x, y = map(int, self.__tk.geometry().split('+')[1:])
x += round(math.sin(math.pi * frame / 2.5) * \
math.sin(math.pi * frame / 50) * 5)
self.__tk.geometry('+{}+{}'.format(x, y))
if frame < 50:
# Schedule next step in the animation.
self.after(20, self.__shake, frame)
else:
# Enable operations after one second.
self.operations_enabled = True
################################################################################
class TreeviewNode:
"Interface to allow easier interaction with Treeview instance."
@classmethod
def current(cls, tree):
"Take a tree view and return its currently selected node."
node = tree.selection()
return cls(tree, node[0] if node else node)
########################################################################
# Standard Treeview Operations
__slots__ = '__tree', '__node'
def __init__(self, tree, node=''):
"Initialize the TreeviewNode object (root if node not given)."
self.__tree = tree
self.__node = node
def __str__(self):
"Return a string representation of this node."
return '''\
NODE: {!r}
Name: {}
Total Size: {}
File Size: {}
Path {}\
'''.format(self.__node, self.name, self.total_size, self.file_size, self.path)
def insert(self, position, text):
"Insert a new node with text at position in current node."
node = self.__tree.insert(self.__node, position, text=text)
return TreeviewNode(self.__tree, node)
def append(self, text):
"Add a new node with text to the end of this node."
return self.insert(tkinter.END, text)
def move(self, parent, index):
"Insert this node under parent at index."
self.__tree.move(self.__node, parent, index)
def reattach(self, parent='', index=tkinter.END):
"Attach node to parent at index (defaults to end of root)."
self.move(parent, index)
def detach(self):
"Unlink this node from its parent but do not delete."
self.__tree.detach(self.__node)
def delete(self, get_parent=False):
"Delete this node (optionally, return parent)."
if self.__tree.exists(self.__node):
parent = self.parent if get_parent else None
self.__tree.delete(self.__node)
return parent
assert not get_parent, 'Cannot return parent!'
########################################################################
# Standard Treeview Properties
@property
def root(self):
"Return if this is the root node."
return self.__node == ''
@property
def parent(self):
"Return the parent of this node."
return TreeviewNode(self.__tree, self.__tree.parent(self.__node))
@property
def level(self):
"Return number of levels this node is under root."
count, node = 0, self
while not node.root:
node = node.parent
count += 1
return count
@property
def position(self):
"Return the position of this node in its parent."
return self.__tree.index(self.__node)
@property
def expanded(self):
"Return whether or not the node is current open."
value = self.__tree.item(self.__node, 'open')
return bool(value) and value.string == 'true'
@property
def children(self):
"Yield back each child of this node."
for child in self.__tree.get_children(self.__node):
yield TreeviewNode(self.__tree, child)
########################################################################
# Custom Treeview Properties
# (specific for application)
@property
def name(self):
"Return the name of this node (tree column)."
return self.__tree.item(self.__node, 'text')
def __get_total_size(self):
return parse(self.__tree.set(self.__node, GUISizeTree.CLMS[0]))
def __set_total_size(self, value):
self.__tree.set(self.__node, GUISizeTree.CLMS[0], convert(value))
total_size = property(__get_total_size, __set_total_size,
doc="Total size of this node (first column)")
def __get_file_size(self):
return parse(self.__tree.set(self.__node, GUISizeTree.CLMS[1]))
def __set_file_size(self, value):
self.__tree.set(self.__node, GUISizeTree.CLMS[1], convert(value))
file_size = property(__get_file_size, __set_file_size,
doc="File size of this node (second column)")
def __get_path(self):
return self.__tree.set(self.__node, GUISizeTree.CLMS[2])
def __set_path(self, value):
self.__tree.set(self.__node, GUISizeTree.CLMS[2], value)
path = property(__get_path, __set_path,
doc="Path of this node (third column)")
########################################################################
# Custom Treeview Sort Order
# (specific for application)
def sort_name(self):
"If the node is open, sort its children by name."
self.__sort(lambda child: child.name)
def sort_total_size(self):
"If the node is open, sort its children by total size."
self.__sort(lambda child: child.total_size)
def sort_file_size(self):
"If the node is open, sort its children by file size."
self.__sort(lambda child: child.file_size)
def sort_path(self):
"If the node is open, sort its children by path."
self.__sort(lambda child: child.path)
def __sort(self, key):
"Sort an expanded node's children by the given key."
if self.expanded:
nodes = list(self.children)
order = sorted(nodes, key=key)
if order == nodes:
order = reversed(order)
for child in order:
self.__tree.move(child.__node, self.__node, tkinter.END)
################################################################################
class SizeTree:
"Create a tree structure outlining a directory's size."
__slots__ = 'name path children file_size total_size total_nodes'.split()
def __init__(self, callback, path):
"Initialize the SizeTree object and search the path while updating."
callback() # Allow the GUI to be updated.
head, tail = os.path.split(path)
# Create attributes for this instance.
self.name = tail or head
self.path = path
self.children = []
self.file_size = 0
self.total_size = 0
self.total_nodes = 0
# Try searching this directory.
try:
dir_list = os.listdir(path)
except OSError:
pass
else:
# Examine each object in this directory.
for name in dir_list:
path_name = os.path.join(path, name)
if os.path.isdir(path_name):
# Create child nodes for subdirectories.
size_tree = SizeTree(callback, path_name)
self.children.append(size_tree)
self.total_size += size_tree.total_size
self.total_nodes += size_tree.total_nodes + 1
elif os.path.isfile(path_name):
# Try getting the size of files.
try:
self.file_size += os.path.getsize(path_name)
except OSError:
pass
# Add in the total file size to the total size.
self.total_size += self.file_size
def pop_child(self, name):
"Return a named child or None if not found."
for index, child in enumerate(self.children):
if child.name == name:
return self.children.pop(index)
########################################################################
def __str__(self):
"Return a representation of the tree formed by this object."
lines = [self.path]
self.__walk(lines, self.children, '')
return '\n'.join(lines)
@classmethod
def __walk(cls, lines, children, prefix):
"Generate lines based on children and keep track of prefix."
dir_prefix, walk_prefix = prefix + '+---', prefix + '| '
for pos, neg, child in cls.__enumerate(children):
if neg == -1:
dir_prefix, walk_prefix = prefix + '\\---', prefix + ' '
lines.append(dir_prefix + child.name)
cls.__walk(lines, child.children, walk_prefix)
@staticmethod
def __enumerate(sequence):
"Generate positive and negative indices for sequence."
length = len(sequence)
for count, value in enumerate(sequence):
yield count, count - length, value
################################################################################
class Apply(tuple):
"Create a container that can run a method from its contents."
def __getattr__(self, name):
"Get a virtual method to map and apply to the contents."
return self.__Method(self, name)
########################################################################
class __Method:
"Provide a virtual method that can be called on the array."
def __init__(self, array, name):
"Initialize the method with array and method name."
self.__array = array
self.__name = name
def __call__(self, *args, **kwargs):
"Execute method on contents with provided arguments."
name, error, buffer = self.__name, False, []
for item in self.__array:
attr = getattr(item, name)
try:
data = attr(*args, **kwargs)
except Exception as problem:
error = problem
else:
if not error:
buffer.append(data)
if error:
raise error
return tuple(buffer)
################################################################################
# Provide a way of converting byte sizes into strings.
def convert(number):
"Convert bytes into human-readable representation."
if not number:
return '0 Bytes'
assert 0 < number < 1 << 110, 'number out of range'
ordered = reversed(tuple(format_bytes(partition_number(number, 1 << 10))))
cleaned = ', '.join(item for item in ordered if item[0] != '0')
return cleaned
def partition_number(number, base):
"Continually divide number by base until zero."
div, mod = divmod(number, base)
yield mod
while div:
div, mod = divmod(div, base)
yield mod
def format_bytes(parts):
"Format partitioned bytes into human-readable strings."
for power, number in enumerate(parts):
yield '{} {}'.format(number, format_suffix(power, number))
def format_suffix(power, number):
"Compute the suffix for a certain power of bytes."
return (PREFIX[power] + 'byte').capitalize() + ('s' if number != 1 else '')
PREFIX = ' kilo mega giga tera peta exa zetta yotta bronto geop'.split(' ')
################################################################################
# Allow conversion of byte size strings back into numbers.
def parse(string):
"Convert human-readable string back into bytes."
total = 0
for part in string.split(', '):
number, unit = part.split(' ')
s = number != '1' and 's' or ''
for power, prefix in enumerate(PREFIX):
if unit == (prefix + 'byte' + s).capitalize():
break
else:
raise ValueError('{!r} not found!'.format(unit))
total += int(number) * 1 << 10 * power
return total
################################################################################
# Execute the main method if ran directly.
if __name__ == '__main__':
GUISizeTree.main()