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.
This commit is contained in:
Virgil Dupras 2014-10-13 15:08:59 -04:00
parent 24643a9b5d
commit 2166a0996c
46 changed files with 794 additions and 612 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
*.waf* *.waf*
.lock-waf* .lock-waf*
.idea .idea
.tox
build build
dist dist

115
build.py
View File

@ -18,10 +18,12 @@ import compileall
from setuptools import setup, Extension from setuptools import setup, Extension
from hscommon import sphinxgen 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, get_module_version, move_all, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib, 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 import loc
from hscommon.plat import ISOSX, ISLINUX from hscommon.plat import ISOSX, ISLINUX
from hscommon.util import ensure_folder, delete_files_with_pattern 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(): def parse_args():
usage = "usage: %prog [options]" usage = "usage: %prog [options]"
parser = OptionParser(usage=usage) parser = OptionParser(usage=usage)
parser.add_option('--clean', action='store_true', dest='clean', parser.add_option(
help="Clean build folder before building") '--clean', action='store_true', dest='clean',
parser.add_option('--doc', action='store_true', dest='doc', help="Clean build folder before building"
help="Build only the help file") )
parser.add_option('--loc', action='store_true', dest='loc', parser.add_option(
help="Build only localization") '--doc', action='store_true', dest='doc',
parser.add_option('--cocoa-ext', action='store_true', dest='cocoa_ext', help="Build only the help file"
help="Build only Cocoa extensions") )
parser.add_option('--cocoa-compile', action='store_true', dest='cocoa_compile', parser.add_option(
help="Build only Cocoa executable") '--loc', action='store_true', dest='loc',
parser.add_option('--xibless', action='store_true', dest='xibless', help="Build only localization"
help="Build only xibless UIs") )
parser.add_option('--updatepot', action='store_true', dest='updatepot', parser.add_option(
help="Generate .pot files from source code.") '--cocoa-ext', action='store_true', dest='cocoa_ext',
parser.add_option('--mergepot', action='store_true', dest='mergepot', help="Build only Cocoa extensions"
help="Update all .po files based on .pot files.") )
parser.add_option('--normpo', action='store_true', dest='normpo', parser.add_option(
help="Normalize all PO files (do this before commit).") '--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() (options, args) = parser.parse_args()
return options return options
@ -75,12 +95,20 @@ def build_xibless(edition, dest='cocoa/autogen'):
('preferences_panel.py', 'PreferencesPanel_UI'), ('preferences_panel.py', 'PreferencesPanel_UI'),
] ]
for srcname, dstname in FNPAIRS: for srcname, dstname in FNPAIRS:
xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname), xibless.generate(
localizationTable='Localizable', args={'edition': edition}) op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
localizationTable='Localizable', args={'edition': edition}
)
if edition == 'pe': 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: 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): def build_cocoa(edition, dev):
print("Creating OS X app structure") print("Creating OS X app structure")
@ -225,8 +253,10 @@ def build_updatepot():
os.remove(cocoalib_pot) os.remove(cocoalib_pot)
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot) loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
print("Enhancing ui.pot with Cocoa's strings files") print("Enhancing ui.pot with Cocoa's strings files")
loc.strings2pot(op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'), loc.strings2pot(
op.join('locale', 'ui.pot')) op.join('cocoa', 'base', 'en.lproj', 'Localizable.strings'),
op.join('locale', 'ui.pot')
)
def build_mergepot(): def build_mergepot():
print("Updating .po files using .pot files") print("Updating .po files using .pot files")
@ -243,11 +273,15 @@ def build_cocoa_proxy_module():
print("Building Cocoa Proxy") print("Building Cocoa Proxy")
import objp.p2o import objp.p2o
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m') objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
build_cocoa_ext("CocoaProxy", 'cocoalib/cocoa', build_cocoa_ext(
['cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m', "CocoaProxy", 'cocoalib/cocoa',
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'], [
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
],
['AppKit', 'CoreServices'], ['AppKit', 'CoreServices'],
['cocoalib', 'cocoa/autogen']) ['cocoalib', 'cocoa/autogen']
)
def build_cocoa_bridging_interfaces(edition): def build_cocoa_bridging_interfaces(edition):
print("Building Cocoa Bridging Interfaces") print("Building Cocoa Bridging Interfaces")
@ -255,9 +289,11 @@ def build_cocoa_bridging_interfaces(edition):
import objp.p2o import objp.p2o
add_to_pythonpath('cocoa') add_to_pythonpath('cocoa')
add_to_pythonpath('cocoalib') 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, OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
PyTextField, ProgressWindowView, PyProgressWindow) PyTextField, ProgressWindowView, PyProgressWindow
)
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
from inter.details_panel import PyDetailsPanel, DetailsPanelView from inter.details_panel import PyDetailsPanel, DetailsPanelView
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView 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.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuruBase, DupeGuruView from inter.app import PyDupeGuruBase, DupeGuruView
appmod = importlib.import_module('inter.app_{}'.format(edition)) 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, PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase, PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
PyTextField, PyProgressWindow, appmod.PyDupeGuru] PyTextField, PyProgressWindow, appmod.PyDupeGuru
]
for class_ in allclasses: for class_ in allclasses:
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True) 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, DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView, IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
ProgressWindowView, DupeGuruView] ProgressWindowView, DupeGuruView
]
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses] clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m') objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m']) build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
@ -297,7 +337,8 @@ def build_pe_modules(ui):
extra_link_args=[ extra_link_args=[
"-framework", "CoreFoundation", "-framework", "CoreFoundation",
"-framework", "Foundation", "-framework", "Foundation",
"-framework", "ApplicationServices",] "-framework", "ApplicationServices",
]
)) ))
setup( setup(
script_args=['build_ext', '--inplace'], script_args=['build_ext', '--inplace'],

View File

@ -6,7 +6,6 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
import sys
from optparse import OptionParser from optparse import OptionParser
import json import json
@ -29,11 +28,18 @@ def main(options):
if __name__ == '__main__': if __name__ == '__main__':
usage = "usage: %prog [options]" usage = "usage: %prog [options]"
parser = OptionParser(usage=usage) parser = OptionParser(usage=usage)
parser.add_option('--edition', dest='edition', parser.add_option(
help="dupeGuru edition to build (se, me or pe). Default is se.") '--edition', dest='edition',
parser.add_option('--ui', dest='ui', help="dupeGuru edition to build (se, me or pe). Default is se."
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, parser.add_option(
help="If this flag is set, will configure for dev builds.") '--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() (options, args) = parser.parse_args()
main(options) main(options)

View File

@ -38,8 +38,10 @@ DEBUG_MODE_PREFERENCE = 'DebugMode'
MSG_NO_MARKED_DUPES = tr("There are no marked duplicates. Nothing has been done.") 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_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 " MSG_MANY_FILES_TO_OPEN = tr(
"files are opened with, doing so can create quite a mess. Continue?") "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: class DestType:
Direct = 0 Direct = 0
@ -265,8 +267,10 @@ class DupeGuru(Broadcaster):
return None return None
def _get_export_data(self): def _get_export_data(self):
columns = [col for col in self.result_table.columns.ordered_columns columns = [
if col.visible and col.name != 'marked'] col for col in self.result_table.columns.ordered_columns
if col.visible and col.name != 'marked'
]
colnames = [col.display for col in columns] colnames = [col.display for col in columns]
rows = [] rows = []
for group_id, group in enumerate(self.results.groups): for group_id, group in enumerate(self.results.groups):
@ -278,8 +282,10 @@ class DupeGuru(Broadcaster):
return colnames, rows return colnames, rows
def _results_changed(self): def _results_changed(self):
self.selected_dupes = [d for d in self.selected_dupes self.selected_dupes = [
if self.results.get_group_of_duplicate(d) is not None] d for d in self.selected_dupes
if self.results.get_group_of_duplicate(d) is not None
]
self.notify('results_changed') self.notify('results_changed')
def _start_job(self, jobid, func, args=()): def _start_job(self, jobid, func, args=()):
@ -287,7 +293,10 @@ class DupeGuru(Broadcaster):
try: try:
self.progress_window.run(jobid, title, func, args=args) self.progress_window.run(jobid, title, func, args=args)
except job.JobInProgressError: 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) self.view.show_message(msg)
def _job_completed(self, jobid): def _job_completed(self, jobid):
@ -439,8 +448,10 @@ class DupeGuru(Broadcaster):
return return
if not self.deletion_options.show(self.results.mark_count): if not self.deletion_options.show(self.results.mark_count):
return return
args = [self.deletion_options.link_deleted, self.deletion_options.use_hardlinks, args = [
self.deletion_options.direct] self.deletion_options.link_deleted, self.deletion_options.use_hardlinks,
self.deletion_options.direct
]
logging.debug("Starting deletion job with args %r", args) logging.debug("Starting deletion job with args %r", args)
self._start_job(JobType.Delete, self._do_delete, args=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 no group was changed, however, we don't touch the selection.
if not self.result_table.power_marker: if not self.result_table.power_marker:
if changed_groups: if changed_groups:
self.selected_dupes = [d for d in self.selected_dupes self.selected_dupes = [
if self.results.get_group_of_duplicate(d).ref is d] d for d in self.selected_dupes
if self.results.get_group_of_duplicate(d).ref is d
]
self.notify('results_changed') self.notify('results_changed')
else: else:
# If we're in "Dupes Only" mode (previously called Power Marker), things are a bit # If we're in "Dupes Only" mode (previously called Power Marker), things are a bit

View File

@ -95,7 +95,8 @@ class Directories:
file.is_ref = state == DirectoryState.Reference file.is_ref = state == DirectoryState.Reference
filepaths.add(file.path) filepaths.add(file.path)
yield file 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] 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 subfolder in subfolders:
for file in self._get_files(subfolder, j): for file in self._get_files(subfolder, j):

View File

@ -17,9 +17,11 @@ from hscommon.util import flatten, multi_replace
from hscommon.trans import tr from hscommon.trans import tr
from hscommon.jobprogress import job from hscommon.jobprogress import job
(WEIGHT_WORDS, (
WEIGHT_WORDS,
MATCH_SIMILAR_WORDS, MATCH_SIMILAR_WORDS,
NO_FIELD_ORDER) = range(3) NO_FIELD_ORDER,
) = range(3)
JOB_REFRESH_RATE = 100 JOB_REFRESH_RATE = 100
@ -259,6 +261,7 @@ def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob)
filesize = getattr(file, sizeattr) filesize = getattr(file, sizeattr)
if filesize: if filesize:
size2files[filesize].add(file) size2files[filesize].add(file)
del files
possible_matches = [files for files in size2files.values() if len(files) > 1] possible_matches = [files for files in size2files.values() if len(files) > 1]
del size2files del size2files
result = [] result = []
@ -495,7 +498,10 @@ def get_groups(matches, j=job.nulljob):
matched_files = set(flatten(groups)) matched_files = set(flatten(groups))
orphan_matches = [] orphan_matches = []
for group in groups: 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: if groups and orphan_matches:
groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time groups += get_groups(orphan_matches) # no job, as it isn't supposed to take a long time
return groups return groups

View File

@ -32,6 +32,7 @@ NOT_SET = object()
class FSError(Exception): class FSError(Exception):
cls_message = "An error has occured on '{name}' in '{parent}'" cls_message = "An error has occured on '{name}' in '{parent}'"
def __init__(self, fsobject, parent=None): def __init__(self, fsobject, parent=None):
message = self.cls_message message = self.cls_message
if isinstance(fsobject, str): if isinstance(fsobject, str):

View File

@ -13,3 +13,4 @@ blue, which is supposed to be orange, does the sorting logic, holds selection, e
.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software .. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software
""" """

View File

@ -7,7 +7,6 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from hscommon.notify import Listener from hscommon.notify import Listener
from hscommon.gui.base import NoopGUI
class DupeGuruGUIObject(Listener): class DupeGuruGUIObject(Listener):
def __init__(self, app): def __init__(self, app):

View File

@ -71,6 +71,7 @@ class PrioritizeDialog(GUIObject):
return return
crit = self.criteria[self.criteria_list.selected_index] crit = self.criteria[self.criteria_list.selected_index]
self.prioritizations.append(crit) self.prioritizations.append(crit)
del crit
self.prioritization_list[:] = [crit.display for crit in self.prioritizations] self.prioritization_list[:] = [crit.display for crit in self.prioritizations]
def remove_selected(self): def remove_selected(self):

View File

@ -96,7 +96,10 @@ class Results(Markable):
self.__dupes = flatten(group.dupes for group in self.groups) self.__dupes = flatten(group.dupes for group in self.groups)
if None in self.__dupes: if None in self.__dupes:
# This is debug logging to try to figure out #44 # 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: if self.__filtered_dupes:
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes] self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
sd = self.__dupes_sort_descriptor sd = self.__dupes_sort_descriptor
@ -249,7 +252,8 @@ class Results(Markable):
second_file = dupes[int(attrs['second'])] second_file = dupes[int(attrs['second'])]
percentage = int(attrs['percentage']) percentage = int(attrs['percentage'])
group.add_match(engine.Match(first_file, second_file, 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 pass
if (not group.matches) and (len(dupes) >= 2): if (not group.matches) and (len(dupes) >= 2):
do_match(dupes[0], dupes[1:], group) do_match(dupes[0], dupes[1:], group)
@ -411,3 +415,4 @@ class Results(Markable):
dupes = property(__get_dupe_list) dupes = property(__get_dupe_list)
groups = property(__get_groups, __set_groups) groups = property(__get_groups, __set_groups)
stat_line = property(__get_stat_line) stat_line = property(__get_stat_line)

View File

@ -81,7 +81,9 @@ class Scanner:
files = [f for f in files if f.size >= self.size_threshold] files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}: if self.scan_type in {ScanType.Contents, ScanType.ContentsAudio, ScanType.Folders}:
sizeattr = 'audiosize' if self.scan_type == ScanType.ContentsAudio else 'size' 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: else:
j = j.start_subjob([2, 8]) j = j.start_subjob([2, 8])
kw = {} kw = {}
@ -94,7 +96,11 @@ class Scanner:
func = { func = {
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)), ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
ScanType.Fields: lambda f: engine.getfields(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] }[self.scan_type]
for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")): for f in j.iter_with_progress(files, tr("Read metadata of %d/%d files")):
logging.debug("Reading metadata of {}".format(str(f.path))) logging.debug("Reading metadata of {}".format(str(f.path)))
@ -152,8 +158,10 @@ class Scanner:
if self.ignore_list: if self.ignore_list:
j = j.start_subjob(2) j = j.start_subjob(2)
iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list")) iter_matches = j.iter_with_progress(matches, tr("Processed %d/%d matches against the ignore list"))
matches = [m for m in iter_matches matches = [
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))] m for m in iter_matches
if not self.ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
]
logging.info('Grouping matches') logging.info('Grouping matches')
groups = engine.get_groups(matches, j) groups = engine.get_groups(matches, j)
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches]) matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
@ -185,3 +193,4 @@ class Scanner:
scanned_tags = {'artist', 'title'} scanned_tags = {'artist', 'title'}
size_threshold = 0 size_threshold = 0
word_weighting = False word_weighting = False

