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*
.lock-waf*
.idea
.tox
build
dist

121
build.py
View File

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

View File

@ -6,7 +6,6 @@
# 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)

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_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):

View File

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

View File

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

View File

@ -19,26 +19,26 @@ MAIN_TEMPLATE = """
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>dupeGuru Results</title>
<style type="text/css">
<title>dupeGuru Results</title>
<style type="text/css">
BODY
{
background-color:white;
background-color:white;
}
BODY,A,P,UL,TABLE,TR,TD
{
font-family:Tahoma,Arial,sans-serif;
font-size:10pt;
color: #4477AA;
font-family:Tahoma,Arial,sans-serif;
font-size:10pt;
color: #4477AA;
}
TABLE
{
background-color: #225588;
margin-left: auto;
margin-right: auto;
width: 90%;
background-color: #225588;
margin-left: auto;
margin-right: auto;
width: 90%;
}
TR
@ -48,9 +48,9 @@ TR
TH
{
font-weight: bold;
color: black;
background-color: #C8D6E5;
font-weight: bold;
color: black;
background-color: #C8D6E5;
}
TH TD
@ -65,10 +65,10 @@ TD
TD.rightelem
{
text-align:right;
/*padding-left:0pt;*/
padding-right: 2pt;
width: 17%;
text-align:right;
/*padding-left:0pt;*/
padding-right: 2pt;
width: 17%;
}
TD.indented
@ -78,19 +78,19 @@ TD.indented
H1
{
font-family:&quot;Courier New&quot;,monospace;
color:#6699CC;
font-family:&quot;Courier New&quot;,monospace;
color:#6699CC;
font-size:18pt;
color:#6da500;
border-color: #70A0CF;
border-width: 1pt;
border-style: solid;
margin-top: 16pt;
margin-left: 5%;
margin-right: 5%;
padding-top: 2pt;
padding-bottom:2pt;
text-align: center;
color:#6da500;
border-color: #70A0CF;
border-width: 1pt;
border-style: solid;
margin-top: 16pt;
margin-left: 5%;
margin-right: 5%;
padding-top: 2pt;
padding-bottom:2pt;
text-align: center;
}
</style>
</head>

View File

@ -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):
@ -208,7 +209,7 @@ 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)

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

View File

@ -7,7 +7,6 @@
# 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):

View File

@ -71,6 +71,7 @@ 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):

View File

@ -22,41 +22,41 @@ class IgnoreList:
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]

View File

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

View File

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

View File

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

View File

@ -13,8 +13,10 @@ 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)

View File

@ -12,23 +12,25 @@ 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())
@ -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,

View File

@ -7,8 +7,10 @@
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')
@ -31,5 +33,8 @@ class SamplerateCategory(NumericalCategory):
return dupe.samplerate
def all_categories():
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DurationCategory,
BitrateCategory, SamplerateCategory, MtimeCategory]
return [
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
# 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):

View File

@ -19,7 +19,7 @@ def colors_to_string(colors):
[(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):
@ -93,9 +93,9 @@ class Cache:
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)

View File

@ -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,7 +283,7 @@ 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.

View File

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

View File

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

View File

@ -23,7 +23,7 @@ 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())

View File

@ -7,8 +7,10 @@
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')
@ -23,5 +25,7 @@ class DimensionsCategory(NumericalCategory):
return (-width, -height)
def all_categories():
return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
MtimeCategory]
return [
KindCategory, FolderCategory, FilenameCategory, SizeCategory, DimensionsCategory,
MtimeCategory
]

View File

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

View File

@ -14,7 +14,6 @@ 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
@ -127,8 +126,8 @@ class DupeGuru(QObject):
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):

View File

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

View File

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

View File

@ -7,9 +7,11 @@
# 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
@ -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)
@ -225,7 +227,7 @@ class DirectoriesDialog(QMainWindow):
if __name__ == '__main__':
import sys
from . import dg_rc
from . import dg_rc # NOQA
from ..testapp import TestApp
app = QApplication([])
dgapp = TestApp()

View File

@ -9,8 +9,10 @@
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,7 +25,7 @@ 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
@ -47,7 +49,7 @@ class DirectoriesDelegate(QStyledItemDelegate):
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):

View File

@ -45,8 +45,12 @@ 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):

View File

@ -7,8 +7,10 @@
# 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
@ -190,3 +192,4 @@ class PreferencesDialogBase(QDialog):
role = self.buttonBox.buttonRole(button)
if role == QDialogButtonBox.ResetRole:
self.resetToDefaults()

View File

@ -7,8 +7,10 @@
# 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
@ -59,7 +61,9 @@ 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)
@ -72,9 +76,11 @@ class PrioritizeDialog(QDialog):
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()

View File

@ -7,8 +7,10 @@
# 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
@ -32,9 +34,11 @@ class ProblemDialog(QDialog):
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)

View File

@ -7,8 +7,10 @@
# 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
@ -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),
@ -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)

View File

@ -8,8 +8,10 @@
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
@ -117,8 +119,10 @@ class PreferencesDialog(PreferencesDialogBase):
#--- 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)
@ -138,3 +142,4 @@ if __name__ == '__main__':
dialog = PreferencesDialog(None, dgapp)
dialog.show()
sys.exit(app.exec_())

View File

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

View File

@ -6,7 +6,7 @@
# 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):

View File

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

View File

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

View File

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

View File

@ -8,8 +8,10 @@
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
@ -73,7 +75,10 @@ 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)

View File

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

View File

@ -1,2 +1,4 @@
pytest>=2.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