From 2166a0996c6916e4e491ece298e4f31f2085ba05 Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Mon, 13 Oct 2014 15:08:59 -0400 Subject: [PATCH] Added tox configuration ... and fixed pep8 warnings. There's a lot of them that are still ignored, but that's because it's too much of a step to take at once. --- .gitignore | 1 + build.py | 121 +++++++++++++++++++++++----------- configure.py | 26 +++++--- core/app.py | 39 +++++++---- core/directories.py | 9 +-- core/engine.py | 14 ++-- core/export.py | 76 ++++++++++----------- core/fs.py | 63 +++++++++--------- core/gui/__init__.py | 3 +- core/gui/base.py | 19 +++--- core/gui/prioritize_dialog.py | 25 +++---- core/ignore.py | 58 ++++++++-------- core/results.py | 17 +++-- core/scanner.py | 29 +++++--- core_me/__init__.py | 3 +- core_me/app.py | 20 +++--- core_me/fs.py | 44 +++++++------ core_me/prioritize.py | 25 ++++--- core_pe/block.py | 20 +++--- core_pe/cache.py | 48 +++++++------- core_pe/exif.py | 28 ++++---- core_pe/matchblock.py | 11 ++-- core_pe/matchexif.py | 9 +-- core_pe/photo.py | 28 ++++---- core_pe/prioritize.py | 22 ++++--- package.py | 15 +++-- qt/base/app.py | 77 +++++++++++----------- qt/base/cxfreeze_fix.py | 1 + qt/base/deletion_options.py | 32 +++++---- qt/base/directories_dialog.py | 66 ++++++++++--------- qt/base/directories_model.py | 58 ++++++++-------- qt/base/ignore_list_dialog.py | 22 ++++--- qt/base/preferences_dialog.py | 39 ++++++----- qt/base/prioritize_dialog.py | 36 +++++----- qt/base/problem_dialog.py | 26 ++++---- qt/base/result_window.py | 112 +++++++++++++++++-------------- qt/me/preferences_dialog.py | 35 +++++----- qt/me/results_model.py | 9 +-- qt/pe/block.py | 10 +-- qt/pe/preferences_dialog.py | 23 +++---- qt/pe/results_model.py | 9 +-- qt/se/app.py | 15 +++-- qt/se/preferences_dialog.py | 35 +++++----- qt/se/results_model.py | 9 +-- requirements-extra.txt | 2 + tox.ini | 17 +++++ 46 files changed, 794 insertions(+), 612 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index ef94f59c..831c9fe5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.waf* .lock-waf* .idea +.tox build dist diff --git a/build.py b/build.py index fe988d29..01413699 100644 --- a/build.py +++ b/build.py @@ -18,10 +18,12 @@ import compileall from setuptools import setup, Extension from hscommon import sphinxgen -from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, filereplace, +from hscommon.build import ( + add_to_pythonpath, print_and_do, copy_packages, filereplace, get_module_version, move_all, copy_all, OSXAppStructure, build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib, - collect_stdlib_dependencies, copy) + collect_stdlib_dependencies, copy +) from hscommon import loc from hscommon.plat import ISOSX, ISLINUX from hscommon.util import ensure_folder, delete_files_with_pattern @@ -29,24 +31,42 @@ from hscommon.util import ensure_folder, delete_files_with_pattern def parse_args(): usage = "usage: %prog [options]" parser = OptionParser(usage=usage) - parser.add_option('--clean', action='store_true', dest='clean', - help="Clean build folder before building") - parser.add_option('--doc', action='store_true', dest='doc', - help="Build only the help file") - parser.add_option('--loc', action='store_true', dest='loc', - help="Build only localization") - parser.add_option('--cocoa-ext', action='store_true', dest='cocoa_ext', - help="Build only Cocoa extensions") - parser.add_option('--cocoa-compile', action='store_true', dest='cocoa_compile', - help="Build only Cocoa executable") - parser.add_option('--xibless', action='store_true', dest='xibless', - help="Build only xibless UIs") - parser.add_option('--updatepot', action='store_true', dest='updatepot', - help="Generate .pot files from source code.") - parser.add_option('--mergepot', action='store_true', dest='mergepot', - help="Update all .po files based on .pot files.") - parser.add_option('--normpo', action='store_true', dest='normpo', - help="Normalize all PO files (do this before commit).") + parser.add_option( + '--clean', action='store_true', dest='clean', + help="Clean build folder before building" + ) + parser.add_option( + '--doc', action='store_true', dest='doc', + help="Build only the help file" + ) + parser.add_option( + '--loc', action='store_true', dest='loc', + help="Build only localization" + ) + parser.add_option( + '--cocoa-ext', action='store_true', dest='cocoa_ext', + help="Build only Cocoa extensions" + ) + parser.add_option( + '--cocoa-compile', action='store_true', dest='cocoa_compile', + help="Build only Cocoa executable" + ) + parser.add_option( + '--xibless', action='store_true', dest='xibless', + help="Build only xibless UIs" + ) + parser.add_option( + '--updatepot', action='store_true', dest='updatepot', + help="Generate .pot files from source code." + ) + parser.add_option( + '--mergepot', action='store_true', dest='mergepot', + help="Update all .po files based on .pot files." + ) + parser.add_option( + '--normpo', action='store_true', dest='normpo', + help="Normalize all PO files (do this before commit)." + ) (options, args) = parser.parse_args() return options @@ -75,12 +95,20 @@ def build_xibless(edition, dest='cocoa/autogen'): ('preferences_panel.py', 'PreferencesPanel_UI'), ] for srcname, dstname in FNPAIRS: - xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname), - localizationTable='Localizable', args={'edition': edition}) + xibless.generate( + op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname), + localizationTable='Localizable', args={'edition': edition} + ) if edition == 'pe': - xibless.generate('cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable') + xibless.generate( + 'cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), + localizationTable='Localizable' + ) else: - xibless.generate('cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable') + xibless.generate( + 'cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), + localizationTable='Localizable' + ) def build_cocoa(edition, dev): print("Creating OS X app structure") @@ -119,7 +147,7 @@ def build_cocoa(edition, dev): if edition == 'pe': # ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have # to manually specify it. - extra_deps=['multiprocessing'] + extra_deps = ['multiprocessing'] collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps) del sys.path[0] # Views are not referenced by python code, so they're not found by the collector. @@ -225,8 +253,10 @@ def build_updatepot(): os.remove(cocoalib_pot) loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot) print("Enhancing ui.pot with Cocoa's strings files") - loc.strings2pot(op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'), - op.join('locale', 'ui.pot')) + loc.strings2pot( + op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'), + op.join('locale', 'ui.pot') + ) def build_mergepot(): print("Updating .po files using .pot files") @@ -243,11 +273,15 @@ def build_cocoa_proxy_module(): print("Building Cocoa Proxy") import objp.p2o objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m') - build_cocoa_ext("CocoaProxy", 'cocoalib/cocoa', - ['cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m', - 'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'], + build_cocoa_ext( + "CocoaProxy", 'cocoalib/cocoa', + [ + 'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m', + 'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m' + ], ['AppKit', 'CoreServices'], - ['cocoalib', 'cocoa/autogen']) + ['cocoalib', 'cocoa/autogen'] + ) def build_cocoa_bridging_interfaces(edition): print("Building Cocoa Bridging Interfaces") @@ -255,9 +289,11 @@ def build_cocoa_bridging_interfaces(edition): import objp.p2o add_to_pythonpath('cocoa') add_to_pythonpath('cocoalib') - from cocoa.inter import (PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline, + from cocoa.inter import ( + PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline, OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp, - PyTextField, ProgressWindowView, PyProgressWindow) + PyTextField, ProgressWindowView, PyProgressWindow + ) from inter.deletion_options import PyDeletionOptions, DeletionOptionsView from inter.details_panel import PyDetailsPanel, DetailsPanelView from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView @@ -269,16 +305,20 @@ def build_cocoa_bridging_interfaces(edition): from inter.stats_label import PyStatsLabel, StatsLabelView from inter.app import PyDupeGuruBase, DupeGuruView appmod = importlib.import_module('inter.app_{}'.format(edition)) - allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, + allclasses = [ + PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog, PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase, - PyTextField, PyProgressWindow, appmod.PyDupeGuru] + PyTextField, PyProgressWindow, appmod.PyDupeGuru + ] for class_ in allclasses: objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True) - allclasses = [GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView, + allclasses = [ + GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView, DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView, IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView, - ProgressWindowView, DupeGuruView] + ProgressWindowView, DupeGuruView + ] clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses] objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m') build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m']) @@ -297,11 +337,12 @@ def build_pe_modules(ui): extra_link_args=[ "-framework", "CoreFoundation", "-framework", "Foundation", - "-framework", "ApplicationServices",] + "-framework", "ApplicationServices", + ] )) setup( - script_args = ['build_ext', '--inplace'], - ext_modules = exts, + script_args=['build_ext', '--inplace'], + ext_modules=exts, ) move_all('_block_qt*', op.join('qt', 'pe')) move_all('_block*', 'core_pe') diff --git a/configure.py b/configure.py index 0952673e..1de54688 100644 --- a/configure.py +++ b/configure.py @@ -1,12 +1,11 @@ # Created By: Virgil Dupras # Created On: 2009-12-30 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license -import sys from optparse import OptionParser import json @@ -29,11 +28,18 @@ def main(options): if __name__ == '__main__': usage = "usage: %prog [options]" parser = OptionParser(usage=usage) - parser.add_option('--edition', dest='edition', - help="dupeGuru edition to build (se, me or pe). Default is se.") - parser.add_option('--ui', dest='ui', - help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system.") - parser.add_option('--dev', action='store_true', dest='dev', default=False, - help="If this flag is set, will configure for dev builds.") + parser.add_option( + '--edition', dest='edition', + help="dupeGuru edition to build (se, me or pe). Default is se." + ) + parser.add_option( + '--ui', dest='ui', + help="Type of UI to build. 'qt' or 'cocoa'. Default is determined by your system." + ) + parser.add_option( + '--dev', action='store_true', dest='dev', default=False, + help="If this flag is set, will configure for dev builds." + ) (options, args) = parser.parse_args() main(options) + diff --git a/core/app.py b/core/app.py index b1645998..965efac4 100644 --- a/core/app.py +++ b/core/app.py @@ -38,8 +38,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode' MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.") MSG_NO_SELECTED_DUPES = tr("There are no selected duplicates. Nothing has been done.") -MSG_MANY_FILES_TO_OPEN = tr("You're about to open many files at once. Depending on what those " - "files are opened with, doing so can create quite a mess. Continue?") +MSG_MANY_FILES_TO_OPEN = tr( + "You're about to open many files at once. Depending on what those " + "files are opened with, doing so can create quite a mess. Continue?" +) class DestType: Direct = 0 @@ -265,8 +267,10 @@ class DupeGuru(Broadcaster): return None def _get_export_data(self): - columns = [col for col in self.result_table.columns.ordered_columns - if col.visible and col.name != 'marked'] + columns = [ + col for col in self.result_table.columns.ordered_columns + if col.visible and col.name != 'marked' + ] colnames = [col.display for col in columns] rows = [] for group_id, group in enumerate(self.results.groups): @@ -278,8 +282,10 @@ class DupeGuru(Broadcaster): return colnames, rows def _results_changed(self): - self.selected_dupes = [d for d in self.selected_dupes - if self.results.get_group_of_duplicate(d) is not None] + self.selected_dupes = [ + d for d in self.selected_dupes + if self.results.get_group_of_duplicate(d) is not None + ] self.notify('results_changed') def _start_job(self, jobid, func, args=()): @@ -287,7 +293,10 @@ class DupeGuru(Broadcaster): try: self.progress_window.run(jobid, title, func, args=args) except job.JobInProgressError: - msg = tr("A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.") + msg = tr( + "A previous action is still hanging in there. You can't start a new one yet. Wait " + "a few seconds, then try again." + ) self.view.show_message(msg) def _job_completed(self, jobid): @@ -439,8 +448,10 @@ class DupeGuru(Broadcaster): return if not self.deletion_options.show(self.results.mark_count): return - args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks, - self.deletion_options.direct] + args = [ + self.deletion_options.link_deleted, self.deletion_options.use_hardlinks, + self.deletion_options.direct + ] logging.debug("Starting deletion job with args %r", args) self._start_job(JobType.Delete, self._do_delete, args=args) @@ -550,8 +561,10 @@ class DupeGuru(Broadcaster): # If no group was changed, however, we don't touch the selection. if not self.result_table.power_marker: if changed_groups: - self.selected_dupes = [d for d in self.selected_dupes - if self.results.get_group_of_duplicate(d).ref is d] + self.selected_dupes = [ + d for d in self.selected_dupes + if self.results.get_group_of_duplicate(d).ref is d + ] self.notify('results_changed') else: # If we're in "Dupes Only" mode (previously called Power Marker), things are a bit @@ -604,7 +617,7 @@ class DupeGuru(Broadcaster): def purge_ignore_list(self): """Remove files that don't exist from :attr:`ignore_list`. """ - self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s)) + self.scanner.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s)) self.ignore_list_dialog.refresh() def remove_directories(self, indexes): @@ -641,7 +654,7 @@ class DupeGuru(Broadcaster): msg = tr("You are about to remove %d files from results. Continue?") if not self.view.ask_yes_no(msg % self.results.mark_count): return - self.results.perform_on_marked(lambda x:None, True) + self.results.perform_on_marked(lambda x: None, True) self._results_changed() def remove_selected(self): diff --git a/core/directories.py b/core/directories.py index 31d53775..2b76b737 100644 --- a/core/directories.py +++ b/core/directories.py @@ -62,10 +62,10 @@ class Directories: return True return False - def __delitem__(self,key): + def __delitem__(self, key): self._dirs.__delitem__(key) - def __getitem__(self,key): + def __getitem__(self, key): return self._dirs.__getitem__(key) def __len__(self): @@ -95,7 +95,8 @@ class Directories: file.is_ref = state == DirectoryState.Reference filepaths.add(file.path) yield file - # it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it + # it's possible that a folder (bundle) gets into the file list. in that case, we don't + # want to recurse into it subfolders = [p for p in from_path.listdir() if not p.islink() and p.isdir() and p not in filepaths] for subfolder in subfolders: for file in self._get_files(subfolder, j): @@ -144,7 +145,7 @@ class Directories: """ try: subpaths = [p for p in path.listdir() if p.isdir()] - subpaths.sort(key=lambda x:x.name.lower()) + subpaths.sort(key=lambda x: x.name.lower()) return subpaths except EnvironmentError: return [] diff --git a/core/engine.py b/core/engine.py index c57eb3dc..421f0583 100644 --- a/core/engine.py +++ b/core/engine.py @@ -17,9 +17,11 @@ from hscommon.util import flatten, multi_replace from hscommon.trans import tr from hscommon.jobprogress import job -(WEIGHT_WORDS, -MATCH_SIMILAR_WORDS, -NO_FIELD_ORDER) = range(3) +( + WEIGHT_WORDS, + MATCH_SIMILAR_WORDS, + NO_FIELD_ORDER, +) = range(3) JOB_REFRESH_RATE = 100 @@ -259,6 +261,7 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob) filesize = getattr(file, sizeattr) if filesize: size2files[filesize].add(file) + del files possible_matches = [files for files in size2files.values() if len(files) > 1] del size2files result = [] @@ -495,7 +498,10 @@ def get_groups(matches, j=job.nulljob): matched_files = set(flatten(groups)) orphan_matches = [] for group in groups: - orphan_matches += set(m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second])) + orphan_matches += { + m for m in group.discard_matches() + if not any(obj in matched_files for obj in [m.first, m.second]) + } if groups and orphan_matches: groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time return groups diff --git a/core/export.py b/core/export.py index 3dc6b6c4..bde82628 100644 --- a/core/export.py +++ b/core/export.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2006/09/16 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import os.path as op @@ -19,56 +19,56 @@ MAIN_TEMPLATE = """ - dupeGuru Results - diff --git a/core/fs.py b/core/fs.py index 78c764e8..d06c1184 100644 --- a/core/fs.py +++ b/core/fs.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2009-10-22 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license # This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru @@ -32,6 +32,7 @@ NOT_SET = object() class FSError(Exception): cls_message = "An error has occured on '{name}' in '{parent}'" + def __init__(self, fsobject, parent=None): message = self.cls_message if isinstance(fsobject, str): @@ -42,7 +43,7 @@ class FSError(Exception): name = '' parentname = str(parent) if parent is not None else '' Exception.__init__(self, message.format(name=name, parent=parentname)) - + class AlreadyExistsError(FSError): "The directory or file name we're trying to add already exists" @@ -57,7 +58,7 @@ class InvalidDestinationError(FSError): cls_message = "'{name}' is an invalid destination for this operation." class OperationError(FSError): - """A copy/move/delete operation has been called, but the checkup after the + """A copy/move/delete operation has been called, but the checkup after the operation shows that it didn't work.""" cls_message = "Operation on '{name}' failed." @@ -74,15 +75,15 @@ class File: # files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become # even greater when we take into account read attributes (70%!). Yeah, it's worth it. __slots__ = ('path', 'is_ref', 'words') + tuple(INITIAL_INFO.keys()) - + def __init__(self, path): self.path = path for attrname in self.INITIAL_INFO: setattr(self, attrname, NOT_SET) - + def __repr__(self): return "<{} {}>".format(self.__class__.__name__, str(self.path)) - + def __getattribute__(self, attrname): result = object.__getattribute__(self, attrname) if result is NOT_SET: @@ -94,12 +95,12 @@ class File: if result is NOT_SET: result = self.INITIAL_INFO[attrname] return result - + #This offset is where we should start reading the file to get a partial md5 #For audio file, it should be where audio data starts def _get_md5partial_offset_and_size(self): return (0x4000, 0x4000) #16Kb - + def _read_info(self, field): if field in ('size', 'mtime'): stats = self.path.stat() @@ -129,24 +130,24 @@ class File: fp.close() except Exception: pass - + def _read_all_info(self, attrnames=None): """Cache all possible info. - + If `attrnames` is not None, caches only attrnames. """ if attrnames is None: attrnames = self.INITIAL_INFO.keys() for attrname in attrnames: getattr(self, attrname) - + #--- Public @classmethod def can_handle(cls, path): """Returns whether this file wrapper class can handle ``path``. """ return not path.islink() and path.isfile() - + def rename(self, newname): if newname == self.name: return @@ -160,42 +161,42 @@ class File: if not destpath.exists(): raise OperationError(self) self.path = destpath - + def get_display_info(self, group, delta): """Returns a display-ready dict of dupe's data. """ raise NotImplementedError() - + #--- Properties @property def extension(self): return get_file_ext(self.name) - + @property def name(self): return self.path.name - + @property def folder_path(self): return self.path.parent() - + class Folder(File): """A wrapper around a folder path. - + It has the size/md5 info of a File, but it's value are the sum of its subitems. """ __slots__ = File.__slots__ + ('_subfolders', ) - + def __init__(self, path): File.__init__(self, path) self._subfolders = None - + def _all_items(self): folders = self.subfolders files = get_files(self.path) return folders + files - + def _read_info(self, field): if field in {'size', 'mtime'}: size = sum((f.size for f in self._all_items()), 0) @@ -208,31 +209,31 @@ class Folder(File): # different md5 if a file gets moved in a different subdirectory. def get_dir_md5_concat(): items = self._all_items() - items.sort(key=lambda f:f.path) + items.sort(key=lambda f: f.path) md5s = [getattr(f, field) for f in items] return b''.join(md5s) - + md5 = hashlib.md5(get_dir_md5_concat()) digest = md5.digest() setattr(self, field, digest) - + @property def subfolders(self): if self._subfolders is None: subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()] self._subfolders = [self.__class__(p) for p in subfolders] return self._subfolders - + @classmethod def can_handle(cls, path): return not path.islink() and path.isdir() - + def get_file(path, fileclasses=[File]): """Wraps ``path`` around its appropriate :class:`File` class. - + Whether a class is "appropriate" is decided by :meth:`File.can_handle` - + :param Path path: path to wrap :param fileclasses: List of candidate :class:`File` classes """ @@ -242,7 +243,7 @@ def get_file(path, fileclasses=[File]): def get_files(path, fileclasses=[File]): """Returns a list of :class:`File` for each file contained in ``path``. - + :param Path path: path to scan :param fileclasses: List of candidate :class:`File` classes """ diff --git a/core/gui/__init__.py b/core/gui/__init__.py index 39c05ee6..31341ea0 100644 --- a/core/gui/__init__.py +++ b/core/gui/__init__.py @@ -12,4 +12,5 @@ either Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell blue, which is supposed to be orange, does the sorting logic, holds selection, etc.. .. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software -""" \ No newline at end of file +""" + diff --git a/core/gui/base.py b/core/gui/base.py index 8b973f8b..fd44a2d5 100644 --- a/core/gui/base.py +++ b/core/gui/base.py @@ -1,31 +1,30 @@ # Created By: Virgil Dupras # Created On: 2010-02-06 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from hscommon.notify import Listener -from hscommon.gui.base import NoopGUI class DupeGuruGUIObject(Listener): def __init__(self, app): Listener.__init__(self, app) self.app = app - + def directories_changed(self): pass - + def dupes_selected(self): pass - + def marking_changed(self): pass - + def results_changed(self): pass - + def results_changed_but_keep_selection(self): pass - + diff --git a/core/gui/prioritize_dialog.py b/core/gui/prioritize_dialog.py index 3d80c2ef..bf48bcd0 100644 --- a/core/gui/prioritize_dialog.py +++ b/core/gui/prioritize_dialog.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from hscommon.gui.base import GUIObject @@ -13,7 +13,7 @@ class CriterionCategoryList(GUISelectableList): def __init__(self, dialog): self.dialog = dialog GUISelectableList.__init__(self, [c.NAME for c in dialog.categories]) - + def _update_selection(self): self.dialog.select_category(self.dialog.categories[self.selected_index]) GUISelectableList._update_selection(self) @@ -22,10 +22,10 @@ class PrioritizationList(GUISelectableList): def __init__(self, dialog): self.dialog = dialog GUISelectableList.__init__(self) - + def _refresh_contents(self): self[:] = [crit.display for crit in self.dialog.prioritizations] - + def move_indexes(self, indexes, dest_index): indexes.sort() prilist = self.dialog.prioritizations @@ -34,7 +34,7 @@ class PrioritizationList(GUISelectableList): del prilist[i] prilist[dest_index:dest_index] = selected self._refresh_contents() - + def remove_selected(self): prilist = self.dialog.prioritizations for i in sorted(self.selected_indexes, reverse=True): @@ -51,15 +51,15 @@ class PrioritizeDialog(GUIObject): self.criteria_list = GUISelectableList() self.prioritizations = [] self.prioritization_list = PrioritizationList(self) - + #--- Override def _view_updated(self): self.category_list.select(0) - + #--- Private def _sort_key(self, dupe): return tuple(crit.sort_key(dupe) for crit in self.prioritizations) - + #--- Public def select_category(self, category): self.criteria = category.criteria_list() @@ -71,10 +71,11 @@ class PrioritizeDialog(GUIObject): return crit = self.criteria[self.criteria_list.selected_index] self.prioritizations.append(crit) + del crit self.prioritization_list[:] = [crit.display for crit in self.prioritizations] - + def remove_selected(self): self.prioritization_list.remove_selected() - + def perform_reprioritization(self): self.app.reprioritize_groups(self._sort_key) diff --git a/core/ignore.py b/core/ignore.py index a15cd0dd..fb13039b 100644 --- a/core/ignore.py +++ b/core/ignore.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2006/05/02 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from xml.etree import ElementTree as ET @@ -12,7 +12,7 @@ from hscommon.util import FileOrPath class IgnoreList: """An ignore list implementation that is iterable, filterable and exportable to XML. - + Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list. When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together. """ @@ -20,43 +20,43 @@ class IgnoreList: def __init__(self): self._ignored = {} self._count = 0 - + def __iter__(self): - for first,seconds in self._ignored.items(): + for first, seconds in self._ignored.items(): for second in seconds: - yield (first,second) - + yield (first, second) + def __len__(self): return self._count - + #---Public - def AreIgnored(self,first,second): - def do_check(first,second): + def AreIgnored(self, first, second): + def do_check(first, second): try: matches = self._ignored[first] return second in matches except KeyError: return False - - return do_check(first,second) or do_check(second,first) - + + return do_check(first, second) or do_check(second, first) + def Clear(self): self._ignored = {} self._count = 0 - - def Filter(self,func): + + def Filter(self, func): """Applies a filter on all ignored items, and remove all matches where func(first,second) doesn't return True. """ filtered = IgnoreList() - for first,second in self: - if func(first,second): - filtered.Ignore(first,second) + for first, second in self: + if func(first, second): + filtered.Ignore(first, second) self._ignored = filtered._ignored self._count = filtered._count - - def Ignore(self,first,second): - if self.AreIgnored(first,second): + + def Ignore(self, first, second): + if self.AreIgnored(first, second): return try: matches = self._ignored[first] @@ -70,7 +70,7 @@ class IgnoreList: matches.add(second) self._ignored[first] = matches self._count += 1 - + def remove(self, first, second): def inner(first, second): try: @@ -85,14 +85,14 @@ class IgnoreList: return False except KeyError: return False - + if not inner(first, second): if not inner(second, first): raise ValueError() - + def load_from_xml(self, infile): """Loads the ignore list from a XML created with save_to_xml. - + infile can be a file object or a filename. """ try: @@ -109,10 +109,10 @@ class IgnoreList: subfile_path = sfn.get('path') if subfile_path: self.Ignore(file_path, subfile_path) - + def save_to_xml(self, outfile): """Create a XML file that can be used by load_from_xml. - + outfile can be a file object or a filename. """ root = ET.Element('ignore_list') @@ -125,5 +125,5 @@ class IgnoreList: tree = ET.ElementTree(root) with FileOrPath(outfile, 'wb') as fp: tree.write(fp, encoding='utf-8') - + diff --git a/core/results.py b/core/results.py index 41604c7d..73eb897c 100644 --- a/core/results.py +++ b/core/results.py @@ -96,7 +96,10 @@ class Results(Markable): self.__dupes = flatten(group.dupes for group in self.groups) if None in self.__dupes: # This is debug logging to try to figure out #44 - logging.warning("There is a None value in the Results' dupe list. dupes: %r groups: %r", self.__dupes, self.groups) + logging.warning( + "There is a None value in the Results' dupe list. dupes: %r groups: %r", + self.__dupes, self.groups + ) if self.__filtered_dupes: self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes] sd = self.__dupes_sort_descriptor @@ -249,7 +252,8 @@ class Results(Markable): second_file = dupes[int(attrs['second'])] percentage = int(attrs['percentage']) group.add_match(engine.Match(first_file, second_file, percentage)) - except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds + except (IndexError, KeyError, ValueError): + # Covers missing attr, non-int values and indexes out of bounds pass if (not group.matches) and (len(dupes) >= 2): do_match(dupes[0], dupes[1:], group) @@ -393,7 +397,7 @@ class Results(Markable): self.__get_dupe_list() keyfunc = lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta) self.__dupes.sort(key=keyfunc, reverse=not asc) - self.__dupes_sort_descriptor = (key,asc,delta) + self.__dupes_sort_descriptor = (key, asc, delta) def sort_groups(self, key, asc=True): """Sort :attr:`groups` according to ``key``. @@ -405,9 +409,10 @@ class Results(Markable): """ keyfunc = lambda g: self.app._get_group_sort_key(g, key) self.groups.sort(key=keyfunc, reverse=not asc) - self.__groups_sort_descriptor = (key,asc) + self.__groups_sort_descriptor = (key, asc) #---Properties - dupes = property(__get_dupe_list) - groups = property(__get_groups, __set_groups) + dupes = property(__get_dupe_list) + groups = property(__get_groups, __set_groups) stat_line = property(__get_stat_line) + diff --git a/core/scanner.py b/core/scanner.py index 6f86f5d1..c0609ba4 100644 --- a/core/scanner.py +++ b/core/scanner.py @@ -81,7 +81,9 @@ class Scanner: files = [f for f in files if f.size >= self.size_threshold] if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}: sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size' - return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==ScanType.ContentsAudio, j=j) + return engine.getmatches_by_contents( + files, sizeattr, partial=self.scan_type == ScanType.ContentsAudio, j=j + ) else: j = j.start_subjob([2, 8]) kw = {} @@ -94,7 +96,11 @@ class Scanner: func = { ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)), ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)), - ScanType.Tag: lambda f: [engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags], + ScanType.Tag: lambda f: [ + engine.getwords(str(getattr(f, attrname))) + for attrname in SCANNABLE_TAGS + if attrname in self.scanned_tags + ], }[self.scan_type] for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")): logging.debug("Reading metadata of {}".format(str(f.path))) @@ -152,8 +158,10 @@ class Scanner: if self.ignore_list: j = j.start_subjob(2) iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list")) - matches = [m for m in iter_matches - if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))] + matches = [ + m for m in iter_matches + if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path)) + ] logging.info('Grouping matches') groups = engine.get_groups(matches, j) matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) @@ -178,10 +186,11 @@ class Scanner: g.prioritize(self._key_func, self._tie_breaker) return groups - match_similar_words = False + match_similar_words = False min_match_percentage = 80 - mix_file_kind = True - scan_type = ScanType.Filename - scanned_tags = {'artist', 'title'} - size_threshold = 0 - word_weighting = False + mix_file_kind = True + scan_type = ScanType.Filename + scanned_tags = {'artist', 'title'} + size_threshold = 0 + word_weighting = False + diff --git a/core_me/__init__.py b/core_me/__init__.py index 66c165f4..d5d48b43 100644 --- a/core_me/__init__.py +++ b/core_me/__init__.py @@ -1,2 +1,3 @@ __version__ = '6.8.0' -__appname__ = 'dupeGuru Music Edition' \ No newline at end of file +__appname__ = 'dupeGuru Music Edition' + diff --git a/core_me/app.py b/core_me/app.py index 662cdf5f..ed49aecc 100644 --- a/core_me/app.py +++ b/core_me/app.py @@ -1,8 +1,8 @@ # Created On: 2011/09/20 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from core.app import DupeGuru as DupeGuruBase @@ -13,28 +13,30 @@ from .result_table import ResultTable class DupeGuru(DupeGuruBase): NAME = __appname__ - METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', - 'album', 'genre', 'year', 'track', 'comment'] + METADATA_TO_READ = [ + 'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', + 'album', 'genre', 'year', 'track', 'comment' + ] def __init__(self, view): DupeGuruBase.__init__(self, view) self.scanner = scanner.ScannerME() self.directories.fileclasses = [fs.MusicFile] - + def _get_dupe_sort_key(self, dupe, get_group, key, delta): if key == 'folder_path': dupe_folder_path = getattr(dupe, 'display_folder_path', dupe.folder_path) return str(dupe_folder_path).lower() return DupeGuruBase._get_dupe_sort_key(self, dupe, get_group, key, delta) - + def _get_group_sort_key(self, group, key): if key == 'folder_path': dupe_folder_path = getattr(group.ref, 'display_folder_path', group.ref.folder_path) return str(dupe_folder_path).lower() return DupeGuruBase._get_group_sort_key(self, group, key) - + def _prioritization_categories(self): return prioritize.all_categories() - + def _create_result_table(self): return ResultTable(self) diff --git a/core_me/fs.py b/core_me/fs.py index 35f7bebc..43e43037 100644 --- a/core_me/fs.py +++ b/core_me/fs.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2009-10-23 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from hsaudiotag import auto @@ -12,32 +12,34 @@ from hscommon.util import get_file_ext, format_size, format_time from core.app import format_timestamp, format_perc, format_words, format_dupe_count from core import fs -TAG_FIELDS = {'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist', - 'album', 'genre', 'year', 'track', 'comment'} +TAG_FIELDS = { + 'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist', + 'album', 'genre', 'year', 'track', 'comment' +} class MusicFile(fs.File): INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO.update({ 'audiosize': 0, - 'bitrate' : 0, - 'duration' : 0, - 'samplerate':0, - 'artist' : '', - 'album' : '', - 'title' : '', - 'genre' : '', - 'comment' : '', - 'year' : '', - 'track' : 0, + 'bitrate': 0, + 'duration': 0, + 'samplerate': 0, + 'artist': '', + 'album': '', + 'title': '', + 'genre': '', + 'comment': '', + 'year': '', + 'track': 0, }) __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys()) - + @classmethod def can_handle(cls, path): if not fs.File.can_handle(path): return False return get_file_ext(path.name) in auto.EXT2CLASS - + def get_display_info(self, group, delta): size = self.size duration = self.duration @@ -67,7 +69,7 @@ class MusicFile(fs.File): 'bitrate': str(bitrate), 'samplerate': str(samplerate), 'extension': self.extension, - 'mtime': format_timestamp(mtime,delta and m), + 'mtime': format_timestamp(mtime, delta and m), 'title': self.title, 'artist': self.artist, 'album': self.album, @@ -79,11 +81,11 @@ class MusicFile(fs.File): 'words': format_words(self.words) if hasattr(self, 'words') else '', 'dupe_count': format_dupe_count(dupe_count), } - + def _get_md5partial_offset_and_size(self): f = auto.File(str(self.path)) return (f.audio_offset, f.audio_size) - + def _read_info(self, field): fs.File._read_info(self, field) if field in TAG_FIELDS: @@ -99,4 +101,4 @@ class MusicFile(fs.File): self.comment = f.comment self.year = f.year self.track = f.track - + diff --git a/core_me/prioritize.py b/core_me/prioritize.py index 649392be..99fd4940 100644 --- a/core_me/prioritize.py +++ b/core_me/prioritize.py @@ -1,35 +1,40 @@ # Created On: 2011/09/16 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from hscommon.trans import trget -from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory, - SizeCategory, MtimeCategory) +from core.prioritize import ( + KindCategory, FolderCategory, FilenameCategory, NumericalCategory, + SizeCategory, MtimeCategory +) coltr = trget('columns') class DurationCategory(NumericalCategory): NAME = coltr("Duration") - + def extract_value(self, dupe): return dupe.duration class BitrateCategory(NumericalCategory): NAME = coltr("Bitrate") - + def extract_value(self, dupe): return dupe.bitrate class SamplerateCategory(NumericalCategory): NAME = coltr("Samplerate") - + def extract_value(self, dupe): return dupe.samplerate def all_categories(): - return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory, - BitrateCategory, SamplerateCategory, MtimeCategory] + return [ + KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory, + BitrateCategory, SamplerateCategory, MtimeCategory + ] + diff --git a/core_pe/block.py b/core_pe/block.py index 565a112e..0a707038 100644 --- a/core_pe/block.py +++ b/core_pe/block.py @@ -1,17 +1,17 @@ # Created By: Virgil Dupras # Created On: 2006/09/01 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license -from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 +from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # NOQA # Converted to C # def getblock(image): # """Returns a 3 sized tuple containing the mean color of 'image'. -# +# # image: a PIL image or crop. # """ # if image.size[0]: @@ -28,7 +28,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # This is not used anymore # def getblocks(image,blocksize): # """Returns a list of blocks (3 sized tuples). -# +# # image: A PIL image to base the blocks on. # blocksize: The size of the blocks to be create. This is a single integer, defining # both width and height (blocks are square). @@ -46,7 +46,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # Converted to C # def getblocks2(image,block_count_per_side): # """Returns a list of blocks (3 sized tuples). -# +# # image: A PIL image to base the blocks on. # block_count_per_side: This integer determine the number of blocks the function will return. # If it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not @@ -73,7 +73,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # Converted to C # def diff(first, second): # """Returns the difference between the first block and the second. -# +# # It returns an absolute sum of the 3 differences (RGB). # """ # r1, g1, b1 = first @@ -83,7 +83,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # Converted to C # def avgdiff(first, second, limit=768, min_iterations=1): # """Returns the average diff between first blocks and seconds. -# +# # If the result surpasses limit, limit + 1 is returned, except if less than min_iterations # iterations have been made in the blocks. # """ @@ -106,7 +106,7 @@ from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2 # This is not used anymore # def maxdiff(first,second,limit=768): # """Returns the max diff between first blocks and seconds. -# +# # If the result surpasses limit, the first max being over limit is returned. # """ # if len(first) != len(second): diff --git a/core_pe/cache.py b/core_pe/cache.py index 81e8031a..c8fb127b 100644 --- a/core_pe/cache.py +++ b/core_pe/cache.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2006/09/14 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import os @@ -15,11 +15,11 @@ from ._cache import string_to_colors def colors_to_string(colors): """Transform the 3 sized tuples 'colors' into a hex string. - + [(0,100,255)] --> 0064ff [(1,2,3),(4,5,6)] --> 010203040506 """ - return ''.join(['%02x%02x%02x' % (r,g,b) for r,g,b in colors]) + return ''.join(['%02x%02x%02x' % (r, g, b) for r, g, b in colors]) # This function is an important bottleneck of dupeGuru PE. It has been converted to C. # def string_to_colors(s): @@ -38,18 +38,18 @@ class Cache: self.dbname = db self.con = None self._create_con() - + def __contains__(self, key): sql = "select count(*) from pictures where path = ?" result = self.con.execute(sql, [key]).fetchall() return result[0][0] > 0 - + def __delitem__(self, key): if key not in self: raise KeyError(key) sql = "delete from pictures where path = ?" self.con.execute(sql, [key]) - + # Optimized def __getitem__(self, key): if isinstance(key, int): @@ -62,17 +62,17 @@ class Cache: return result else: raise KeyError(key) - + def __iter__(self): sql = "select path from pictures" result = self.con.execute(sql) return (row[0] for row in result) - + def __len__(self): sql = "select count(*) from pictures" result = self.con.execute(sql).fetchall() return result[0][0] - + def __setitem__(self, path_str, blocks): blocks = colors_to_string(blocks) if op.exists(path_str): @@ -89,15 +89,15 @@ class Cache: logging.warning('Picture cache could not set value for key %r', path_str) except sqlite.DatabaseError as e: logging.warning('DatabaseError while setting value for key %r: %s', path_str, str(e)) - + def _create_con(self, second_try=False): def create_tables(): logging.debug("Creating picture cache tables.") - self.con.execute("drop table if exists pictures"); - self.con.execute("drop index if exists idx_path"); - self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)"); + self.con.execute("drop table if exists pictures") + self.con.execute("drop index if exists idx_path") + self.con.execute("create table pictures(path TEXT, mtime INTEGER, blocks TEXT)") self.con.execute("create index idx_path on pictures (path)") - + self.con = sqlite.connect(self.dbname, isolation_level=None) try: self.con.execute("select path, mtime, blocks from pictures where 1=2") @@ -110,23 +110,23 @@ class Cache: self.con.close() os.remove(self.dbname) self._create_con(second_try=True) - + def clear(self): self.close() if self.dbname != ':memory:': os.remove(self.dbname) self._create_con() - + def close(self): if self.con is not None: self.con.close() self.con = None - + def filter(self, func): to_delete = [key for key in self if not func(key)] for key in to_delete: del self[key] - + def get_id(self, path): sql = "select rowid from pictures where path = ?" result = self.con.execute(sql, [path]).fetchone() @@ -134,15 +134,15 @@ class Cache: return result[0] else: raise ValueError(path) - + def get_multiple(self, rowids): sql = "select rowid, blocks from pictures where rowid in (%s)" % ','.join(map(str, rowids)) cur = self.con.execute(sql) return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur) - + def purge_outdated(self): """Go through the cache and purge outdated records. - + A record is outdated if the picture doesn't exist or if its mtime is greater than the one in the db. """ @@ -159,4 +159,4 @@ class Cache: if todelete: sql = "delete from pictures where rowid in (%s)" % ','.join(map(str, todelete)) self.con.execute(sql) - + diff --git a/core_pe/exif.py b/core_pe/exif.py index 93841fa6..3b46d7b9 100644 --- a/core_pe/exif.py +++ b/core_pe/exif.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2011-04-20 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license # Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain) @@ -181,7 +181,7 @@ class Fraction: def __repr__(self): return '%d/%d' % (self.num, self.den) - + class TIFF_file: def __init__(self, data): @@ -201,14 +201,14 @@ class TIFF_file: logging.debug(self.endian) logging.debug("Slice for offset %d length %d: %r and value: %d", offset, length, slice, val) return val - + def first_IFD(self): return self.s2n(4, 4) - + def next_IFD(self, ifd): entries = self.s2n(ifd, 2) return self.s2n(ifd + 2 + 12 * entries, 4) - + def list_IFDs(self): i = self.first_IFD() a = [] @@ -216,7 +216,7 @@ class TIFF_file: a.append(i) i = self.next_IFD(i) return a - + def dump_IFD(self, ifd): entries = self.s2n(ifd, 2) logging.debug("Entries for IFD %d: %d", ifd, entries) @@ -230,7 +230,7 @@ class TIFF_file: type = self.s2n(entry+2, 2) if not 1 <= type <= 10: continue # not handled - typelen = [ 1, 1, 2, 4, 8, 1, 1, 2, 4, 8 ] [type-1] + typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type-1] count = self.s2n(entry+4, 4) if count > MAX_COUNT: logging.debug("Probably corrupt. Aborting.") @@ -247,7 +247,7 @@ class TIFF_file: for j in range(count): if type in {5, 10}: # The type is either 5 or 10 - value_j = Fraction(self.s2n(offset, 4, signed), + value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset+4, 4, signed)) else: # Not a fraction @@ -255,7 +255,7 @@ class TIFF_file: values.append(value_j) offset = offset + typelen # Now "values" is either a string or an array - a.append((tag,type,values)) + a.append((tag, type, values)) return a def read_exif_header(fp): @@ -283,13 +283,13 @@ def get_fields(fp): logging.debug("Exif header length: %d bytes", length) data = fp.read(length-8) data_format = data[0] - logging.debug("%s format", {INTEL_ENDIAN:'Intel', MOTOROLA_ENDIAN:'Motorola'}[data_format]) + logging.debug("%s format", {INTEL_ENDIAN: 'Intel', MOTOROLA_ENDIAN: 'Motorola'}[data_format]) T = TIFF_file(data) # There may be more than one IFD per file, but we only read the first one because others are # most likely thumbnails. main_IFD_offset = T.first_IFD() result = {} - + def add_tag_to_result(tag, values): try: stag = EXIF_TAGS[tag] @@ -298,7 +298,7 @@ def get_fields(fp): if stag in result: return # don't overwrite data result[stag] = values - + logging.debug("IFD at offset %d", main_IFD_offset) IFD = T.dump_IFD(main_IFD_offset) exif_off = gps_off = 0 diff --git a/core_pe/matchblock.py b/core_pe/matchblock.py index 22e2e6fb..85c047e5 100644 --- a/core_pe/matchblock.py +++ b/core_pe/matchblock.py @@ -93,8 +93,10 @@ def get_chunks(pictures): chunk_count = max(min_chunk_count, chunk_count) chunk_size = (len(pictures) // chunk_count) + 1 chunk_size = max(MIN_CHUNK_SIZE, chunk_size) - logging.info("Creating %d chunks with a chunk size of %d for %d pictures", chunk_count, - chunk_size, len(pictures)) + logging.info( + "Creating %d chunks with a chunk size of %d for %d pictures", chunk_count, + chunk_size, len(pictures) + ) chunks = [pictures[i:i+chunk_size] for i in range(0, len(pictures), chunk_size)] return chunks @@ -142,7 +144,7 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul def collect_results(collect_all=False): # collect results and wait until the queue is small enough to accomodate a new results. - nonlocal async_results, matches, comparison_count + nonlocal async_results, matches, comparison_count, comparisons_to_do limit = 0 if collect_all else RESULTS_QUEUE_LIMIT while len(async_results) > limit: ready, working = extract(lambda r: r.ready(), async_results) @@ -150,7 +152,8 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul matches += result.get() async_results.remove(result) comparison_count += 1 - progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do)) + # About the NOQA below: I think there's a bug in pyflakes. To investigate... + progress_msg = tr("Performed %d/%d chunk matches") % (comparison_count, len(comparisons_to_do)) # NOQA j.set_progress(comparison_count, progress_msg) j = j.start_subjob([3, 7]) diff --git a/core_pe/matchexif.py b/core_pe/matchexif.py index 7b461e0e..d180161b 100644 --- a/core_pe/matchexif.py +++ b/core_pe/matchexif.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2011-04-20 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from collections import defaultdict @@ -27,4 +27,5 @@ def getmatches(files, match_scaled, j): if (not match_scaled) and (p1.dimensions != p2.dimensions): continue matches.append(Match(p1, p2, 100)) - return matches \ No newline at end of file + return matches + diff --git a/core_pe/photo.py b/core_pe/photo.py index 61f0cab3..fa57c929 100644 --- a/core_pe/photo.py +++ b/core_pe/photo.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2011-05-29 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import logging @@ -23,20 +23,20 @@ def get_delta_dimensions(value, ref_value): class Photo(fs.File): INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO.update({ - 'dimensions': (0,0), + 'dimensions': (0, 0), 'exif_timestamp': '', }) __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys()) - + # These extensions are supported on all platforms HANDLED_EXTS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'} - + def _plat_get_dimensions(self): raise NotImplementedError() - + def _plat_get_blocks(self, block_count_per_side, orientation): raise NotImplementedError() - + def _get_orientation(self): if not hasattr(self, '_cached_orientation'): try: @@ -48,7 +48,7 @@ class Photo(fs.File): except Exception: # Couldn't read EXIF data, no transforms self._cached_orientation = 0 return self._cached_orientation - + def _get_exif_timestamp(self): try: with self.path.open('rb') as fp: @@ -57,11 +57,11 @@ class Photo(fs.File): except Exception: logging.info("Couldn't read EXIF of picture: %s", self.path) return '' - + @classmethod def can_handle(cls, path): return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS - + def get_display_info(self, group, delta): size = self.size mtime = self.mtime @@ -90,7 +90,7 @@ class Photo(fs.File): 'percentage': format_perc(percentage), 'dupe_count': format_dupe_count(dupe_count), } - + def _read_info(self, field): fs.File._read_info(self, field) if field == 'dimensions': @@ -99,7 +99,7 @@ class Photo(fs.File): self.dimensions = (self.dimensions[1], self.dimensions[0]) elif field == 'exif_timestamp': self.exif_timestamp = self._get_exif_timestamp() - + def get_blocks(self, block_count_per_side): return self._plat_get_blocks(block_count_per_side, self._get_orientation()) - + diff --git a/core_pe/prioritize.py b/core_pe/prioritize.py index d65c8d73..fbad9187 100644 --- a/core_pe/prioritize.py +++ b/core_pe/prioritize.py @@ -1,27 +1,31 @@ # Created On: 2011/09/16 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from hscommon.trans import trget -from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory, - SizeCategory, MtimeCategory) +from core.prioritize import ( + KindCategory, FolderCategory, FilenameCategory, NumericalCategory, + SizeCategory, MtimeCategory +) coltr = trget('columns') class DimensionsCategory(NumericalCategory): NAME = coltr("Dimensions") - + def extract_value(self, dupe): return dupe.dimensions - + def invert_numerical_value(self, value): width, height = value return (-width, -height) def all_categories(): - return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory, - MtimeCategory] + return [ + KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory, + MtimeCategory + ] diff --git a/package.py b/package.py index 122872ba..a4f2f32d 100644 --- a/package.py +++ b/package.py @@ -16,9 +16,11 @@ import platform import glob from hscommon.plat import ISWINDOWS, ISLINUX -from hscommon.build import (add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog, +from hscommon.build import ( + add_to_pythonpath, print_and_do, copy_packages, build_debian_changelog, copy_qt_plugins, get_module_version, filereplace, copy, setup_package_argparser, - package_cocoa_app_in_dmg, copy_all, find_in_path) + package_cocoa_app_in_dmg, copy_all, find_in_path +) def parse_args(): parser = ArgumentParser() @@ -52,7 +54,8 @@ def package_windows(edition, dev): plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats'] copy_qt_plugins(plugin_names, plugin_dest) - # Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it explicitly because of that. + # Since v4.2.3, cx_freeze started to falsely include tkinter in the package. We exclude it + # explicitly because of that. options = { 'build_exe': { 'includes': 'atexit', @@ -151,8 +154,10 @@ def package_debian_distribution(edition, distribution): changelog_dest = op.join(debdest, 'changelog') project_name = debopts['pkgname'] from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition] - build_debian_changelog(changelogpath, changelog_dest, project_name, from_version=from_version, - distribution=distribution) + build_debian_changelog( + changelogpath, changelog_dest, project_name, from_version=from_version, + distribution=distribution + ) shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath) os.chdir(destpath) cmd = "dpkg-buildpackage -S" diff --git a/qt/base/app.py b/qt/base/app.py index cbd40e68..e8ab53eb 100644 --- a/qt/base/app.py +++ b/qt/base/app.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2009-04-25 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import sys @@ -14,13 +14,12 @@ from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox from hscommon.trans import trget -from hscommon.plat import ISLINUX from hscommon import desktop from qtlib.about_box import AboutBox from qtlib.recent import Recent from qtlib.util import createActions -from qtlib.progress_window import ProgressWindow +from qtlib.progress_window import ProgressWindow from . import platform from .result_window import ResultWindow @@ -35,13 +34,13 @@ class DupeGuru(QObject): MODELCLASS = None LOGO_NAME = '' NAME = '' - + DETAILS_DIALOG_CLASS = None RESULT_WINDOW_CLASS = ResultWindow RESULT_MODEL_CLASS = None PREFERENCES_CLASS = None PREFERENCES_DIALOG_CLASS = None - + def __init__(self, **kwargs): super().__init__(**kwargs) self.prefs = self.PREFERENCES_CLASS() @@ -49,7 +48,7 @@ class DupeGuru(QObject): self.model = self.MODELCLASS(view=self) self._setup() self.prefsChanged.emit(self.prefs) - + #--- Private def _setup(self): self._setupActions() @@ -58,24 +57,24 @@ class DupeGuru(QObject): self.recentResults.mustOpenItem.connect(self.model.load_from) self.directories_dialog = DirectoriesDialog(self) self.resultWindow = self.RESULT_WINDOW_CLASS(self.directories_dialog, self) - self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window) + self.progress_window = ProgressWindow(self.resultWindow, self.model.progress_window) self.details_dialog = self.DETAILS_DIALOG_CLASS(self.resultWindow, self) self.problemDialog = ProblemDialog(parent=self.resultWindow, model=self.model.problem_dialog) self.ignoreListDialog = IgnoreListDialog(parent=self.resultWindow, model=self.model.ignore_list_dialog) self.deletionOptions = DeletionOptions(parent=self.resultWindow, model=self.model.deletion_options) self.preferences_dialog = self.PREFERENCES_DIALOG_CLASS(self.resultWindow, self) self.about_box = AboutBox(self.resultWindow, self) - + self.directories_dialog.show() self.model.load() - - # The timer scheme is because if the nag is not shown before the application is + + # The timer scheme is because if the nag is not shown before the application is # completely initialized, the nag will be shown before the app shows up in the task bar # In some circumstances, the nag is hidden by other window, which may make the user think # that the application haven't launched. QTimer.singleShot(0, self.finishedLaunching) QCoreApplication.instance().aboutToQuit.connect(self.application_will_terminate) - + def _setupActions(self): # Setup actions that are common to both the directory dialog and the results window. # (name, shortcut, icon, desc, func) @@ -88,61 +87,61 @@ class DupeGuru(QObject): ('actionOpenDebugLog', '', '', tr("Open Debug Log"), self.openDebugLogTriggered), ] createActions(ACTIONS, self) - + def _update_options(self): self.model.scanner.mix_file_kind = self.prefs.mix_file_kind self.model.options['escape_filter_regexp'] = self.prefs.use_regexp self.model.options['clean_empty_dirs'] = self.prefs.remove_empty_folders self.model.options['ignore_hardlink_matches'] = self.prefs.ignore_hardlink_matches self.model.options['copymove_dest_type'] = self.prefs.destination_type - + #--- Public def add_selected_to_ignore_list(self): self.model.add_selected_to_ignore_list() - + def remove_selected(self): self.model.remove_selected(self) - + def confirm(self, title, msg, default_button=QMessageBox.Yes): active = QApplication.activeWindow() buttons = QMessageBox.Yes | QMessageBox.No answer = QMessageBox.question(active, title, msg, buttons, default_button) return answer == QMessageBox.Yes - + def invokeCustomCommand(self): self.model.invoke_custom_command() - + def show_details(self): self.details_dialog.show() - + def showResultsWindow(self): self.resultWindow.show() - + #--- Signals willSavePrefs = pyqtSignal() prefsChanged = pyqtSignal(object) - + #--- Events def finishedLaunching(self): if sys.getfilesystemencoding() == 'ascii': # No need to localize this, it's a debugging message. msg = "Something is wrong with the way your system locale is set. If the files you're "\ - "scanning have accented letters, you'll probably get a crash. It is advised that "\ - "you set your system locale properly." + "scanning have accented letters, you'll probably get a crash. It is advised that "\ + "you set your system locale properly." QMessageBox.warning(self.directories_dialog, "Wrong Locale", msg) - + def application_will_terminate(self): self.willSavePrefs.emit() self.prefs.save() self.model.save() - + def ignoreListTriggered(self): self.model.ignore_list_dialog.show() - + def openDebugLogTriggered(self): debugLogPath = op.join(self.model.appdata, 'debug.log') desktop.open_path(debugLogPath) - + def preferencesTriggered(self): self.preferences_dialog.load() result = self.preferences_dialog.exec() @@ -151,42 +150,42 @@ class DupeGuru(QObject): self.prefs.save() self._update_options() self.prefsChanged.emit(self.prefs) - + def quitTriggered(self): self.directories_dialog.close() - + def showAboutBoxTriggered(self): self.about_box.show() - + def showHelpTriggered(self): base_path = platform.HELP_PATH url = QUrl.fromLocalFile(op.abspath(op.join(base_path, 'index.html'))) QDesktopServices.openUrl(url) - + #--- model --> view def get_default(self, key): return self.prefs.get_value(key) - + def set_default(self, key, value): self.prefs.set_value(key, value) - + def show_message(self, msg): window = QApplication.activeWindow() QMessageBox.information(window, '', msg) - + def ask_yes_no(self, prompt): return self.confirm('', prompt) - + def show_results_window(self): self.showResultsWindow() - + def show_problem_dialog(self): self.problemDialog.show() - + def select_dest_folder(self, prompt): flags = QFileDialog.ShowDirsOnly return QFileDialog.getExistingDirectory(self.resultWindow, prompt, '', flags) - + def select_dest_file(self, prompt, extension): files = tr("{} file (*.{})").format(extension.upper(), extension) destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, '', files) diff --git a/qt/base/cxfreeze_fix.py b/qt/base/cxfreeze_fix.py index 026da82a..d659d077 100644 --- a/qt/base/cxfreeze_fix.py +++ b/qt/base/cxfreeze_fix.py @@ -1,5 +1,6 @@ # cxfreeze has some problems detecting all dependencies. # This modules explicitly import those problematic modules. +# flake8: noqa import xml.etree.ElementPath import gzip diff --git a/qt/base/deletion_options.py b/qt/base/deletion_options.py index ef570931..ed47c466 100644 --- a/qt/base/deletion_options.py +++ b/qt/base/deletion_options.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2012-05-30 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import Qt @@ -21,11 +21,11 @@ class DeletionOptions(QDialog): self.model = model self._setupUi() self.model.view = self - + self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) - + def _setupUi(self): self.setWindowTitle(tr("Deletion Options")) self.resize(400, 270) @@ -34,8 +34,10 @@ class DeletionOptions(QDialog): self.verticalLayout.addWidget(self.msgLabel) self.linkCheckbox = QCheckBox(tr("Link deleted files")) self.verticalLayout.addWidget(self.linkCheckbox) - text = tr("After having deleted a duplicate, place a link targeting the reference file " - "to replace the deleted file.") + text = tr( + "After having deleted a duplicate, place a link targeting the reference file " + "to replace the deleted file." + ) self.linkMessageLabel = QLabel(text) self.linkMessageLabel.setWordWrap(True) self.verticalLayout.addWidget(self.linkMessageLabel) @@ -46,8 +48,10 @@ class DeletionOptions(QDialog): self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)")) self.directCheckbox = QCheckBox(tr("Directly delete files")) self.verticalLayout.addWidget(self.directCheckbox) - text = tr("Instead of sending files to trash, delete them directly. This option is usually " - "used as a workaround when the normal deletion method doesn't work.") + text = tr( + "Instead of sending files to trash, delete them directly. This option is usually " + "used as a workaround when the normal deletion method doesn't work." + ) self.directMessageLabel = QLabel(text) self.directMessageLabel.setWordWrap(True) self.verticalLayout.addWidget(self.directMessageLabel) @@ -55,15 +59,15 @@ class DeletionOptions(QDialog): self.buttonBox.addButton(tr("Proceed"), QDialogButtonBox.AcceptRole) self.buttonBox.addButton(tr("Cancel"), QDialogButtonBox.RejectRole) self.verticalLayout.addWidget(self.buttonBox) - + #--- Signals def linkCheckboxChanged(self, changed: int): self.model.link_deleted = bool(changed) - + #--- model --> view def update_msg(self, msg: str): self.msgLabel.setText(msg) - + def show(self): self.linkCheckbox.setChecked(self.model.link_deleted) self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0 @@ -73,7 +77,7 @@ class DeletionOptions(QDialog): self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1 self.model.direct = self.directCheckbox.isChecked() return result == QDialog.Accepted - + def set_hardlink_option_enabled(self, is_enabled: bool): self.linkTypeRadio.setEnabled(is_enabled) - + diff --git a/qt/base/directories_dialog.py b/qt/base/directories_dialog.py index f90b30ac..2d293f70 100644 --- a/qt/base/directories_dialog.py +++ b/qt/base/directories_dialog.py @@ -1,15 +1,17 @@ # Created By: Virgil Dupras # Created On: 2009-04-25 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import QRect -from PyQt5.QtWidgets import (QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView, +from PyQt5.QtWidgets import ( + QWidget, QFileDialog, QHeaderView, QVBoxLayout, QHBoxLayout, QTreeView, QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel, - QApplication) + QApplication +) from PyQt5.QtGui import QPixmap, QIcon from hscommon.trans import trget @@ -39,7 +41,7 @@ class DirectoriesDialog(QMainWindow): self._updateRemoveButton() self._updateLoadResultsButton() self._setupBindings() - + def _setupBindings(self): self.scanButton.clicked.connect(self.scanButtonClicked) self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger) @@ -51,7 +53,7 @@ class DirectoriesDialog(QMainWindow): self.recentFolders.mustOpenItem.connect(self.app.model.add_directory) self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders) self.app.willSavePrefs.connect(self.appWillSavePrefs) - + def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ @@ -60,7 +62,7 @@ class DirectoriesDialog(QMainWindow): ('actionAddFolder', '', '', tr("Add Folder..."), self.addFolderTriggered), ] createActions(ACTIONS, self) - + def _setupMenu(self): self.menubar = QMenuBar(self) self.menubar.setGeometry(QRect(0, 0, 42, 22)) @@ -73,7 +75,7 @@ class DirectoriesDialog(QMainWindow): self.menuLoadRecent = QMenu(self.menuFile) self.menuLoadRecent.setTitle(tr("Load Recent Results")) self.setMenuBar(self.menubar) - + self.menuFile.addAction(self.actionLoadResults) self.menuFile.addAction(self.menuLoadRecent.menuAction()) self.menuFile.addSeparator() @@ -84,21 +86,21 @@ class DirectoriesDialog(QMainWindow): self.menuHelp.addAction(self.app.actionShowHelp) self.menuHelp.addAction(self.app.actionOpenDebugLog) self.menuHelp.addAction(self.app.actionAbout) - + self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) - + # Recent folders menu self.menuRecentFolders = QMenu() self.menuRecentFolders.addAction(self.actionAddFolder) self.menuRecentFolders.addSeparator() - + # Recent results menu self.menuRecentResults = QMenu() self.menuRecentResults.addAction(self.actionLoadResults) self.menuRecentResults.addSeparator() - + def _setupUi(self): self.setWindowTitle(self.app.NAME) self.resize(420, 338) @@ -110,8 +112,8 @@ class DirectoriesDialog(QMainWindow): self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection) self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows) self.treeView.setAcceptDrops(True) - triggers = QAbstractItemView.DoubleClicked|QAbstractItemView.EditKeyPressed\ - |QAbstractItemView.SelectedClicked + triggers = QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed\ + | QAbstractItemView.SelectedClicked self.treeView.setEditTriggers(triggers) self.treeView.setDragDropOverwriteMode(True) self.treeView.setDragDropMode(QAbstractItemView.DropOnly) @@ -136,41 +138,41 @@ class DirectoriesDialog(QMainWindow): self.horizontalLayout.addWidget(self.scanButton) self.verticalLayout.addLayout(self.horizontalLayout) self.setCentralWidget(self.centralwidget) - + self._setupActions() self._setupMenu() - + if self.app.prefs.directoriesWindowRect is not None: self.setGeometry(self.app.prefs.directoriesWindowRect) else: moveToScreenCenter(self) - + def _setupColumns(self): header = self.treeView.header() header.setStretchLastSection(False) header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 100) - + def _updateAddButton(self): if self.recentFolders.isEmpty(): self.addFolderButton.setMenu(None) else: self.addFolderButton.setMenu(self.menuRecentFolders) - + def _updateRemoveButton(self): indexes = self.treeView.selectedIndexes() if not indexes: self.removeFolderButton.setEnabled(False) return self.removeFolderButton.setEnabled(True) - + def _updateLoadResultsButton(self): if self.app.recentResults.isEmpty(): self.loadResultsButton.setMenu(None) else: self.loadResultsButton.setMenu(self.menuRecentResults) - + #--- QWidget overrides def closeEvent(self, event): event.accept() @@ -181,7 +183,7 @@ class DirectoriesDialog(QMainWindow): event.ignore() if event.isAccepted(): QApplication.quit() - + #--- Events def addFolderTriggered(self): title = tr("Select a folder to add to the scanning list") @@ -192,14 +194,14 @@ class DirectoriesDialog(QMainWindow): self.lastAddedFolder = dirpath self.app.model.add_directory(dirpath) self.recentFolders.insertItem(dirpath) - + def appWillSavePrefs(self): self.app.prefs.directoriesWindowRect = self.geometry() - + def directoriesModelAddedFolders(self, folders): for folder in folders: self.recentFolders.insertItem(folder) - + def loadResultsTriggered(self): title = tr("Select a results file to load") files = ';;'.join([tr("dupeGuru Results (*.dupeguru)"), tr("All Files (*.*)")]) @@ -207,10 +209,10 @@ class DirectoriesDialog(QMainWindow): if destination: self.app.model.load_from(destination) self.app.recentResults.insertItem(destination) - + def removeFolderButtonClicked(self): self.directoriesModel.model.remove_selected() - + def scanButtonClicked(self): if self.app.model.results.is_modified: title = tr("Start a new scan") @@ -218,17 +220,17 @@ class DirectoriesDialog(QMainWindow): if not self.app.confirm(title, msg): return self.app.model.start_scanning() - + def selectionChanged(self, selected, deselected): self._updateRemoveButton() - + if __name__ == '__main__': import sys - from . import dg_rc + from . import dg_rc # NOQA from ..testapp import TestApp app = QApplication([]) dgapp = TestApp() dialog = DirectoriesDialog(None, dgapp) dialog.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt/base/directories_model.py b/qt/base/directories_model.py index 6c9e8759..8bcea30d 100644 --- a/qt/base/directories_model.py +++ b/qt/base/directories_model.py @@ -1,16 +1,18 @@ # Created By: Virgil Dupras # Created On: 2009-04-25 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import urllib.parse from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection -from PyQt5.QtWidgets import (QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox, - QStyleOptionViewItem, QApplication) +from PyQt5.QtWidgets import ( + QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox, + QStyleOptionViewItem, QApplication +) from PyQt5.QtGui import QBrush from hscommon.trans import trget @@ -23,10 +25,10 @@ STATES = [tr("Normal"), tr("Reference"), tr("Excluded")] class DirectoriesDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): - editor = QComboBox(parent); + editor = QComboBox(parent) editor.addItems(STATES) return editor - + def paint(self, painter, option, index): self.initStyleOption(option, index) # No idea why, but this cast is required if we want to have access to the V4 valuess @@ -44,19 +46,19 @@ class DirectoriesDelegate(QStyledItemDelegate): painter.drawText(rect, Qt.AlignLeft, option.text) else: super().paint(painter, option, index) - + def setEditorData(self, editor, index): value = index.model().data(index, Qt.EditRole) - editor.setCurrentIndex(value); + editor.setCurrentIndex(value) editor.showPopup() - + def setModelData(self, editor, model, index): value = editor.currentIndex() model.setData(index, value, Qt.EditRole) - + def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) - + class DirectoriesModel(TreeModel): def __init__(self, model, view, **kwargs): @@ -65,18 +67,18 @@ class DirectoriesModel(TreeModel): self.model.view = self self.view = view self.view.setModel(self) - + self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged) - + def _createNode(self, ref, row): return RefNode(self, None, ref, row) - + def _getChildren(self): return list(self.model) - + def columnCount(self, parent=QModelIndex()): return 2 - + def data(self, index, role): if not index.isValid(): return None @@ -96,7 +98,7 @@ class DirectoriesModel(TreeModel): elif state == 2: return QBrush(Qt.red) return None - + def dropMimeData(self, mimeData, action, row, column, parentIndex): # the data in mimeData is urlencoded **in utf-8**!!! What we do is to decode, the mime data # with 'ascii', which works since it's urlencoded. Then, we pass that to urllib. @@ -111,7 +113,7 @@ class DirectoriesModel(TreeModel): self.foldersAdded.emit(paths) self.reset() return True - + def flags(self, index): if not index.isValid(): return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled @@ -119,16 +121,16 @@ class DirectoriesModel(TreeModel): if index.column() == 1: result |= Qt.ItemIsEditable return result - + def headerData(self, section, orientation, role): if orientation == Qt.Horizontal: if role == Qt.DisplayRole and section < len(HEADERS): return HEADERS[section] return None - + def mimeTypes(self): return ['text/uri-list'] - + def setData(self, index, value, role): if not index.isValid() or role != Qt.EditRole or index.column() != 1: return False @@ -136,24 +138,24 @@ class DirectoriesModel(TreeModel): ref = node.ref ref.state = value return True - + def supportedDropActions(self): # Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't # work with ActionMove either. So screw that, and accept anything. return Qt.ActionMask - + #--- Events def selectionChanged(self, selected, deselected): newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()] self.model.selected_nodes = newNodes - + #--- Signals foldersAdded = pyqtSignal(list) - + #--- model --> view def refresh(self): self.reset() - + def refresh_states(self): self.refreshData() - + diff --git a/qt/base/ignore_list_dialog.py b/qt/base/ignore_list_dialog.py index 5c6dd2ee..4e8c9350 100644 --- a/qt/base/ignore_list_dialog.py +++ b/qt/base/ignore_list_dialog.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2012-03-13 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import Qt @@ -23,11 +23,11 @@ class IgnoreListDialog(QDialog): self.model = model self.model.view = self self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView) - + self.removeSelectedButton.clicked.connect(self.model.remove_selected) self.clearButton.clicked.connect(self.model.clear) self.closeButton.clicked.connect(self.accept) - + def _setupUi(self): self.setWindowTitle(tr("Ignore List")) self.resize(540, 330) @@ -45,10 +45,14 @@ class IgnoreListDialog(QDialog): self.removeSelectedButton = QPushButton(tr("Remove Selected")) self.clearButton = QPushButton(tr("Clear")) self.closeButton = QPushButton(tr("Close")) - self.verticalLayout.addLayout(horizontalWrap([self.removeSelectedButton, self.clearButton, - None, self.closeButton])) - + self.verticalLayout.addLayout( + horizontalWrap([ + self.removeSelectedButton, self.clearButton, + None, self.closeButton + ]) + ) + #--- model --> view def show(self): super().show() - + diff --git a/qt/base/preferences_dialog.py b/qt/base/preferences_dialog.py index 39dfce63..e7b681ac 100644 --- a/qt/base/preferences_dialog.py +++ b/qt/base/preferences_dialog.py @@ -1,14 +1,16 @@ # Created By: Virgil Dupras # Created On: 2011-01-21 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, - QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox) +from PyQt5.QtWidgets import ( + QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, + QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox +) from hscommon.plat import ISOSX, ISLINUX from hscommon.trans import trget @@ -25,12 +27,12 @@ class PreferencesDialogBase(QDialog): super().__init__(parent, flags, **kwargs) self.app = app self._setupUi() - + self.filterHardnessSlider.valueChanged['int'].connect(self.filterHardnessLabel.setNum) self.buttonBox.clicked.connect(self.buttonClicked) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) - + def _setupScanTypeBox(self, labels): self.scanTypeHLayout = QHBoxLayout() self.scanTypeLabel = QLabel(self) @@ -43,7 +45,7 @@ class PreferencesDialogBase(QDialog): self.scanTypeComboBox.addItem(label) self.scanTypeHLayout.addWidget(self.scanTypeComboBox) self.widgetsVLayout.addLayout(self.scanTypeHLayout) - + def _setupFilterHardnessBox(self): self.filterHardnessHLayout = QHBoxLayout() self.filterHardnessLabel = QLabel(self) @@ -82,7 +84,7 @@ class PreferencesDialogBase(QDialog): self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel) self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2) self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout) - + def _setupBottomPart(self): # The bottom part of the pref panel is always the same in all editions. self.fontSizeLabel = QLabel(tr("Font size:")) @@ -107,18 +109,18 @@ class PreferencesDialogBase(QDialog): self.widgetsVLayout.addWidget(self.customCommandLabel) self.customCommandEdit = QLineEdit(self) self.widgetsVLayout.addWidget(self.customCommandEdit) - + def _setupAddCheckbox(self, name, label, parent=None): if parent is None: parent = self cb = QCheckBox(parent) cb.setText(label) setattr(self, name, cb) - + def _setupPreferenceWidgets(self): # Edition-specific pass - + def _setupUi(self): self.setWindowTitle(tr("Preferences")) self.resize(304, 263) @@ -134,15 +136,15 @@ class PreferencesDialogBase(QDialog): if (not ISOSX) and (not ISLINUX): self.mainVLayout.removeWidget(self.ignoreHardlinkMatches) self.ignoreHardlinkMatches.setHidden(True) - + def _load(self, prefs, setchecked): # Edition-specific pass - + def _save(self, prefs, ischecked): # Edition-specific pass - + def load(self, prefs=None): if prefs is None: prefs = self.app.prefs @@ -163,7 +165,7 @@ class PreferencesDialogBase(QDialog): langindex = 0 self.languageComboBox.setCurrentIndex(langindex) self._load(prefs, setchecked) - + def save(self): prefs = self.app.prefs prefs.filter_hardness = self.filterHardnessSlider.value() @@ -184,9 +186,10 @@ class PreferencesDialogBase(QDialog): QMessageBox.information(self, "", tr("dupeGuru has to restart for language changes to take effect.")) self.app.prefs.language = lang self._save(prefs, ischecked) - + #--- Events def buttonClicked(self, button): role = self.buttonBox.buttonRole(button) if role == QDialogButtonBox.ResetRole: - self.resetToDefaults() \ No newline at end of file + self.resetToDefaults() + diff --git a/qt/base/prioritize_dialog.py b/qt/base/prioritize_dialog.py index 89b90258..1388c6da 100644 --- a/qt/base/prioritize_dialog.py +++ b/qt/base/prioritize_dialog.py @@ -1,14 +1,16 @@ # Created By: Virgil Dupras # Created On: 2011-09-06 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import Qt, QMimeData, QByteArray -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView, - QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy) +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView, + QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy +) from hscommon.trans import trget from qtlib.selectable_list import ComboboxModel, ListviewModel @@ -24,7 +26,7 @@ class PrioritizationList(ListviewModel): if not index.isValid(): return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled - + #--- Drag & Drop def dropMimeData(self, mimeData, action, row, column, parentIndex): if not mimeData.hasFormat(MIME_INDEXES): @@ -37,17 +39,17 @@ class PrioritizationList(ListviewModel): indexes = list(map(int, strMimeData.split(','))) self.model.move_indexes(indexes, row) return True - + def mimeData(self, indexes): rows = {str(index.row()) for index in indexes} data = ','.join(rows) mimeData = QMimeData() mimeData.setData(MIME_INDEXES, QByteArray(data.encode())) return mimeData - + def mimeTypes(self): return [MIME_INDEXES] - + def supportedDropActions(self): return Qt.MoveAction @@ -59,9 +61,11 @@ class PrioritizeDialog(QDialog): self.model = PrioritizeDialogModel(app=app.model) self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox) self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView) - self.prioritizationList = PrioritizationList(model=self.model.prioritization_list, view=self.prioritizationListView) + self.prioritizationList = PrioritizationList( + model=self.model.prioritization_list, view=self.prioritizationListView + ) self.model.view = self - + self.addCriteriaButton.clicked.connect(self.model.add_selected) self.removeCriteriaButton.clicked.connect(self.model.remove_selected) self.buttonBox.accepted.connect(self.accept) @@ -70,11 +74,13 @@ class PrioritizeDialog(QDialog): def _setupUi(self): self.setWindowTitle(tr("Re-Prioritize duplicates")) self.resize(700, 400) - + #widgets - msg = tr("Add criteria to the right box and click OK to send the dupes that correspond the " + msg = tr( + "Add criteria to the right box and click OK to send the dupes that correspond the " "best to these criteria to their respective group's " - "reference position. Read the help file for more information.") + "reference position. Read the help file for more information." + ) self.promptLabel = QLabel(msg) self.promptLabel.setWordWrap(True) self.categoryCombobox = QComboBox() @@ -88,7 +94,7 @@ class PrioritizeDialog(QDialog): self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows) self.buttonBox = QDialogButtonBox() self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) - + # layout self.mainLayout = QVBoxLayout(self) self.mainLayout.addWidget(self.promptLabel) diff --git a/qt/base/problem_dialog.py b/qt/base/problem_dialog.py index b616a6d2..ca84a274 100644 --- a/qt/base/problem_dialog.py +++ b/qt/base/problem_dialog.py @@ -1,14 +1,16 @@ # Created By: Virgil Dupras # Created On: 2010-04-12 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy, - QLabel, QTableView, QAbstractItemView, QApplication) +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy, + QLabel, QTableView, QAbstractItemView, QApplication +) from hscommon.trans import trget from .problem_table import ProblemTable @@ -23,18 +25,20 @@ class ProblemDialog(QDialog): self.model = model self.model.view = self self.table = ProblemTable(self.model.problem_table, view=self.tableView) - + self.revealButton.clicked.connect(self.model.reveal_selected_dupe) self.closeButton.clicked.connect(self.accept) - + def _setupUi(self): self.setWindowTitle(tr("Problems!")) self.resize(413, 323) self.verticalLayout = QVBoxLayout(self) self.label = QLabel(self) - msg = tr("There were problems processing some (or all) of the files. The cause of " + msg = tr( + "There were problems processing some (or all) of the files. The cause of " "these problems are described in the table below. Those files were not " - "removed from your results.") + "removed from your results." + ) self.label.setText(msg) self.label.setWordWrap(True) self.verticalLayout.addWidget(self.label) @@ -58,7 +62,7 @@ class ProblemDialog(QDialog): self.closeButton.setDefault(True) self.horizontalLayout.addWidget(self.closeButton) self.verticalLayout.addLayout(self.horizontalLayout) - + if __name__ == '__main__': import sys @@ -67,4 +71,4 @@ if __name__ == '__main__': dgapp = TestApp() dialog = ProblemDialog(None, dgapp) dialog.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/qt/base/result_window.py b/qt/base/result_window.py index 9ac5b611..fc7b1ac4 100644 --- a/qt/base/result_window.py +++ b/qt/base/result_window.py @@ -1,14 +1,16 @@ # Created By: Virgil Dupras # Created On: 2009-04-25 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from PyQt5.QtCore import Qt, QRect -from PyQt5.QtWidgets import (QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget, - QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox) +from PyQt5.QtWidgets import ( + QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget, + QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox +) from hscommon.trans import trget from qtlib.util import moveToScreenCenter, horizontalWrap, createActions @@ -28,7 +30,7 @@ class ResultWindow(QMainWindow): self.resultsModel = app.RESULT_MODEL_CLASS(self.app, self.resultsView) self.stats = StatsLabel(app.model.stats_label, self.statusLabel) self._update_column_actions_status() - + self.menuColumns.triggered.connect(self.columnToggled) self.resultsView.doubleClicked.connect(self.resultsDoubleClicked) self.resultsView.spacePressed.connect(self.resultsSpacePressed) @@ -37,7 +39,7 @@ class ResultWindow(QMainWindow): self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered) self.searchEdit.searchChanged.connect(self.searchChanged) self.app.willSavePrefs.connect(self.appWillSavePrefs) - + def _setupActions(self): # (name, shortcut, icon, desc, func) ACTIONS = [ @@ -50,11 +52,23 @@ class ResultWindow(QMainWindow): ('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered), ('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered), ('actionReprioritize', '', '', tr("Re-Prioritize Results..."), self.reprioritizeTriggered), - ('actionRemoveSelected', 'Ctrl+Del', '', tr("Remove Selected from Results"), self.removeSelectedTriggered), - ('actionIgnoreSelected', 'Ctrl+Shift+Del', '', tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered), - ('actionMakeSelectedReference', 'Ctrl+Space', '', tr("Make Selected into Reference"), self.app.model.make_selected_reference), + ( + 'actionRemoveSelected', 'Ctrl+Del', '', + tr("Remove Selected from Results"), self.removeSelectedTriggered + ), + ( + 'actionIgnoreSelected', 'Ctrl+Shift+Del', '', + tr("Add Selected to Ignore List"), self.addToIgnoreListTriggered + ), + ( + 'actionMakeSelectedReference', 'Ctrl+Space', '', + tr("Make Selected into Reference"), self.app.model.make_selected_reference + ), ('actionOpenSelected', 'Ctrl+O', '', tr("Open Selected with Default Application"), self.openTriggered), - ('actionRevealSelected', 'Ctrl+Shift+O', '', tr("Open Containing Folder of Selected"), self.revealTriggered), + ( + 'actionRevealSelected', 'Ctrl+Shift+O', '', + tr("Open Containing Folder of Selected"), self.revealTriggered + ), ('actionRenameSelected', 'F2', '', tr("Rename Selected"), self.renameTriggered), ('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered), ('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered), @@ -68,7 +82,7 @@ class ResultWindow(QMainWindow): createActions(ACTIONS, self) self.actionDelta.setCheckable(True) self.actionPowerMarker.setCheckable(True) - + def _setupMenu(self): self.menubar = QMenuBar() self.menubar.setGeometry(QRect(0, 0, 630, 22)) @@ -85,7 +99,7 @@ class ResultWindow(QMainWindow): self.menuHelp = QMenu(self.menubar) self.menuHelp.setTitle(tr("Help")) self.setMenuBar(self.menubar) - + self.menuActions.addAction(self.actionDeleteMarked) self.menuActions.addAction(self.actionMoveMarked) self.menuActions.addAction(self.actionCopyMarked) @@ -118,14 +132,14 @@ class ResultWindow(QMainWindow): self.menuFile.addAction(self.actionExportToCSV) self.menuFile.addSeparator() self.menuFile.addAction(self.app.actionQuit) - + self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuMark.menuAction()) self.menubar.addAction(self.menuActions.menuAction()) self.menubar.addAction(self.menuColumns.menuAction()) self.menubar.addAction(self.menuView.menuAction()) self.menubar.addAction(self.menuHelp.menuAction()) - + # Columns menu menu = self.menuColumns self._column_actions = [] @@ -138,7 +152,7 @@ class ResultWindow(QMainWindow): menu.addSeparator() action = menu.addAction(tr("Reset to Defaults")) action.item_index = -1 - + # Action menu actionMenu = QMenu(tr("Actions"), self.menubar) actionMenu.addAction(self.actionDeleteMarked) @@ -156,7 +170,7 @@ class ResultWindow(QMainWindow): actionMenu.addAction(self.actionRenameSelected) self.actionActions.setMenu(actionMenu) self.actionsButton.setMenu(self.actionActions.menu()) - + def _setupUi(self): self.setWindowTitle(tr("{} Results").format(self.app.NAME)) self.resize(630, 514) @@ -170,8 +184,10 @@ class ResultWindow(QMainWindow): self.deltaValuesCheckBox = QCheckBox(tr("Delta Values")) self.searchEdit = SearchEdit() self.searchEdit.setMaximumWidth(300) - self.horizontalLayout = horizontalWrap([self.actionsButton, self.detailsButton, - self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8]) + self.horizontalLayout = horizontalWrap([ + self.actionsButton, self.detailsButton, + self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8 + ]) self.horizontalLayout.setSpacing(8) self.verticalLayout.addLayout(self.horizontalLayout) self.resultsView = ResultsView(self.centralwidget) @@ -193,7 +209,7 @@ class ResultWindow(QMainWindow): self.setStatusBar(self.statusbar) self.statusLabel = QLabel(self) self.statusbar.addPermanentWidget(self.statusLabel, 1) - + if self.app.prefs.resultWindowIsMaximized: self.setWindowState(self.windowState() | Qt.WindowMaximized) else: @@ -201,85 +217,85 @@ class ResultWindow(QMainWindow): self.setGeometry(self.app.prefs.resultWindowRect) else: moveToScreenCenter(self) - + #--- Private def _update_column_actions_status(self): # Update menu checked state menu_items = self.app.model.result_table.columns.menu_items() for action, (display, visible) in zip(self._column_actions, menu_items): action.setChecked(visible) - + #--- Actions def actionsTriggered(self): self.actionsButton.showMenu() - + def addToIgnoreListTriggered(self): self.app.model.add_selected_to_ignore_list() - + def copyTriggered(self): self.app.model.copy_or_move_marked(True) - + def deleteTriggered(self): self.app.model.delete_marked() - + def deltaTriggered(self, state=None): # The sender can be either the action or the checkbox, but both have a isChecked() method. self.resultsModel.delta_values = self.sender().isChecked() self.actionDelta.setChecked(self.resultsModel.delta_values) self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values) - + def detailsTriggered(self): self.app.show_details() - + def markAllTriggered(self): self.app.model.mark_all() - + def markInvertTriggered(self): self.app.model.mark_invert() - + def markNoneTriggered(self): self.app.model.mark_none() - + def markSelectedTriggered(self): self.app.model.toggle_selected_mark_state() - + def moveTriggered(self): self.app.model.copy_or_move_marked(False) - + def openTriggered(self): self.app.model.open_selected() - + def powerMarkerTriggered(self, state=None): # see deltaTriggered self.resultsModel.power_marker = self.sender().isChecked() self.actionPowerMarker.setChecked(self.resultsModel.power_marker) self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker) - + def preferencesTriggered(self): self.app.show_preferences() - + def removeMarkedTriggered(self): self.app.model.remove_marked() - + def removeSelectedTriggered(self): self.app.model.remove_selected() - + def renameTriggered(self): index = self.resultsView.selectionModel().currentIndex() # Our index is the current row, with column set to 0. Our filename column is 1 and that's # what we want. index = index.sibling(index.row(), 1) self.resultsView.edit(index) - + def reprioritizeTriggered(self): dlg = PrioritizeDialog(self, self.app) result = dlg.exec() if result == QDialog.Accepted: dlg.model.perform_reprioritization() - + def revealTriggered(self): self.app.model.reveal_selected() - + def saveResultsTriggered(self): title = tr("Select a file to save your results to") files = tr("dupeGuru Results (*.dupeguru)") @@ -289,13 +305,13 @@ class ResultWindow(QMainWindow): destination = '{}.dupeguru'.format(destination) self.app.model.save_as(destination) self.app.recentResults.insertItem(destination) - + #--- Events def appWillSavePrefs(self): prefs = self.app.prefs prefs.resultWindowIsMaximized = self.isMaximized() prefs.resultWindowRect = self.geometry() - + def columnToggled(self, action): index = action.item_index if index == -1: @@ -304,16 +320,16 @@ class ResultWindow(QMainWindow): else: visible = self.app.model.result_table.columns.toggle_menu_item(index) action.setChecked(visible) - + def contextMenuEvent(self, event): self.actionActions.menu().exec_(event.globalPos()) - + def resultsDoubleClicked(self, modelIndex): self.app.model.open_selected() - + def resultsSpacePressed(self): self.app.model.toggle_selected_mark_state() - + def searchChanged(self): self.app.model.apply_filter(self.searchEdit.text()) - + diff --git a/qt/me/preferences_dialog.py b/qt/me/preferences_dialog.py index 5d93f7f8..804701bb 100644 --- a/qt/me/preferences_dialog.py +++ b/qt/me/preferences_dialog.py @@ -1,15 +1,17 @@ # Created By: Virgil Dupras # Created On: 2009-04-29 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import sys from PyQt5.QtCore import QSize -from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, - QApplication) +from PyQt5.QtWidgets import ( + QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, + QApplication +) from hscommon.trans import trget from core.scanner import ScanType @@ -31,9 +33,9 @@ SCAN_TYPE_ORDER = [ class PreferencesDialog(PreferencesDialogBase): def __init__(self, parent, app): PreferencesDialogBase.__init__(self, parent, app) - + self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged) - + def _setupPreferenceWidgets(self): scanTypeLabels = [ tr("Filename"), @@ -87,7 +89,7 @@ class PreferencesDialog(PreferencesDialogBase): self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)")) self.widgetsVLayout.addWidget(self.debugModeBox) self._setupBottomPart() - + def _load(self, prefs, setchecked): scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) @@ -99,7 +101,7 @@ class PreferencesDialog(PreferencesDialogBase): setchecked(self.tagYearBox, prefs.scan_tag_year) setchecked(self.matchSimilarBox, prefs.match_similar) setchecked(self.wordWeightingBox, prefs.word_weighting) - + def _save(self, prefs, ischecked): prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] prefs.scan_tag_track = ischecked(self.tagTrackBox) @@ -110,15 +112,17 @@ class PreferencesDialog(PreferencesDialogBase): prefs.scan_tag_year = ischecked(self.tagYearBox) prefs.match_similar = ischecked(self.matchSimilarBox) prefs.word_weighting = ischecked(self.wordWeightingBox) - + def resetToDefaults(self): self.load(preferences.Preferences()) - + #--- Events def scanTypeChanged(self, index): scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] - word_based = scan_type in (ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder, - ScanType.Tag) + word_based = scan_type in ( + ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder, + ScanType.Tag + ) tag_based = scan_type == ScanType.Tag self.filterHardnessSlider.setEnabled(word_based) self.matchSimilarBox.setEnabled(word_based) @@ -129,7 +133,7 @@ class PreferencesDialog(PreferencesDialogBase): self.tagTitleBox.setEnabled(tag_based) self.tagGenreBox.setEnabled(tag_based) self.tagYearBox.setEnabled(tag_based) - + if __name__ == '__main__': from ..testapp import TestApp @@ -137,4 +141,5 @@ if __name__ == '__main__': dgapp = TestApp() dialog = PreferencesDialog(None, dgapp) dialog.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) + diff --git a/qt/me/results_model.py b/qt/me/results_model.py index b86a5f64..19686b1f 100644 --- a/qt/me/results_model.py +++ b/qt/me/results_model.py @@ -1,8 +1,8 @@ # Created On: 2011-11-27 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from qtlib.column import Column @@ -29,4 +29,5 @@ class ResultsModel(ResultsModelBase): Column('percentage', defaultWidth=60), Column('words', defaultWidth=120), Column('dupe_count', defaultWidth=80), - ] \ No newline at end of file + ] + diff --git a/qt/pe/block.py b/qt/pe/block.py index 2e5f2654..c97d9ced 100644 --- a/qt/pe/block.py +++ b/qt/pe/block.py @@ -1,12 +1,12 @@ # Created By: Virgil Dupras # Created On: 2009-05-10 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license -from ._block_qt import getblocks +from ._block_qt import getblocks # NOQA # Converted to C # def getblock(image): @@ -24,7 +24,7 @@ from ._block_qt import getblocks # return (red // pixel_count, green // pixel_count, blue // pixel_count) # else: # return (0, 0, 0) -# +# # def getblocks(image, block_count_per_side): # width = image.width() # height = image.height() diff --git a/qt/pe/preferences_dialog.py b/qt/pe/preferences_dialog.py index 6a7155aa..9167cc47 100644 --- a/qt/pe/preferences_dialog.py +++ b/qt/pe/preferences_dialog.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2009-04-29 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import sys @@ -25,9 +25,9 @@ SCAN_TYPE_ORDER = [ class PreferencesDialog(PreferencesDialogBase): def __init__(self, parent, app): PreferencesDialogBase.__init__(self, parent, app) - + self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged) - + def _setupPreferenceWidgets(self): scanTypeLabels = [ tr("Contents"), @@ -49,25 +49,25 @@ class PreferencesDialog(PreferencesDialogBase): self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)")) self.widgetsVLayout.addWidget(self.debugModeBox) self._setupBottomPart() - + def _load(self, prefs, setchecked): scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) setchecked(self.matchScaledBox, prefs.match_scaled) - + def _save(self, prefs, ischecked): prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] prefs.match_scaled = ischecked(self.matchScaledBox) - + def resetToDefaults(self): self.load(preferences.Preferences()) - + #--- Events def scanTypeChanged(self, index): scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] fuzzy_scan = scan_type == ScanType.FuzzyBlock self.filterHardnessSlider.setEnabled(fuzzy_scan) - + if __name__ == '__main__': from ..testapp import TestApp @@ -75,4 +75,5 @@ if __name__ == '__main__': dgapp = TestApp() dialog = PreferencesDialog(None, dgapp) dialog.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) + diff --git a/qt/pe/results_model.py b/qt/pe/results_model.py index 6e85980e..3b8ead40 100644 --- a/qt/pe/results_model.py +++ b/qt/pe/results_model.py @@ -1,8 +1,8 @@ # Created On: 2011-11-27 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from qtlib.column import Column @@ -20,4 +20,5 @@ class ResultsModel(ResultsModelBase): Column('mtime', defaultWidth=120), Column('percentage', defaultWidth=60), Column('dupe_count', defaultWidth=80), - ] \ No newline at end of file + ] + diff --git a/qt/se/app.py b/qt/se/app.py index 1e7ce398..f620c3c6 100644 --- a/qt/se/app.py +++ b/qt/se/app.py @@ -1,9 +1,9 @@ # Created By: Virgil Dupras # Created On: 2009-05-24 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from core_se import __appname__ @@ -18,6 +18,7 @@ from .preferences_dialog import PreferencesDialog class Directories(DirectoriesBase): ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files']) + def _default_state_for_path(self, path): result = DirectoriesBase._default_state_for_path(self, path) if result is not None: @@ -30,16 +31,16 @@ class DupeGuru(DupeGuruBase): EDITION = 'se' LOGO_NAME = 'logo_se' NAME = __appname__ - + DETAILS_DIALOG_CLASS = DetailsDialog RESULT_MODEL_CLASS = ResultsModel PREFERENCES_CLASS = Preferences PREFERENCES_DIALOG_CLASS = PreferencesDialog - + def _setup(self): self.directories = Directories() DupeGuruBase._setup(self) - + def _update_options(self): DupeGuruBase._update_options(self) self.model.scanner.min_match_percentage = self.prefs.filter_hardness @@ -48,4 +49,4 @@ class DupeGuru(DupeGuruBase): self.model.scanner.match_similar_words = self.prefs.match_similar threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0 self.model.scanner.size_threshold = threshold * 1024 # threshold is in KB. the scanner wants bytes - + diff --git a/qt/se/preferences_dialog.py b/qt/se/preferences_dialog.py index 13722199..701f2a80 100644 --- a/qt/se/preferences_dialog.py +++ b/qt/se/preferences_dialog.py @@ -1,15 +1,17 @@ # Created By: Virgil Dupras # Created On: 2009-05-24 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license import sys from PyQt5.QtCore import QSize -from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, - QLineEdit, QApplication) +from PyQt5.QtWidgets import ( + QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, + QLineEdit, QApplication +) from hscommon.plat import ISWINDOWS, ISLINUX from hscommon.trans import trget @@ -31,9 +33,9 @@ SCAN_TYPE_ORDER = [ class PreferencesDialog(PreferencesDialogBase): def __init__(self, parent, app, **kwargs): super().__init__(parent, app, **kwargs) - + self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged) - + def _setupPreferenceWidgets(self): scanTypeLabels = [ tr("Filename"), @@ -73,23 +75,26 @@ class PreferencesDialog(PreferencesDialogBase): spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem1) self.verticalLayout_4.addLayout(self.horizontalLayout_2) - self._setupAddCheckbox('ignoreHardlinkMatches', tr("Ignore duplicates hardlinking to the same file"), self.widget) + self._setupAddCheckbox( + 'ignoreHardlinkMatches', + tr("Ignore duplicates hardlinking to the same file"), self.widget + ) self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches) self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget) self.verticalLayout_4.addWidget(self.debugModeBox) self.widgetsVLayout.addWidget(self.widget) self._setupBottomPart() - + def _setupUi(self): PreferencesDialogBase._setupUi(self) - + if ISLINUX: # Under linux, whether it's a Qt layout bug or something else, the size threshold text edit # doesn't have enough space, so we make the pref pane higher to compensate. self.resize(self.width(), 530) elif ISWINDOWS: self.resize(self.width(), 440) - + def _load(self, prefs, setchecked): scan_type_index = SCAN_TYPE_ORDER.index(prefs.scan_type) self.scanTypeComboBox.setCurrentIndex(scan_type_index) @@ -97,17 +102,17 @@ class PreferencesDialog(PreferencesDialogBase): setchecked(self.wordWeightingBox, prefs.word_weighting) setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files) self.sizeThresholdEdit.setText(str(prefs.small_file_threshold)) - + def _save(self, prefs, ischecked): prefs.scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] prefs.match_similar = ischecked(self.matchSimilarBox) prefs.word_weighting = ischecked(self.wordWeightingBox) prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox) prefs.small_file_threshold = tryint(self.sizeThresholdEdit.text()) - + def resetToDefaults(self): self.load(preferences.Preferences()) - + #--- Events def scanTypeChanged(self, index): scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] @@ -115,7 +120,7 @@ class PreferencesDialog(PreferencesDialogBase): self.filterHardnessSlider.setEnabled(word_based) self.matchSimilarBox.setEnabled(word_based) self.wordWeightingBox.setEnabled(word_based) - + if __name__ == '__main__': from ..testapp import TestApp diff --git a/qt/se/results_model.py b/qt/se/results_model.py index 978ef582..07949faf 100644 --- a/qt/se/results_model.py +++ b/qt/se/results_model.py @@ -1,8 +1,8 @@ # Created On: 2011-11-27 # Copyright 2014 Hardcoded Software (http://www.hardcoded.net) -# -# This software is licensed under the "BSD" License as described in the "LICENSE" file, -# which should be included with this package. The terms are also available at +# +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at # http://www.hardcoded.net/licenses/bsd_license from qtlib.column import Column @@ -19,4 +19,5 @@ class ResultsModel(ResultsModelBase): Column('percentage', defaultWidth=60), Column('words', defaultWidth=120), Column('dupe_count', defaultWidth=80), - ] \ No newline at end of file + ] + diff --git a/requirements-extra.txt b/requirements-extra.txt index b6e0e645..b6027f25 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -1,2 +1,4 @@ pytest>=2.0.0 pytest-monkeyplus>=1.0.0 +flake8 + diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..352b24c1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py33,py34 +skipsdist = True + +[testenv] +commands = + flake8 + py.test core core_se core_me core_pe hscommon +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-extra.txt + +[flake8] +exclude = .tox,env,build,hscommon,qtlib,cocoalib,cocoa,help,./get-pip.py,./qt/dg_rc.py,./core*/tests,qt/run_template.py,cocoa/run_template.py,./run.py,./pkg +max-line-length = 120 +ignore = W391,W293,E302,E261,E226,E227,W291,E262,E303,E265 +