View File

@ -1,2 +1,3 @@
__version__ = '6.8.0' __version__ = '6.8.0'
__appname__ = 'dupeGuru Music Edition' __appname__ = 'dupeGuru Music Edition'

View File

@ -13,8 +13,10 @@ from .result_table import ResultTable
class DupeGuru(DupeGuruBase): class DupeGuru(DupeGuruBase):
NAME = __appname__ NAME = __appname__
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist', METADATA_TO_READ = [
'album', 'genre', 'year', 'track', 'comment'] 'size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
]
def __init__(self, view): def __init__(self, view):
DupeGuruBase.__init__(self, view) DupeGuruBase.__init__(self, view)

View File

@ -12,8 +12,10 @@ 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.app import format_timestamp, format_perc, format_words, format_dupe_count
from core import fs from core import fs
TAG_FIELDS = {'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist', TAG_FIELDS = {
'album', 'genre', 'year', 'track', 'comment'} 'audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment'
}
class MusicFile(fs.File): class MusicFile(fs.File):
INITIAL_INFO = fs.File.INITIAL_INFO.copy() INITIAL_INFO = fs.File.INITIAL_INFO.copy()

View File

@ -7,8 +7,10 @@
from hscommon.trans import trget from hscommon.trans import trget
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory, from core.prioritize import (
SizeCategory, MtimeCategory) KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
SizeCategory, MtimeCategory
)
coltr = trget('columns') coltr = trget('columns')
@ -31,5 +33,8 @@ class SamplerateCategory(NumericalCategory):
return dupe.samplerate return dupe.samplerate
def all_categories(): def all_categories():
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory, return [
BitrateCategory, SamplerateCategory, MtimeCategory] KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
BitrateCategory, SamplerateCategory, MtimeCategory
]

