diff --git a/mugshot/CameraMugshotDialog.py b/mugshot/CameraMugshotDialog.py index daf5fb9..2561137 100644 --- a/mugshot/CameraMugshotDialog.py +++ b/mugshot/CameraMugshotDialog.py @@ -3,16 +3,16 @@ # Copyright (C) 2013 Sean Davis # Copyright (C) 2010 Rick Spencer # Portions of this code are inspired by web_cam_box by Rick Spencer. -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -21,65 +21,69 @@ from locale import gettext as _ import logging logger = logging.getLogger('mugshot') -from gi.repository import Gtk, GdkX11, GObject, Gst, GstVideo, GdkPixbuf +from gi.repository import Gtk, GObject, Gst, GdkPixbuf import cairo -import tempfile, os +import tempfile +import os from mugshot_lib.CameraDialog import CameraDialog + def draw_message(widget, message, ctx): - """Draw a message (including newlines) vertically centered on a cairo context.""" + """Draw a message (including newlines) vertically centered on a cairo + context.""" split_msg = message.split('\n') - + # Get the height and width of the drawing area. alloc = widget.get_allocation() height = alloc.height - width = alloc.width - + # Make the background black. - ctx.set_source_rgb(0,0,0) + ctx.set_source_rgb(0, 0, 0) ctx.paint() - + # Set the font details. font_size = 20 - font_color = (255,255,255) + font_color = (255, 255, 255) font_name = "Sans" row_spacing = 6 left_spacing = 10 - + # Get start position - message_height = (len(split_msg)*font_size)+len(split_msg)-1-14 - current_pos = (height-message_height)/2 + message_height = (len(split_msg) * font_size) + len(split_msg) - 15 + current_pos = (height - message_height) / 2 # Draw the message to the drawing area. ctx.set_source_rgb(*font_color) ctx.select_font_face(font_name, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) ctx.set_font_size(font_size) - + for line in split_msg: - ctx.move_to(left_spacing,current_pos) + ctx.move_to(left_spacing, current_pos) ctx.show_text(line) current_pos = current_pos + font_size + row_spacing + class CameraMugshotDialog(CameraDialog): + """Camera Capturing Dialog""" __gtype_name__ = "CameraMugshotDialog" - def finish_initializing(self, builder): # pylint: disable=E1002 + def finish_initializing(self, builder): # pylint: disable=E1002 """Set up the camera dialog""" super(CameraMugshotDialog, self).finish_initializing(builder) - + # Initialize Gst or nothing will work. Gst.init(None) # Pack the video widget into the dialog. vbox = builder.get_object('camera_box') self.video_window = Gtk.DrawingArea() - self.video_window.connect("realize",self.__on_video_window_realized) + self.video_window.connect("realize", self.__on_video_window_realized) vbox.pack_start(self.video_window, True, True, 0) self.video_window.show() - + # Prepare the camerabin element. self.camerabin = Gst.ElementFactory.make("camerabin", "camera-source") if self.camerabin: @@ -94,55 +98,62 @@ class CameraMugshotDialog(CameraDialog): else: devices = [] for device in os.listdir('/dev/'): - if device.startswith('video'): devices.append(device) - logger.error(_('Camera failed to load. Devices: %s') % '; '.join(devices)) - self.draw_handler = self.video_window.connect('draw', self.on_failed_draw) + if device.startswith('video'): + devices.append(device) + logger.error(_('Camera failed to load. Devices: %s') % + '; '.join(devices)) + self.draw_handler = self.video_window.connect('draw', + self.on_failed_draw) self.realized = True - + # Essential widgets self.record_button = builder.get_object('camera_record') self.apply_button = builder.get_object('camera_apply') - + # Store the temporary filename to be used. self.filename = None - + self.show_all() - + def on_failed_draw(self, widget, ctx): """Display a message that the camera failed to load.""" # Translators: Please include newlines, as required to fit the message. message = _("Sorry, but your camera\nfailed to initialize.") draw_message(widget, message, ctx) - + def on_draw(self, widget, ctx): """Display a message that the camera is initializing on first draw. Afterwards, blank the drawing area to clear the message.""" # Translators: Please include newlines, as required to fit the message. message = _("Please wait while your\ncamera is initialized.") draw_message(widget, message, ctx) - + # Redefine on_draw to blank the drawing area next time. def on_draw(self, widget, ctx): - ctx.set_source_rgb(0,0,0) + """Redefinition of on_draw to blank the drawing area next time.""" + ctx.set_source_rgb(0, 0, 0) ctx.paint() + # Redefine on_draw once more to do nothing else. def on_draw(self, widget, ctx): + """Redefinition of on_draw no longer do anything.""" pass - + def play(self): - """Start the camera streaming and display the output. It is necessary + """Start the camera streaming and display the output. It is necessary to start the camera playing before using most other functions.""" if not self.realized: self._set_video_window_id() if not self.realized: - logger.error(_("Cannot display camera output. Ignoring play command")) + logger.error(_("Cannot display camera output." + "Ignoring play command")) else: if self.camerabin: self.camerabin.set_state(Gst.State.PLAYING) def pause(self): - """Pause the camera output. It will cause the image to "freeze". - Use play() to start the camera playing again. Note that calling pause + """Pause the camera output. It will cause the image to "freeze". + Use play() to start the camera playing again. Note that calling pause before play may cause errors on certain camera.""" if self.camerabin: self.camerabin.set_state(Gst.State.PAUSED) @@ -150,11 +161,11 @@ class CameraMugshotDialog(CameraDialog): def take_picture(self, filename): """take_picture - grab a frame from the webcam and save it to 'filename. - + If play is not called before take_picture, an error may occur. If take_picture is called immediately after play, - the camera may not be fully initialized, and an error may occur. - + the camera may not be fully initialized, and an error may occur. + Connect to the signal "image-captured" to be alerted when the picture is saved.""" self.camerabin.set_property("location", filename) @@ -178,18 +189,18 @@ class CameraMugshotDialog(CameraDialog): # Get the message type. t = message.type - + # Initial load, wait until camera is ready before enabling capture. if t == Gst.MessageType.ASYNC_DONE: self.record_button.set_sensitive(True) - + if t == Gst.MessageType.ELEMENT: # Keep the camera working after several pictures are taken. if message.get_structure().get_name() == "image-captured": self.camerabin.set_state(Gst.Sate.NULL) self.camerabin.set_state(Gst.State.PLAYING) self.emit("image-captured", self.filename) - + # Enable interface elements once the images are finished saving. elif message.get_structure().get_name() == "image-done": self.apply_button.set_sensitive(True) @@ -199,7 +210,7 @@ class CameraMugshotDialog(CameraDialog): # Stop the stream if the EOS (end of stream) message is received. if t == Gst.MessageType.EOS: self.camerabin.set_state(Gst.State.NULL) - + # Capture and report any error received. elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() @@ -223,48 +234,49 @@ class CameraMugshotDialog(CameraDialog): if message_name == "prepare-window-handle": imagesink = message.src imagesink.set_property("force-aspect-ratio", True) - imagesink.set_window_handle(self.video_window.get_window().get_xid()) + imagesink.set_window_handle( + self.video_window.get_window().get_xid()) def __on_video_window_realized(self, widget, data=None): - """Internal signal handler, used to set up the xid for the drawing area + """Internal signal handler, used to set up the xid for the drawing area in a thread safe manner. Do not call directly.""" self._set_video_window_id() def _set_video_window_id(self): """Set the window ID only if not previously configured.""" if not self.realized and self.video_window.get_window() is not None: - x = self.video_window.get_window().get_xid() + self.video_window.get_window().get_xid() self.realized = True - + def on_camera_record_clicked(self, widget): """When the camera record/retry button is clicked: Record: Pause the video, start the capture, enable apply and retry. Retry: Restart the video stream.""" # Remove any previous temporary file. - if self.filename and os.path.isfile(self.filename): + if self.filename and os.path.isfile(self.filename): os.remove(self.filename) - + # Retry action. if self.apply_button.get_sensitive(): self.record_button.set_label(Gtk.STOCK_MEDIA_RECORD) self.apply_button.set_sensitive(False) self.play() - + # Record (Capture) action. else: # Create a new temporary file. tmpfile = tempfile.NamedTemporaryFile(delete=False) tmpfile.close() self.filename = tmpfile.name - + # Capture the current image. self.take_picture(self.filename) - + # Set the record button to retry, and disable it until the capture # finishes. self.record_button.set_label(_("Retry")) self.record_button.set_sensitive(False) - + def on_camera_apply_clicked(self, widget): """When the camera Apply button is clicked, crop the current photo and emit a signal to let the main application know there is a new file @@ -272,7 +284,7 @@ class CameraMugshotDialog(CameraDialog): self.center_crop(self.filename) self.emit("apply", self.filename) self.hide() - + def on_camera_cancel_clicked(self, widget): """When the Cancel button is clicked, just hide the dialog.""" self.hide() @@ -285,54 +297,56 @@ class CameraMugshotDialog(CameraDialog): os.remove(self.filename) # Clean up the camera before exiting self.camerabin.set_state(Gst.State.NULL) - + def on_camera_mugshot_dialog_hide(self, widget, data=None): """When the dialog is hidden, pause the camera recording.""" self.pause() - + def on_camera_mugshot_dialog_show(self, widget, data=None): - """When the dialog is shown, set the record button to record, disable + """When the dialog is shown, set the record button to record, disable the apply button, and start the camera.""" self.record_button.set_label(Gtk.STOCK_MEDIA_RECORD) self.apply_button.set_sensitive(False) self.show_all() self.play() - + def center_crop(self, filename): """Crop the specified file to square dimensions.""" # Load the image into a Pixbuf. pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) - + # Get the image dimensions. height = pixbuf.get_height() width = pixbuf.get_width() start_x = 0 start_y = 0 - + # Calculate a balanced center. if width > height: - start_x = (width-height)/2 + start_x = (width - height) / 2 width = height else: - start_y = (height-width)/2 + start_y = (height - width) / 2 height = width - + # Create a new cropped pixbuf. new_pixbuf = pixbuf.new_subpixbuf(start_x, start_y, width, height) - + # Overwrite the temporary file with our new cropped image. new_pixbuf.savev(filename, "png", [], []) - + def on_camera_mugshot_dialog_delete_event(self, widget, data=None): """Override the dialog delete event to just hide the window.""" self.hide() return True - + # Signals used by CameraMugshotDialog: # image-captured: emitted when the camera is done capturing an image. - # apply: emitted when the apply button has been pressed and there is a new file saved for use. - __gsignals__ = {'image-captured' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, - (GObject.TYPE_PYOBJECT,)), - 'apply' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) - } - + # apply: emitted when the apply button has been pressed and there is a + # new file saved for use. + __gsignals__ = {'image-captured': (GObject.SIGNAL_RUN_LAST, + GObject.TYPE_NONE, + (GObject.TYPE_PYOBJECT,)), + 'apply': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) + } diff --git a/mugshot/MugshotWindow.py b/mugshot/MugshotWindow.py index 16af7c3..763b83d 100644 --- a/mugshot/MugshotWindow.py +++ b/mugshot/MugshotWindow.py @@ -1,16 +1,16 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2013 Sean Davis -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -28,7 +28,7 @@ import dbus import tempfile -from gi.repository import Gtk, Gdk, GdkPixbuf # pylint: disable=E0611 +from gi.repository import Gtk, GdkPixbuf # pylint: disable=E0611 import logging logger = logging.getLogger('mugshot') @@ -44,52 +44,64 @@ libreoffice_prefs = os.path.join(home, '.config', 'libreoffice', '4', 'user', pidgin_prefs = os.path.join(home, '.purple', 'prefs.xml') faces_dir = '/usr/share/pixmaps/faces/' + def which(command): '''Use the system command which to get the absolute path for the given command.''' - command = subprocess.Popen(['which', command], \ + command = subprocess.Popen(['which', command], stdout=subprocess.PIPE).stdout.read().strip() - if command == '': + if command == '': logger.debug('Command "%s" could not be found.' % command) return None return command - + + def has_running_process(name): """Check for a running process, return True if any listings are found.""" command = 'ps -ef | grep " %s" | grep -v "grep" | wc -l' % name - n = subprocess.Popen(command, stdout=subprocess.PIPE, + n = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).stdout.read().strip() return int(n) > 0 - + + def has_gstreamer_camerabin_support(): """Return True if gstreamer1.0 camerabin element is available.""" - process = subprocess.Popen(["gst-inspect-1.0", "camerabin"], - stdout=subprocess.PIPE, + process = subprocess.Popen(["gst-inspect-1.0", "camerabin"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.communicate() has_support = process.returncode == 0 if not has_support: - logger.debug('camerabin element unavailable. Do you have gstreamer1.0-plugins-good installed?') + element = 'camerabin' + plugin = 'gstreamer1.0-plugins-good' + logger.debug('%s element unavailable. ' + 'Do you have %s installed?' % (element, plugin)) return has_support + def has_gstreamer_camerasrc_support(): """Return True if gstreamer1.0 v4l2src element is available.""" - process = subprocess.Popen(["gst-inspect-1.0", "v4l2src"], - stdout=subprocess.PIPE, + process = subprocess.Popen(["gst-inspect-1.0", "v4l2src"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.communicate() has_support = process.returncode == 0 if not has_support: - logger.debug('v4l2src element unavailable. Do you have gstreamer1.0-plugins-good installed?') + element = 'v4l2src' + plugin = 'gstreamer1.0-plugins-good' + logger.debug('%s element unavailable. ' + 'Do you have %s installed?' % (element, plugin)) return has_support - + + def get_camera_installed(): """Return True if /dev/video0 exists.""" if not os.path.exists('/dev/video0'): logger.debug('Camera not detected at /dev/video0') return False return True - + + def get_has_camera_support(): """Return True if cameras are fully supported by this application.""" if not get_camera_installed(): @@ -102,10 +114,12 @@ def get_has_camera_support(): return False return True + def detach_cb(menu, widget): '''Detach a widget from its attached widget.''' menu.detach() - + + def get_entry_value(entry_widget): """Get the value from one of the Mugshot entries.""" # Get the text from an entry, changing none to '' @@ -113,10 +127,11 @@ def get_entry_value(entry_widget): if value.lower() == 'none': value = '' return value - -def get_confirmation_dialog(parent, primary_message, secondary_message, + + +def get_confirmation_dialog(parent, primary_message, secondary_message, icon_name=None): - """Display a confirmation (yes/no) dialog configured with primary and + """Display a confirmation (yes/no) dialog configured with primary and secondary messages, as well as a custom icon if requested.""" dialog = Gtk.MessageDialog(parent, flags=0, type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO, @@ -129,7 +144,8 @@ def get_confirmation_dialog(parent, primary_message, secondary_message, response = dialog.run() dialog.destroy() return response == Gtk.ResponseType.YES - + + def menu_position(self, menu, data=None, something_else=None): '''Position a menu at the bottom of its attached widget''' widget = menu.get_attach_widget() @@ -141,15 +157,17 @@ def menu_position(self, menu, data=None, something_else=None): y = window_pos[1] + allocation.y + allocation.height return (x, y, True) + # See mugshot_lib.Window.py for more details about how this class works class MugshotWindow(Window): + """Mugshot GtkWindow""" __gtype_name__ = "MugshotWindow" - - def finish_initializing(self, builder): # pylint: disable=E1002 + + def finish_initializing(self, builder): # pylint: disable=E1002 """Set up the main window""" super(MugshotWindow, self).finish_initializing(builder) self.set_wmclass("Mugshot", "Mugshot") - + self.CameraDialog = CameraMugshotDialog # User Image widgets @@ -159,9 +177,9 @@ class MugshotWindow(Window): self.image_menu.attach_to_widget(self.image_button, detach_cb) self.image_from_camera = builder.get_object('image_from_camera') image_from_browse = builder.get_object('image_from_browse') - image_from_browse.set_visible( os.path.exists(faces_dir) and \ - len(os.listdir(faces_dir)) > 0 ) - + image_from_browse.set_visible(os.path.exists(faces_dir) and + len(os.listdir(faces_dir)) > 0) + # Entry widgets (chfn) self.first_name_entry = builder.get_object('first_name') self.last_name_entry = builder.get_object('last_name') @@ -170,11 +188,11 @@ class MugshotWindow(Window): self.home_phone_entry = builder.get_object('home_phone') self.email_entry = builder.get_object('email') self.fax_entry = builder.get_object('fax') - + # Stock photo browser self.stock_browser = builder.get_object('stock_browser') self.iconview = builder.get_object('stock_iconview') - + # File Chooser Dialog self.chooser = builder.get_object('filechooserdialog') self.crop_center = builder.get_object('crop_center') @@ -186,12 +204,12 @@ class MugshotWindow(Window): image_filter.set_name('Images') image_filter.add_mime_type('image/*') self.chooser.add_filter(image_filter) - + self.tmpfile = None # Populate all of the widgets. self.init_user_details() - + def init_user_details(self): """Initialize the user details entries and variables.""" # Check for .face and set profile image. @@ -202,7 +220,7 @@ class MugshotWindow(Window): else: self.set_user_image(None) self.updated_image = None - + # Search /etc/passwd for the current user's details. logger.debug('Getting user details from /etc/passwd') for line in open('/etc/passwd', 'r'): @@ -211,7 +229,7 @@ class MugshotWindow(Window): details = line.split(':')[4] name, office, office_phone, home_phone = details.split(',', 3) break - + # Expand the user's fullname into first, last, and initials. try: first_name, last_name = name.split(' ', 1) @@ -220,25 +238,27 @@ class MugshotWindow(Window): first_name = name last_name = '' initials = first_name[0] - + # If the variables are defined as 'none', use blank for cleanliness. - if home_phone == 'none': home_phone = '' - if office_phone == 'none': office_phone = '' - + if home_phone == 'none': + home_phone = '' + if office_phone == 'none': + office_phone = '' + # Get dconf settings logger.debug('Getting initials, email, and fax from dconf') if self.settings['initials'] != '': initials = self.settings['initials'] email = self.settings['email'] fax = self.settings['fax'] - + # Set the class variables self.first_name = first_name self.last_name = last_name self.initials = initials self.home_phone = home_phone self.office_phone = office_phone - + # Populate the GtkEntries. logger.debug('Populating entries') self.first_name_entry.set_text(self.first_name) @@ -248,7 +268,7 @@ class MugshotWindow(Window): self.home_phone_entry.set_text(self.home_phone) self.email_entry.set_text(email) self.fax_entry.set_text(fax) - + # = Mugshot Window ======================================================= # def set_user_image(self, filename=None): """Scale and set the user profile image.""" @@ -264,83 +284,84 @@ class MugshotWindow(Window): """Allow only numbers and + in phone entry fields.""" text = entry.get_text().strip() entry.set_text(''.join([i for i in text if i in '+0123456789'])) - + def on_apply_button_clicked(self, widget): - """When the window Apply button is clicked, commit any relevant + """When the window Apply button is clicked, commit any relevant changes.""" logger.debug('Applying changes...') if self.get_chfn_details_updated(): - returns = self.save_chfn_details() - + self.save_chfn_details() + if self.get_libreoffice_details_updated(): self.set_libreoffice_data() - + if self.updated_image: self.save_image() - + self.save_gsettings() self.destroy() - + def save_gsettings(self): """Save details to dconf (the ones not tracked by /etc/passwd)""" logger.debug('Saving details to dconf: /apps/mugshot') - self.settings.set_string('initials', + self.settings.set_string('initials', get_entry_value(self.initials_entry)) self.settings.set_string('email', get_entry_value(self.email_entry)) self.settings.set_string('fax', get_entry_value(self.fax_entry)) - + def entry_focus_next(self, widget): """Focus the next available entry when pressing Enter.""" logger.debug('Entry activated, focusing next widget.') vbox = widget.get_parent().get_parent().get_parent().get_parent() vbox.child_focus(Gtk.DirectionType.TAB_FORWARD) - + def on_cancel_button_clicked(self, widget): """When the window cancel button is clicked, close the program.""" logger.debug('Cancel clicked, goodbye.') self.destroy() - + # = Image Button and Menu ================================================ # def on_image_button_clicked(self, widget): """When the menu button is clicked, display the photo menu.""" if widget.get_active(): logger.debug('Show photo menu') self.image_from_camera.set_visible(get_has_camera_support()) - self.image_menu.popup(None, None, menu_position, - self.image_menu, 3, + self.image_menu.popup(None, None, menu_position, + self.image_menu, 3, Gtk.get_current_event_time()) - + def on_image_menu_hide(self, widget): """Untoggle the image button when the menu is hidden.""" self.image_button.set_active(False) - + def on_camera_dialog_apply(self, widget, data=None): + """Commit changes when apply is clicked.""" self.updated_image = data self.set_user_image(data) - + def save_image(self): """Copy the updated image filename to ~/.face""" # Check if the image has been updated. if not self.updated_image: logger.debug('Photo not updated, not saving changes.') return False - + face = os.path.expanduser('~/.face') - + # If the .face file already exists, remove it first. logger.debug('Photo updated, saving changes.') if os.path.isfile(face): os.remove(face) - + # Copy the new file to ~/.face shutil.copyfile(self.updated_image, face) self.set_pidgin_buddyicon(face) self.updated_image = None return True - + def set_pidgin_buddyicon(self, filename=None): """Sets the pidgin buddyicon to filename (usually ~/.face). - + If pidgin is running, use the dbus interface, otherwise directly modify the XML file.""" if not os.path.exists(pidgin_prefs): @@ -358,19 +379,19 @@ class MugshotWindow(Window): self.set_pidgin_buddyicon_xml(filename) else: logger.debug('Reject: Not updating pidgin buddy icon') - + def set_pidgin_buddyicon_dbus(self, filename=None): """Set the pidgin buddy icon via dbus.""" logger.debug('Updating pidgin buddy icon via dbus') bus = dbus.SessionBus() - obj = bus.get_object("im.pidgin.purple.PurpleService", + obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject") purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface") # To make the change instantly visible, set the icon to none first. purple.PurplePrefsSetPath('/pidgin/accounts/buddyicon', '') if filename: purple.PurplePrefsSetPath('/pidgin/accounts/buddyicon', filename) - + def set_pidgin_buddyicon_xml(self, filename=None): """Set the buddyicon used by pidgin to filename (via the xml file).""" # This is hacky, but a working implementation for now... @@ -380,9 +401,9 @@ class MugshotWindow(Window): if os.path.isfile(prefs_file): for line in open(prefs_file): if '\n' % filename + new = new + 'value=\'%s\'/>\n' % filename else: new = new + 'value=\'\'/>\n' tmp_buffer.append(new) @@ -392,7 +413,7 @@ class MugshotWindow(Window): for line in tmp_buffer: write_prefs.write(line) write_prefs.close() - + # = chfn functions ============================================ # def get_chfn_details_updated(self): """Return True if chfn-related details have been modified.""" @@ -405,25 +426,25 @@ class MugshotWindow(Window): return True logger.debug('chfn details have NOT been modified.') return False - + def save_chfn_details(self): """Commit changes to chfn-related details. For full name, changes must be performed as root. Other changes are done with the user password. - + Return exit codes for 1) full name changes and 2) home/work phone changes. - + e.g. [0, 0] (both passed)""" return_codes = [] - + # Get the user's password password = self.get_password() if not password: return return_codes - + username = os.getenv('USER') chfn = which('chfn') - + # Get each of the updated values. first_name = get_entry_value(self.first_name_entry) last_name = get_entry_value(self.last_name_entry) @@ -435,7 +456,7 @@ class MugshotWindow(Window): home_phone = get_entry_value(self.home_phone_entry) if home_phone == '': home_phone = 'none' - + # Full name can only be modified by root. Try using sudo to modify. logger.debug('Attempting to set fullname with sudo chfn') child = pexpect.spawn('sudo %s %s' % (chfn, username)) @@ -449,9 +470,9 @@ class MugshotWindow(Window): child.sendline('') except pexpect.TIMEOUT: # Password was incorrect, or sudo rights not granted - logger.debug('Timeout reached, password was incorrect or sudo ' \ + logger.debug('Timeout reached, password was incorrect or sudo ' 'rights not granted.') - pass + pass child.close() if child.exitstatus == 0: self.first_name = first_name @@ -479,7 +500,7 @@ class MugshotWindow(Window): self.home_phone = home_phone return_codes.append(child.exitstatus) return return_codes - + # = LibreOffice ========================================================== # def get_libreoffice_details_updated(self): """Return True if LibreOffice settings need to be updated.""" @@ -506,14 +527,14 @@ class MugshotWindow(Window): return True logger.debug('LibreOffice details do not need to be updated.') return False - + def get_libreoffice_data(self): """Get each of the preferences from the LibreOffice registymodifications preferences file. - + Return a dict with the details.""" prefs_file = libreoffice_prefs - data = {'first_name': '', 'last_name': '', 'initials': '', 'email': '', + data = {'first_name': '', 'last_name': '', 'initials': '', 'email': '', 'home_phone': '', 'office_phone': '', 'fax': ''} if os.path.isfile(prefs_file): logger.debug('Getting settings from %s' % prefs_file) @@ -545,7 +566,7 @@ class MugshotWindow(Window): else: pass return data - + def set_libreoffice_data(self): """Update the LibreOffice registymodifications preferences file.""" prefs_file = libreoffice_prefs @@ -622,40 +643,61 @@ class MugshotWindow(Window): open_prefs = open(prefs_file, 'w') for line in tmp_buffer: open_prefs.write(line) - + if not first_name_updated: - string = '%s\n' % first_name + string = \ + '' + '' + '%s\n' % first_name open_prefs.write(string) if not last_name_updated: - string = '%s\n' % last_name + string = \ + '' + '' + '%s\n' % last_name open_prefs.write(string) if not initials_updated: - string = '%s\n' % initials + string = \ + '' + '' + '%s\n' % initials open_prefs.write(string) if not email_updated: - string = '%s\n' % email + string = \ + '' + '' + '%s\n' % email open_prefs.write(string) if not home_phone_updated: - string = '%s\n' % home_phone + string = \ + '' + '' + '%s\n' % home_phone open_prefs.write(string) if not office_phone_updated: - string = '%s\n' % office_phone + string = \ + '' + '' + '%s\n' % office_phone open_prefs.write(string) if not fax_updated: - string = '%s\n' % fax + string = \ + '' + '' + '%s\n' % fax open_prefs.write(string) open_prefs.write('') open_prefs.close() else: logger.debug('Reject: Not updating.') - + # = Stock Browser ======================================================== # def on_image_from_stock_activate(self, widget): - """When the 'Select image from stock' menu item is clicked, load and + """When the 'Select image from stock' menu item is clicked, load and display the stock photo browser.""" self.load_stock_browser() self.stock_browser.show_all() - + def load_stock_browser(self): """Load the stock photo browser.""" # Check if the photos have already been loaded. @@ -663,7 +705,7 @@ class MugshotWindow(Window): if len(model) != 0: logger.debug("Stock browser already loaded.") return - + # If they have not, load each photo from /usr/share/pixmaps/faces. logger.debug("Loading stock browser photos.") for filename in os.listdir('/usr/share/pixmaps/faces'): @@ -672,24 +714,24 @@ class MugshotWindow(Window): pixbuf = GdkPixbuf.Pixbuf.new_from_file(full_path) scaled = pixbuf.scale_simple(90, 90, GdkPixbuf.InterpType.HYPER) model.append([full_path, scaled]) - + def on_stock_iconview_selection_changed(self, widget): """Enable stock submission only when an item is selected.""" selected_items = self.iconview.get_selected_items() self.builder.get_object('stock_ok').set_sensitive( len(selected_items) > 0) - + def on_stock_browser_delete_event(self, widget, event): """Hide the stock browser instead of deleting it.""" widget.hide() return True - + def on_stock_cancel_clicked(self, widget): """Hide the stock browser when Cancel is clicked.""" self.stock_browser.hide() - + def on_stock_ok_clicked(self, widget): - """When the stock browser OK button is clicked, get the currently + """When the stock browser OK button is clicked, get the currently selected photo and set it to the user profile image.""" selected_items = self.iconview.get_selected_items() if len(selected_items) != 0: @@ -697,16 +739,16 @@ class MugshotWindow(Window): path = int(selected_items[0].to_string()) filename = self.iconview.get_model()[path][0] logger.debug("Selected %s" % filename) - + # Update variables and widgets, then hide. self.set_user_image(filename) self.updated_image = filename self.stock_browser.hide() - + def on_stock_iconview_item_activated(self, widget, path): """Allow selecting a stock photo with Enter.""" self.on_stock_ok_clicked(widget) - + # = Image Browser ======================================================== # def on_image_from_browse_activate(self, widget): """Browse for a user profile image.""" @@ -719,12 +761,14 @@ class MugshotWindow(Window): self.tmpfile = tempfile.NamedTemporaryFile(delete=False) self.tmpfile.close() self.updated_image = self.tmpfile.name - self.filechooser_preview_pixbuf.savev(self.updated_image, "png", [], []) + self.filechooser_preview_pixbuf.savev(self.updated_image, "png", + [], []) logger.debug("Selected %s" % self.updated_image) self.set_user_image(self.updated_image) self.chooser.hide() - + def on_filechooserdialog_update_preview(self, widget): + """Update the preview image used in the file chooser.""" filename = widget.get_filename() if not filename: self.file_chooser_preview.set_from_icon_name('folder', 128) @@ -733,47 +777,50 @@ class MugshotWindow(Window): self.file_chooser_preview.set_from_icon_name('folder', 128) return filechooser_pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) - + # Get the image dimensions. height = filechooser_pixbuf.get_height() width = filechooser_pixbuf.get_width() start_x = 0 start_y = 0 - + if self.crop_center.get_active(): # Calculate a balanced center. if width > height: - start_x = (width-height)/2 + start_x = (width - height) / 2 width = height else: - start_y = (height-width)/2 + start_y = (height - width) / 2 height = width - + elif self.crop_left.get_active(): start_x = 0 if width > height: width = height else: - start_y = (height-width)/2 + start_y = (height - width) / 2 height = width elif self.crop_right.get_active(): if width > height: - start_x = width-height + start_x = width - height width = height else: - start_y = (height-width)/2 + start_y = (height - width) / 2 height = width - + # Create a new cropped pixbuf. - self.filechooser_preview_pixbuf = filechooser_pixbuf.new_subpixbuf(start_x, start_y, width, height) - - scaled = self.filechooser_preview_pixbuf.scale_simple(128, 128, GdkPixbuf.InterpType.HYPER) + self.filechooser_preview_pixbuf = \ + filechooser_pixbuf.new_subpixbuf(start_x, start_y, width, height) + + scaled = self.filechooser_preview_pixbuf.scale_simple(128, 128, + GdkPixbuf.InterpType.HYPER) self.file_chooser_preview.set_from_pixbuf(scaled) - + def on_crop_changed(self, widget, data=None): + """Update the preview image when crop style is modified.""" if widget.get_active(): self.on_filechooserdialog_update_preview(self.chooser) - + # = Password Entry ======================================================= # def get_password(self): """Display a password dialog for authenticating to sudo and chfn.""" @@ -789,9 +836,8 @@ class MugshotWindow(Window): return pw logger.debug("Cancelled") return None - + def on_password_entry_changed(self, widget): """Enable password submission only when password is not blank.""" self.builder.get_object('password_ok').set_sensitive( len(widget.get_text()) > 0) - diff --git a/mugshot_lib/Builder.py b/mugshot_lib/Builder.py index 3c3f461..cef1445 100644 --- a/mugshot_lib/Builder.py +++ b/mugshot_lib/Builder.py @@ -1,16 +1,16 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2013 Sean Davis -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -18,7 +18,7 @@ '''Enhances builder connections, provides object to access glade objects''' -from gi.repository import GObject, Gtk # pylint: disable=E0611 +from gi.repository import GObject, Gtk # pylint: disable=E0611 import inspect import functools @@ -46,6 +46,7 @@ class Builder(Gtk.Builder): ''' def __init__(self): + """Initialize the builder.""" Gtk.Builder.__init__(self) self.widgets = {} self.glade_handler_dict = {} @@ -120,7 +121,7 @@ class Builder(Gtk.Builder): connection_dict = {} connection_dict.update(self.glade_handler_dict) connection_dict.update(callback_handler_dict) - for item in connection_dict.items(): + for item in list(connection_dict.items()): if item[1] is None: # the handler is missing so reroute to default_handler handler = functools.partial( @@ -166,17 +167,19 @@ class Builder(Gtk.Builder): class UiFactory(): ''' provides an object with attributes as glade widgets''' def __init__(self, widget_dict): + """Initialize the UiFactory.""" self._widget_dict = widget_dict - for (widget_name, widget) in widget_dict.items(): + for (widget_name, widget) in list(widget_dict.items()): setattr(self, widget_name, widget) # Mangle any non-usable names (like with spaces or dashes) # into pythonic ones cannot_message = """cannot bind ui.%s, name already exists consider using a pythonic name instead of design name '%s'""" - consider_message = """consider using a pythonic name instead of design name '%s'""" - - for (widget_name, widget) in widget_dict.items(): + consider_message = """consider using a pythonic name instead of + design name '%s'""" + + for (widget_name, widget) in list(widget_dict.items()): pyname = make_pyname(widget_name) if pyname != widget_name: if hasattr(self, pyname): @@ -187,7 +190,7 @@ class UiFactory(): def iterator(): '''Support 'for o in self' ''' - return iter(widget_dict.values()) + return iter(list(widget_dict.values())) setattr(self, '__iter__', iterator) def __getitem__(self, name): @@ -208,10 +211,11 @@ def make_pyname(name): return pyname -# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we +# Until bug https://bugzilla.gnome.org/show_bug.cgi?id=652127 is fixed, we # need to reimplement inspect.getmembers. GObject introspection doesn't # play nice with it. def getmembers(obj, check): + """Reimplementation of getmembers""" members = [] for k in dir(obj): try: @@ -260,7 +264,7 @@ def auto_connect_by_name(callback_obj, builder): callback_handler_dict = dict_from_callback_obj(callback_obj) - for item in builder.widgets.items(): + for item in list(builder.widgets.items()): (widget_name, widget) = item signal_ids = [] try: @@ -296,7 +300,7 @@ def do_connect(item, signal_name, handler_names, widget_name, widget = item for handler_name in handler_names: - target = handler_name in callback_handler_dict.keys() + target = handler_name in list(callback_handler_dict.keys()) connection = (widget_name, signal_name, handler_name) duplicate = connection in connections if target and not duplicate: @@ -312,7 +316,7 @@ def log_unconnected_functions(callback_handler_dict, connections): connected_functions = [x[2] for x in connections] - handler_names = callback_handler_dict.keys() + handler_names = list(callback_handler_dict.keys()) unconnected = [x for x in handler_names if x.startswith('on_')] for handler_name in connected_functions: diff --git a/mugshot_lib/CameraDialog.py b/mugshot_lib/CameraDialog.py index cf8aae3..034a110 100644 --- a/mugshot_lib/CameraDialog.py +++ b/mugshot_lib/CameraDialog.py @@ -1,34 +1,36 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2013 Sean Davis -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE ### DO NOT EDIT THIS FILE ### -from gi.repository import Gtk # pylint: disable=E0611 +from gi.repository import Gtk # pylint: disable=E0611 import logging logger = logging.getLogger('mugshot_lib') from . helpers import get_builder, show_uri, get_help_uri + class CameraDialog(Gtk.Dialog): + """Camera Dialog""" __gtype_name__ = "CameraDialog" def __new__(cls): - """Special static method that's automatically called by Python when + """Special static method that's automatically called by Python when constructing a new instance of this class. - + Returns a fully instantiated PreferencesDialog object. """ builder = get_builder('CameraMugshotDialog') @@ -43,7 +45,7 @@ class CameraDialog(Gtk.Dialog): and creating a PreferencesDialog object with it in order to finish initializing the start of the new PerferencesMugshotDialog instance. - + Put your initialization code in here and leave __init__ undefined. """ @@ -54,8 +56,9 @@ class CameraDialog(Gtk.Dialog): # code for other initialization actions should be added here def on_btn_close_clicked(self, widget, data=None): + """Destroy the dialog when closed.""" self.destroy() def on_btn_help_clicked(self, widget, data=None): + """Show the help dialog when Help is clicked.""" show_uri(self, "ghelp:%s" % get_help_uri('preferences')) - diff --git a/mugshot_lib/Window.py b/mugshot_lib/Window.py index 0157af4..db9448b 100644 --- a/mugshot_lib/Window.py +++ b/mugshot_lib/Window.py @@ -1,22 +1,22 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2013 Sean Davis -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE ### DO NOT EDIT THIS FILE ### -from gi.repository import Gio, Gtk # pylint: disable=E0611 +from gi.repository import Gio, Gtk # pylint: disable=E0611 import logging logger = logging.getLogger('mugshot_lib') @@ -24,12 +24,13 @@ import os from . helpers import get_builder, show_uri, get_help_uri -# This class is meant to be subclassed by MugshotWindow. It provides -# common functions and some boilerplate. + class Window(Gtk.Window): + """This class is meant to be subclassed by MugshotWindow. It provides + common functions and some boilerplate.""" __gtype_name__ = "Window" - # To construct a new instance of this method, the following notable + # To construct a new instance of this method, the following notable # methods are called in this order: # __new__(cls) # __init__(self) @@ -38,11 +39,11 @@ class Window(Gtk.Window): # # For this reason, it's recommended you leave __init__ empty and put # your initialization code in finish_initializing - + def __new__(cls): - """Special static method that's automatically called by Python when + """Special static method that's automatically called by Python when constructing a new instance of this class. - + Returns a fully instantiated BaseMugshotWindow object. """ builder = get_builder('MugshotWindow') @@ -60,15 +61,16 @@ class Window(Gtk.Window): # Get a reference to the builder and set up the signals. self.builder = builder self.ui = builder.get_ui(self, True) - self.CameraDialog = None # class - self.camera_dialog = None # instance + self.CameraDialog = None # class + self.camera_dialog = None # instance self.settings = Gio.Settings("apps.mugshot") self.settings.connect('changed', self.on_preferences_changed) def on_help_activate(self, widget, data=None): + """Show the Help documentation when Help is clicked.""" show_uri(self, "ghelp:%s" % get_help_uri()) - + def on_menu_camera_activate(self, widget, data=None): """Display the camera window for mugshot.""" if self.camera_dialog is not None: @@ -76,8 +78,7 @@ class Window(Gtk.Window): self.camera_dialog.show() elif self.CameraDialog is not None: logger.debug('create new camera_dialog') - self.camera_dialog = self.CameraDialog() # pylint: disable=E1102 - #self.camera_dialog.connect('destroy', self.on_camera_dialog_destroyed) + self.camera_dialog = self.CameraDialog() # pylint: disable=E1102 self.camera_dialog.connect('apply', self.on_camera_dialog_apply) self.camera_dialog.show() @@ -89,5 +90,6 @@ class Window(Gtk.Window): Gtk.main_quit() def on_preferences_changed(self, settings, key, data=None): - logger.debug('preference changed: %s = %s' % (key, str(settings.get_value(key)))) - + """Log preference updates.""" + logger.debug('preference changed: %s = %s' % + (key, str(settings.get_value(key)))) diff --git a/mugshot_lib/helpers.py b/mugshot_lib/helpers.py index 15ca93f..edc02d2 100644 --- a/mugshot_lib/helpers.py +++ b/mugshot_lib/helpers.py @@ -1,16 +1,16 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2013 Sean Davis -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -23,12 +23,11 @@ import os from . mugshotconfig import get_data_file from . Builder import Builder -from locale import gettext as _ def get_builder(builder_file_name): - """Return a fully-instantiated Gtk.Builder instance from specified ui + """Return a fully-instantiated Gtk.Builder instance from specified ui file - + :param builder_file_name: The name of the builder file, without extension. Assumed to be in the 'ui' directory under the data path. """ @@ -43,25 +42,31 @@ def get_builder(builder_file_name): return builder -# Owais Lone : To get quick access to icons and stuff. def get_media_file(media_file_name): + """Retrieve the filename for the specified file.""" media_filename = get_data_file('media', '%s' % (media_file_name,)) if not os.path.exists(media_filename): media_filename = None - return "file:///"+media_filename + return "file:///" + media_filename + class NullHandler(logging.Handler): + """Handle NULL""" def emit(self, record): + """Do not emit anything.""" pass + def set_up_logging(opts): + """Set up the logging formatter.""" # add a handler to prevent basicConfig root = logging.getLogger() null_handler = NullHandler() root.addHandler(null_handler) - formatter = logging.Formatter("%(levelname)s:%(name)s: %(funcName)s() '%(message)s'") + formatter = logging.Formatter("%(levelname)s:%(name)s:" + " %(funcName)s() '%(message)s'") logger = logging.getLogger('mugshot') logger_sh = logging.StreamHandler() @@ -80,7 +85,9 @@ def set_up_logging(opts): if opts.verbose > 1: lib_logger.setLevel(logging.DEBUG) + def get_help_uri(page=None): + """Get the URI to be used for Help.""" # help_uri from source tree - default language here = os.path.dirname(__file__) help_uri = os.path.abspath(os.path.join(here, '..', 'help', 'C')) @@ -95,11 +102,14 @@ def get_help_uri(page=None): return help_uri + def show_uri(parent, link): - from gi.repository import Gtk # pylint: disable=E0611 + """Open the URI.""" + from gi.repository import Gtk # pylint: disable=E0611 screen = parent.get_screen() Gtk.show_uri(screen, link, Gtk.get_current_event_time()) + def alias(alternative_function_name): '''see http://www.drdobbs.com/web-development/184406073#l9''' def decorator(function): diff --git a/mugshot_lib/mugshotconfig.py b/mugshot_lib/mugshotconfig.py index 7551994..aced570 100644 --- a/mugshot_lib/mugshotconfig.py +++ b/mugshot_lib/mugshotconfig.py @@ -1,16 +1,16 @@ # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- ### BEGIN LICENSE # Copyright (C) 2013 Sean Davis -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 3, as published +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranties of -# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along +# +# You should have received a copy of the GNU General Public License along # with this program. If not, see . ### END LICENSE @@ -30,7 +30,6 @@ __version__ = '0.1' import os -from locale import gettext as _ class project_path_not_found(Exception): """Raised when we can't find the project directory.""" @@ -66,4 +65,5 @@ def get_data_path(): def get_version(): + """Return the program version.""" return __version__