View File

@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # 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 # Converted to C
# def getblock(image): # def getblock(image):

View File

@ -93,9 +93,9 @@ class Cache:
def _create_con(self, second_try=False): def _create_con(self, second_try=False):
def create_tables(): def create_tables():
logging.debug("Creating picture cache tables.") logging.debug("Creating picture cache tables.")
self.con.execute("drop table if exists pictures"); self.con.execute("drop table if exists pictures")
self.con.execute("drop index if exists idx_path"); 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 table pictures(path TEXT, mtime INTEGER, blocks TEXT)")
self.con.execute("create index idx_path on pictures (path)") self.con.execute("create index idx_path on pictures (path)")
self.con = sqlite.connect(self.dbname, isolation_level=None) self.con = sqlite.connect(self.dbname, isolation_level=None)

View File

@ -93,8 +93,10 @@ def get_chunks(pictures):
chunk_count = max(min_chunk_count, chunk_count) chunk_count = max(min_chunk_count, chunk_count)
chunk_size = (len(pictures) // chunk_count) + 1 chunk_size = (len(pictures) // chunk_count) + 1
chunk_size = max(MIN_CHUNK_SIZE, chunk_size) chunk_size = max(MIN_CHUNK_SIZE, chunk_size)
logging.info("Creating %d chunks with a chunk size of %d for %d pictures", chunk_count, logging.info(
chunk_size, len(pictures)) "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)] chunks = [pictures[i:i+chunk_size] for i in range(0, len(pictures), chunk_size)]
return chunks 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): def collect_results(collect_all=False):
# collect results and wait until the queue is small enough to accomodate a new results. # 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 limit = 0 if collect_all else RESULTS_QUEUE_LIMIT
while len(async_results) > limit: while len(async_results) > limit:
ready, working = extract(lambda r: r.ready(), async_results) 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() matches += result.get()
async_results.remove(result) async_results.remove(result)
comparison_count += 1 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.set_progress(comparison_count, progress_msg)
j = j.start_subjob([3, 7]) j = j.start_subjob([3, 7])

View File

@ -28,3 +28,4 @@ def getmatches(files, match_scaled, j):
continue continue
matches.append(Match(p1, p2, 100)) matches.append(Match(p1, p2, 100))
return matches return matches

View File

@ -7,8 +7,10 @@
from hscommon.trans import trget from hscommon.trans import trget
from core.prioritize import (KindCategory, FolderCategory, FilenameCategory, NumericalCategory, from core.prioritize import (
SizeCategory, MtimeCategory) KindCategory, FolderCategory, FilenameCategory, NumericalCategory,
SizeCategory, MtimeCategory
)
coltr = trget('columns') coltr = trget('columns')
@ -23,5 +25,7 @@ class DimensionsCategory(NumericalCategory):
return (-width, -height) return (-width, -height)
def all_categories(): def all_categories():
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory, return [
MtimeCategory] KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
MtimeCategory
]

View File

@ -16,9 +16,11 @@ import platform
import glob import glob
from hscommon.plat import ISWINDOWS, ISLINUX 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, 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(): def parse_args():
parser = ArgumentParser() parser = ArgumentParser()
@ -52,7 +54,8 @@ def package_windows(edition, dev):
plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats'] plugin_names = ['accessible', 'codecs', 'iconengines', 'imageformats']
copy_qt_plugins(plugin_names, plugin_dest) 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 = { options = {
'build_exe': { 'build_exe': {
'includes': 'atexit', 'includes': 'atexit',
@ -151,8 +154,10 @@ def package_debian_distribution(edition, distribution):
changelog_dest = op.join(debdest, 'changelog') changelog_dest = op.join(debdest, 'changelog')
project_name = debopts['pkgname'] project_name = debopts['pkgname']
from_version = {'se': '2.9.2', 'me': '5.7.2', 'pe': '1.8.5'}[edition] 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, build_debian_changelog(
distribution=distribution) changelogpath, changelog_dest, project_name, from_version=from_version,
distribution=distribution
)
shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath) shutil.copy(op.join('images', ed('dg{0}_logo_128.png')), srcpath)
os.chdir(destpath) os.chdir(destpath)
cmd = "dpkg-buildpackage -S" cmd = "dpkg-buildpackage -S"

View File

@ -14,7 +14,6 @@ from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox from PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox
from hscommon.trans import trget from hscommon.trans import trget
from hscommon.plat import ISLINUX
from hscommon import desktop from hscommon import desktop
from qtlib.about_box import AboutBox from qtlib.about_box import AboutBox

View File

@ -1,5 +1,6 @@
# cxfreeze has some problems detecting all dependencies. # cxfreeze has some problems detecting all dependencies.
# This modules explicitly import those problematic modules. # This modules explicitly import those problematic modules.
# flake8: noqa
import xml.etree.ElementPath import xml.etree.ElementPath
import gzip import gzip

View File

@ -34,8 +34,10 @@ class DeletionOptions(QDialog):
self.verticalLayout.addWidget(self.msgLabel) self.verticalLayout.addWidget(self.msgLabel)
self.linkCheckbox = QCheckBox(tr("Link deleted files")) self.linkCheckbox = QCheckBox(tr("Link deleted files"))
self.verticalLayout.addWidget(self.linkCheckbox) self.verticalLayout.addWidget(self.linkCheckbox)
text = tr("After having deleted a duplicate, place a link targeting the reference file " text = tr(
"to replace the deleted file.") "After having deleted a duplicate, place a link targeting the reference file "
"to replace the deleted file."
)
self.linkMessageLabel = QLabel(text) self.linkMessageLabel = QLabel(text)
self.linkMessageLabel.setWordWrap(True) self.linkMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.linkMessageLabel) self.verticalLayout.addWidget(self.linkMessageLabel)
@ -46,8 +48,10 @@ class DeletionOptions(QDialog):
self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)")) self.linkCheckbox.setText(self.linkCheckbox.text() + tr(" (unsupported)"))
self.directCheckbox = QCheckBox(tr("Directly delete files")) self.directCheckbox = QCheckBox(tr("Directly delete files"))
self.verticalLayout.addWidget(self.directCheckbox) self.verticalLayout.addWidget(self.directCheckbox)
text = tr("Instead of sending files to trash, delete them directly. This option is usually " text = tr(
"used as a workaround when the normal deletion method doesn't work.") "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 = QLabel(text)
self.directMessageLabel.setWordWrap(True) self.directMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.directMessageLabel) self.verticalLayout.addWidget(self.directMessageLabel)

View File

@ -7,9 +7,11 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from PyQt5.QtCore import QRect 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, QAbstractItemView, QSpacerItem, QSizePolicy, QPushButton, QMainWindow, QMenuBar, QMenu, QLabel,
QApplication) QApplication
)
from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtGui import QPixmap, QIcon
from hscommon.trans import trget from hscommon.trans import trget
@ -225,7 +227,7 @@ class DirectoriesDialog(QMainWindow):
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
from . import dg_rc from . import dg_rc # NOQA
from ..testapp import TestApp from ..testapp import TestApp
app = QApplication([]) app = QApplication([])
dgapp = TestApp() dgapp = TestApp()

View File

@ -9,8 +9,10 @@
import urllib.parse import urllib.parse
from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection from PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection
from PyQt5.QtWidgets import (QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox, from PyQt5.QtWidgets import (
QStyleOptionViewItem, QApplication) QComboBox, QStyledItemDelegate, QStyle, QStyleOptionComboBox,
QStyleOptionViewItem, QApplication
)
from PyQt5.QtGui import QBrush from PyQt5.QtGui import QBrush
from hscommon.trans import trget from hscommon.trans import trget
@ -23,7 +25,7 @@ STATES = [tr("Normal"), tr("Reference"), tr("Excluded")]
class DirectoriesDelegate(QStyledItemDelegate): class DirectoriesDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
editor = QComboBox(parent); editor = QComboBox(parent)
editor.addItems(STATES) editor.addItems(STATES)
return editor return editor
@ -47,7 +49,7 @@ class DirectoriesDelegate(QStyledItemDelegate):
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
value = index.model().data(index, Qt.EditRole) value = index.model().data(index, Qt.EditRole)
editor.setCurrentIndex(value); editor.setCurrentIndex(value)
editor.showPopup() editor.showPopup()
def setModelData(self, editor, model, index): def setModelData(self, editor, model, index):

View File

@ -45,8 +45,12 @@ class IgnoreListDialog(QDialog):
self.removeSelectedButton = QPushButton(tr("Remove Selected")) self.removeSelectedButton = QPushButton(tr("Remove Selected"))
self.clearButton = QPushButton(tr("Clear")) self.clearButton = QPushButton(tr("Clear"))
self.closeButton = QPushButton(tr("Close")) self.closeButton = QPushButton(tr("Close"))
self.verticalLayout.addLayout(horizontalWrap([self.removeSelectedButton, self.clearButton, self.verticalLayout.addLayout(
None, self.closeButton])) horizontalWrap([
self.removeSelectedButton, self.clearButton,
None, self.closeButton
])
)
#--- model --> view #--- model --> view
def show(self): def show(self):

View File

@ -7,8 +7,10 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from PyQt5.QtCore import Qt, QSize from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, from PyQt5.QtWidgets import (
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox) QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
QSlider, QSizePolicy, QSpacerItem, QCheckBox, QLineEdit, QMessageBox, QSpinBox
)
from hscommon.plat import ISOSX, ISLINUX from hscommon.plat import ISOSX, ISLINUX
from hscommon.trans import trget from hscommon.trans import trget
@ -190,3 +192,4 @@ class PreferencesDialogBase(QDialog):
role = self.buttonBox.buttonRole(button) role = self.buttonBox.buttonRole(button)
if role == QDialogButtonBox.ResetRole: if role == QDialogButtonBox.ResetRole:
self.resetToDefaults() self.resetToDefaults()

View File

@ -7,8 +7,10 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from PyQt5.QtCore import Qt, QMimeData, QByteArray from PyQt5.QtCore import Qt, QMimeData, QByteArray
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView, from PyQt5.QtWidgets import (
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy) QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QListView,
QDialogButtonBox, QAbstractItemView, QLabel, QStyle, QSplitter, QWidget, QSizePolicy
)
from hscommon.trans import trget from hscommon.trans import trget
from qtlib.selectable_list import ComboboxModel, ListviewModel from qtlib.selectable_list import ComboboxModel, ListviewModel
@ -59,7 +61,9 @@ class PrioritizeDialog(QDialog):
self.model = PrioritizeDialogModel(app=app.model) self.model = PrioritizeDialogModel(app=app.model)
self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox) self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)
self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView) 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.model.view = self
self.addCriteriaButton.clicked.connect(self.model.add_selected) self.addCriteriaButton.clicked.connect(self.model.add_selected)
@ -72,9 +76,11 @@ class PrioritizeDialog(QDialog):
self.resize(700, 400) self.resize(700, 400)
#widgets #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 " "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 = QLabel(msg)
self.promptLabel.setWordWrap(True) self.promptLabel.setWordWrap(True)
self.categoryCombobox = QComboBox() self.categoryCombobox = QComboBox()

View File

@ -7,8 +7,10 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy, from PyQt5.QtWidgets import (
QLabel, QTableView, QAbstractItemView, QApplication) QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QSpacerItem, QSizePolicy,
QLabel, QTableView, QAbstractItemView, QApplication
)
from hscommon.trans import trget from hscommon.trans import trget
from .problem_table import ProblemTable from .problem_table import ProblemTable
@ -32,9 +34,11 @@ class ProblemDialog(QDialog):
self.resize(413, 323) self.resize(413, 323)
self.verticalLayout = QVBoxLayout(self) self.verticalLayout = QVBoxLayout(self)
self.label = QLabel(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 " "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.setText(msg)
self.label.setWordWrap(True) self.label.setWordWrap(True)
self.verticalLayout.addWidget(self.label) self.verticalLayout.addWidget(self.label)

View File

@ -7,8 +7,10 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from PyQt5.QtCore import Qt, QRect from PyQt5.QtCore import Qt, QRect
from PyQt5.QtWidgets import (QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget, from PyQt5.QtWidgets import (
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox) QMainWindow, QMenu, QLabel, QFileDialog, QMenuBar, QWidget,
QVBoxLayout, QAbstractItemView, QStatusBar, QDialog, QPushButton, QCheckBox
)
from hscommon.trans import trget from hscommon.trans import trget
from qtlib.util import moveToScreenCenter, horizontalWrap, createActions from qtlib.util import moveToScreenCenter, horizontalWrap, createActions
@ -50,11 +52,23 @@ class ResultWindow(QMainWindow):
('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered), ('actionCopyMarked', 'Ctrl+Shift+M', '', tr("Copy Marked to..."), self.copyTriggered),
('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered), ('actionRemoveMarked', 'Ctrl+R', '', tr("Remove Marked from Results"), self.removeMarkedTriggered),
('actionReprioritize', '', '', tr("Re-Prioritize Results..."), self.reprioritizeTriggered), ('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), 'actionRemoveSelected', 'Ctrl+Del', '',
('actionMakeSelectedReference', 'Ctrl+Space', '', tr("Make Selected into Reference"), self.app.model.make_selected_reference), 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), ('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), ('actionRenameSelected', 'F2', '', tr("Rename Selected"), self.renameTriggered),
('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered), ('actionMarkAll', 'Ctrl+A', '', tr("Mark All"), self.markAllTriggered),
('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered), ('actionMarkNone', 'Ctrl+Shift+A', '', tr("Mark None"), self.markNoneTriggered),
@ -170,8 +184,10 @@ class ResultWindow(QMainWindow):
self.deltaValuesCheckBox = QCheckBox(tr("Delta Values")) self.deltaValuesCheckBox = QCheckBox(tr("Delta Values"))
self.searchEdit = SearchEdit() self.searchEdit = SearchEdit()
self.searchEdit.setMaximumWidth(300) self.searchEdit.setMaximumWidth(300)
self.horizontalLayout = horizontalWrap([self.actionsButton, self.detailsButton, self.horizontalLayout = horizontalWrap([
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8]) self.actionsButton, self.detailsButton,
self.dupesOnlyCheckBox, self.deltaValuesCheckBox, None, self.searchEdit, 8
])
self.horizontalLayout.setSpacing(8) self.horizontalLayout.setSpacing(8)
self.verticalLayout.addLayout(self.horizontalLayout) self.verticalLayout.addLayout(self.horizontalLayout)
self.resultsView = ResultsView(self.centralwidget) self.resultsView = ResultsView(self.centralwidget)

View File

@ -8,8 +8,10 @@
import sys import sys
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, from PyQt5.QtWidgets import (
QApplication) QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
QApplication
)
from hscommon.trans import trget from hscommon.trans import trget
from core.scanner import ScanType from core.scanner import ScanType
@ -117,8 +119,10 @@ class PreferencesDialog(PreferencesDialogBase):
#--- Events #--- Events
def scanTypeChanged(self, index): def scanTypeChanged(self, index):
scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()] scan_type = SCAN_TYPE_ORDER[self.scanTypeComboBox.currentIndex()]
word_based = scan_type in (ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder, word_based = scan_type in (
ScanType.Tag) ScanType.Filename, ScanType.Fields, ScanType.FieldsNoOrder,
ScanType.Tag
)
tag_based = scan_type == ScanType.Tag tag_based = scan_type == ScanType.Tag
self.filterHardnessSlider.setEnabled(word_based) self.filterHardnessSlider.setEnabled(word_based)
self.matchSimilarBox.setEnabled(word_based) self.matchSimilarBox.setEnabled(word_based)
@ -138,3 +142,4 @@ if __name__ == '__main__':
dialog = PreferencesDialog(None, dgapp) dialog = PreferencesDialog(None, dgapp)
dialog.show() dialog.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@ -30,3 +30,4 @@ class ResultsModel(ResultsModelBase):
Column('words', defaultWidth=120), Column('words', defaultWidth=120),
Column('dupe_count', defaultWidth=80), Column('dupe_count', defaultWidth=80),
] ]

View File

@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from ._block_qt import getblocks from ._block_qt import getblocks # NOQA
# Converted to C # Converted to C
# def getblock(image): # def getblock(image):

View File

@ -76,3 +76,4 @@ if __name__ == '__main__':
dialog = PreferencesDialog(None, dgapp) dialog = PreferencesDialog(None, dgapp)
dialog.show() dialog.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@ -21,3 +21,4 @@ class ResultsModel(ResultsModelBase):
Column('percentage', defaultWidth=60), Column('percentage', defaultWidth=60),
Column('dupe_count', defaultWidth=80), Column('dupe_count', defaultWidth=80),
] ]

View File

@ -18,6 +18,7 @@ from .preferences_dialog import PreferencesDialog
class Directories(DirectoriesBase): class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files']) ROOT_PATH_TO_EXCLUDE = frozenset(['windows', 'program files'])
def _default_state_for_path(self, path): def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path) result = DirectoriesBase._default_state_for_path(self, path)
if result is not None: if result is not None:

View File

@ -8,8 +8,10 @@
import sys import sys
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget, from PyQt5.QtWidgets import (
QLineEdit, QApplication) QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QSpacerItem, QWidget,
QLineEdit, QApplication
)
from hscommon.plat import ISWINDOWS, ISLINUX from hscommon.plat import ISWINDOWS, ISLINUX
from hscommon.trans import trget from hscommon.trans import trget
@ -73,7 +75,10 @@ class PreferencesDialog(PreferencesDialogBase):
spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1) self.horizontalLayout_2.addItem(spacerItem1)
self.verticalLayout_4.addLayout(self.horizontalLayout_2) 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.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)
self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget) self._setupAddCheckbox('debugModeBox', tr("Debug mode (restart required)"), self.widget)
self.verticalLayout_4.addWidget(self.debugModeBox) self.verticalLayout_4.addWidget(self.debugModeBox)

View File

@ -20,3 +20,4 @@ class ResultsModel(ResultsModelBase):
Column('words', defaultWidth=120), Column('words', defaultWidth=120),
Column('dupe_count', defaultWidth=80), Column('dupe_count', defaultWidth=80),
] ]

View File

@ -1,2 +1,4 @@
pytest>=2.0.0 pytest>=2.0.0
pytest-monkeyplus>=1.0.0 pytest-monkeyplus>=1.0.0
flake8

17
tox.ini Normal file
View File

@ -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