1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2024-12-21 10:59:03 +00:00

Format all files with black correcting line length

This commit is contained in:
Andrew Senetar 2021-08-15 04:10:18 -05:00
parent 9446f37fad
commit ffe6b7047c
Signed by: arsenetar
GPG Key ID: C63300DCE48AB2F1
80 changed files with 517 additions and 970 deletions

View File

@ -30,12 +30,8 @@ def parse_args():
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("--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(
"--updatepot",
action="store_true",
@ -96,9 +92,7 @@ def build_localizations():
locale_dest = op.join("build", "locale")
if op.exists(locale_dest):
shutil.rmtree(locale_dest)
shutil.copytree(
"locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot")
)
shutil.copytree("locale", locale_dest, ignore=shutil.ignore_patterns("*.po", "*.pot"))
def build_updatepot():
@ -165,9 +159,7 @@ def build_normal():
print("Building localizations")
build_localizations()
print("Building Qt stuff")
print_and_do(
"pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py"))
)
print_and_do("pyrcc5 {0} > {1}".format(op.join("qt", "dg.qrc"), op.join("qt", "dg_rc.py")))
fix_qt_resource_file(op.join("qt", "dg_rc.py"))
build_help()

View File

@ -132,9 +132,7 @@ class DupeGuru(Broadcaster):
logging.debug("Debug mode enabled")
Broadcaster.__init__(self)
self.view = view
self.appdata = desktop.special_folder_path(
desktop.SpecialFolder.AppData, appname=self.NAME
)
self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME)
if not op.exists(self.appdata):
os.makedirs(self.appdata)
self.app_mode = AppMode.Standard
@ -182,17 +180,13 @@ class DupeGuru(Broadcaster):
def _get_picture_cache_path(self):
cache_type = self.options["picture_cache_type"]
cache_name = (
"cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
)
cache_name = "cached_pictures.shelve" if cache_type == "shelve" else "cached_pictures.db"
return op.join(self.appdata, cache_name)
def _get_dupe_sort_key(self, dupe, get_group, key, delta):
if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == "folder_path":
dupe_folder_path = getattr(
dupe, "display_folder_path", dupe.folder_path
)
dupe_folder_path = getattr(dupe, "display_folder_path", dupe.folder_path)
return str(dupe_folder_path).lower()
if self.app_mode == AppMode.Picture:
if delta and key == "dimensions":
@ -220,9 +214,7 @@ class DupeGuru(Broadcaster):
def _get_group_sort_key(self, group, key):
if self.app_mode in (AppMode.Music, AppMode.Picture):
if key == "folder_path":
dupe_folder_path = getattr(
group.ref, "display_folder_path", group.ref.folder_path
)
dupe_folder_path = getattr(group.ref, "display_folder_path", group.ref.folder_path)
return str(dupe_folder_path).lower()
if key == "percentage":
return group.percentage
@ -235,9 +227,7 @@ class DupeGuru(Broadcaster):
def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(
dupe, link_deleted, use_hardlinks, direct_deletion
)
return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)
j.start_job(self.results.mark_count)
self.results.perform_on_marked(op, True)
@ -277,11 +267,7 @@ 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):
@ -293,11 +279,7 @@ 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=()):
@ -332,9 +314,7 @@ class DupeGuru(Broadcaster):
msg = {
JobType.Copy: tr("All marked files were copied successfully."),
JobType.Move: tr("All marked files were moved successfully."),
JobType.Delete: tr(
"All marked files were successfully sent to Trash."
),
JobType.Delete: tr("All marked files were successfully sent to Trash."),
}[jobid]
self.view.show_message(msg)
@ -401,15 +381,12 @@ class DupeGuru(Broadcaster):
self.view.show_message(tr("'{}' does not exist.").format(d))
def add_selected_to_ignore_list(self):
"""Adds :attr:`selected_dupes` to :attr:`ignore_list`.
"""
"""Adds :attr:`selected_dupes` to :attr:`ignore_list`."""
dupes = self.without_ref(self.selected_dupes)
if not dupes:
self.view.show_message(MSG_NO_SELECTED_DUPES)
return
msg = tr(
"All selected %d matches are going to be ignored in all subsequent scans. Continue?"
)
msg = tr("All selected %d matches are going to be ignored in all subsequent scans. Continue?")
if not self.view.ask_yes_no(msg % len(dupes)):
return
for dupe in dupes:
@ -483,16 +460,17 @@ class DupeGuru(Broadcaster):
self.view.show_message(MSG_NO_MARKED_DUPES)
return
destination = self.view.select_dest_folder(
tr("Select a directory to copy marked files to") if copy
else tr("Select a directory to move marked files to"))
tr("Select a directory to copy marked files to")
if copy
else tr("Select a directory to move marked files to")
)
if destination:
desttype = self.options["copymove_dest_type"]
jobid = JobType.Copy if copy else JobType.Move
self._start_job(jobid, do)
def delete_marked(self):
"""Start an async job to send marked duplicates to the trash.
"""
"""Start an async job to send marked duplicates to the trash."""
if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES)
return
@ -523,9 +501,7 @@ class DupeGuru(Broadcaster):
The columns and their order in the resulting CSV file is determined in the same way as in
:meth:`export_to_xhtml`.
"""
dest_file = self.view.select_dest_file(
tr("Select a destination for your exported CSV"), "csv"
)
dest_file = self.view.select_dest_file(tr("Select a destination for your exported CSV"), "csv")
if dest_file:
colnames, rows = self._get_export_data()
try:
@ -542,9 +518,7 @@ class DupeGuru(Broadcaster):
try:
return dupe.get_display_info(group, delta)
except Exception as e:
logging.warning(
"Exception (type: %s) on GetDisplayInfo for %s: %s",
type(e), str(dupe.path), str(e))
logging.warning("Exception (type: %s) on GetDisplayInfo for %s: %s", type(e), str(dupe.path), str(e))
return empty_data()
def invoke_custom_command(self):
@ -556,9 +530,7 @@ class DupeGuru(Broadcaster):
"""
cmd = self.view.get_default("CustomCommand")
if not cmd:
msg = tr(
"You have no custom command set up. Set it up in your preferences."
)
msg = tr("You have no custom command set up. Set it up in your preferences.")
self.view.show_message(msg)
return
if not self.selected_dupes:
@ -634,9 +606,7 @@ class DupeGuru(Broadcaster):
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
d for d in self.selected_dupes if self.results.get_group_of_duplicate(d).ref is d
]
self.notify("results_changed")
else:
@ -648,20 +618,17 @@ class DupeGuru(Broadcaster):
self.notify("results_changed_but_keep_selection")
def mark_all(self):
"""Set all dupes in the results as marked.
"""
"""Set all dupes in the results as marked."""
self.results.mark_all()
self.notify("marking_changed")
def mark_none(self):
"""Set all dupes in the results as unmarked.
"""
"""Set all dupes in the results as unmarked."""
self.results.mark_none()
self.notify("marking_changed")
def mark_invert(self):
"""Invert the marked state of all dupes in the results.
"""
"""Invert the marked state of all dupes in the results."""
self.results.mark_invert()
self.notify("marking_changed")
@ -679,8 +646,7 @@ class DupeGuru(Broadcaster):
self.notify("marking_changed")
def open_selected(self):
"""Open :attr:`selected_dupes` with their associated application.
"""
"""Open :attr:`selected_dupes` with their associated application."""
if len(self.selected_dupes) > 10:
if not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):
return
@ -688,8 +654,7 @@ class DupeGuru(Broadcaster):
desktop.open_path(dupe.path)
def purge_ignore_list(self):
"""Remove files that don't exist from :attr:`ignore_list`.
"""
"""Remove files that don't exist from :attr:`ignore_list`."""
self.ignore_list.Filter(lambda f, s: op.exists(f) and op.exists(s))
self.ignore_list_dialog.refresh()
@ -719,8 +684,7 @@ class DupeGuru(Broadcaster):
self.notify("results_changed_but_keep_selection")
def remove_marked(self):
"""Removed marked duplicates from the results (without touching the files themselves).
"""
"""Removed marked duplicates from the results (without touching the files themselves)."""
if not self.results.mark_count:
self.view.show_message(MSG_NO_MARKED_DUPES)
return
@ -731,8 +695,7 @@ class DupeGuru(Broadcaster):
self._results_changed()
def remove_selected(self):
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves).
"""
"""Removed :attr:`selected_dupes` from the results (without touching the files themselves)."""
dupes = self.without_ref(self.selected_dupes)
if not dupes:
self.view.show_message(MSG_NO_SELECTED_DUPES)
@ -773,9 +736,7 @@ class DupeGuru(Broadcaster):
if count:
self.results.refresh_required = True
self._results_changed()
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(
count
)
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(count)
self.view.show_message(msg)
def reveal_selected(self):
@ -819,9 +780,7 @@ class DupeGuru(Broadcaster):
"""
scanner = self.SCANNER_CLASS()
if not self.directories.has_any_file():
self.view.show_message(
tr("The selected directories contain no scannable file.")
)
self.view.show_message(tr("The selected directories contain no scannable file."))
return
# Send relevant options down to the scanner instance
for k, v in self.options.items():
@ -836,13 +795,9 @@ class DupeGuru(Broadcaster):
def do(j):
j.set_progress(0, tr("Collecting files to scan"))
if scanner.scan_type == ScanType.Folders:
files = list(
self.directories.get_folders(folderclass=se.fs.Folder, j=j)
)
files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))
else:
files = list(
self.directories.get_files(fileclasses=self.fileclasses, j=j)
)
files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))
if self.options["ignore_hardlink_matches"]:
files = self._remove_hardlink_dupes(files)
logging.info("Scanning %d files" % len(files))
@ -864,13 +819,8 @@ class DupeGuru(Broadcaster):
self.notify("marking_changed")
def without_ref(self, dupes):
"""Returns ``dupes`` with all reference elements removed.
"""
return [
dupe
for dupe in dupes
if self.results.get_group_of_duplicate(dupe).ref is not dupe
]
"""Returns ``dupes`` with all reference elements removed."""
return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
def get_default(self, key, fallback_value=None):
result = nonone(self.view.get_default(key), fallback_value)

View File

@ -109,8 +109,7 @@ class Directories:
# print(f"len of files: {len(files)} {files}")
for f in files:
if not self._exclude_list.is_excluded(root, f):
found_files.append(fs.get_file(rootPath + f,
fileclasses=fileclasses))
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses))
found_files = [f for f in found_files if f is not None]
# In some cases, directories can be considered as files by dupeGuru, which is
# why we have this line below. In fact, there only one case: Bundle files under

View File

@ -4,6 +4,7 @@
from .markable import Markable
from xml.etree import ElementTree as ET
# TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/
# also https://pypi.org/project/re2/
# TODO update the Result list with newly added regexes if possible
@ -15,13 +16,14 @@ from hscommon.util import FileOrPath
from hscommon.plat import ISWINDOWS
import time
default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP
r"^desktop\.ini$", # Windows metadata
r"^\.DS_Store$", # MacOS metadata
r"^\.Trash\-.*", # Linux trash directories
r"^\$Recycle\.Bin$", # Windows
r"^\..*", # Hidden files on Unix-like
]
default_regexes = [
r"^thumbs\.db$", # Obsolete after WindowsXP
r"^desktop\.ini$", # Windows metadata
r"^\.DS_Store$", # MacOS metadata
r"^\.Trash\-.*", # Linux trash directories
r"^\$Recycle\.Bin$", # Windows
r"^\..*", # Hidden files on Unix-like
]
# These are too broad
forbidden_regexes = [r".*", r"\/.*", r".*\/.*", r".*\\\\.*", r".*\..*"]
@ -34,6 +36,7 @@ def timer(func):
end = time.perf_counter_ns()
print(f"DEBUG: func {func.__name__!r} took {end - start} ns.")
return value
return wrapper_timer
@ -45,11 +48,13 @@ def memoize(func):
if args not in func.cache:
func.cache[args] = func(*args)
return func.cache[args]
return _memoize
class AlreadyThereException(Exception):
"""Expression already in the list"""
def __init__(self, arg="Expression is already in excluded list."):
super().__init__(arg)
@ -148,7 +153,7 @@ class ExcludeList(Markable):
try:
return re.compile(expr)
except Exception as e:
raise(e)
raise (e)
# @timer
# @memoize # probably not worth memoizing this one if we memoize the above
@ -169,10 +174,8 @@ class ExcludeList(Markable):
def build_compiled_caches(self, union=False):
if not union:
self._cached_compiled_files =\
[x for x in self._excluded_compiled if not has_sep(x.pattern)]
self._cached_compiled_paths =\
[x for x in self._excluded_compiled if has_sep(x.pattern)]
self._cached_compiled_files = [x for x in self._excluded_compiled if not has_sep(x.pattern)]
self._cached_compiled_paths = [x for x in self._excluded_compiled if has_sep(x.pattern)]
self._dirty = False
return
@ -185,20 +188,17 @@ class ExcludeList(Markable):
else:
# HACK returned as a tuple to get a free iterator and keep interface
# the same regardless of whether the client asked for union or not
self._cached_compiled_union_all =\
(re.compile('|'.join(marked_count)),)
self._cached_compiled_union_all = (re.compile("|".join(marked_count)),)
files_marked = [x for x in marked_count if not has_sep(x)]
if not files_marked:
self._cached_compiled_union_files = tuple()
else:
self._cached_compiled_union_files =\
(re.compile('|'.join(files_marked)),)
self._cached_compiled_union_files = (re.compile("|".join(files_marked)),)
paths_marked = [x for x in marked_count if has_sep(x)]
if not paths_marked:
self._cached_compiled_union_paths = tuple()
else:
self._cached_compiled_union_paths =\
(re.compile('|'.join(paths_marked)),)
self._cached_compiled_union_paths = (re.compile("|".join(paths_marked)),)
self._dirty = False
@property
@ -218,16 +218,14 @@ class ExcludeList(Markable):
one item (one Pattern in the union case)."""
if self._dirty:
self.build_compiled_caches(self._use_union)
return self._cached_compiled_union_files if self._use_union\
else self._cached_compiled_files
return self._cached_compiled_union_files if self._use_union else self._cached_compiled_files
@property
def compiled_paths(self):
"""Returns patterns with only separators in them, for more precise filtering."""
if self._dirty:
self.build_compiled_caches(self._use_union)
return self._cached_compiled_union_paths if self._use_union\
else self._cached_compiled_paths
return self._cached_compiled_union_paths if self._use_union else self._cached_compiled_paths
# ---Public
def add(self, regex, forced=False):
@ -295,8 +293,7 @@ class ExcludeList(Markable):
was_marked = self.is_marked(regex)
is_compilable, exception, compiled = self.compile_re(newregex)
# We overwrite the found entry
self._excluded[self._excluded.index(item)] =\
[newregex, is_compilable, exception, compiled]
self._excluded[self._excluded.index(item)] = [newregex, is_compilable, exception, compiled]
self._remove_compiled(regex)
break
if not found:
@ -343,8 +340,10 @@ class ExcludeList(Markable):
# "forced" avoids compilation exceptions and adds anyway
self.add(regex_string, forced=True)
except AlreadyThereException:
logging.error(f"Regex \"{regex_string}\" \
loaded from XML was already present in the list.")
logging.error(
f'Regex "{regex_string}" \
loaded from XML was already present in the list.'
)
continue
if exclude_item.get("marked") == "y":
marked.add(regex_string)
@ -369,6 +368,7 @@ loaded from XML was already present in the list.")
class ExcludeDict(ExcludeList):
"""Exclusion list holding a set of regular expressions as keys, the compiled
Pattern, compilation error and compilable boolean as values."""
# Implemntation around a dictionary instead of a list, which implies
# to keep the index of each string-key as its sub-element and keep it updated
# whenever insert/remove is done.
@ -435,12 +435,7 @@ class ExcludeDict(ExcludeList):
# and other indices should be pushed by one
for value in self._excluded.values():
value["index"] += 1
self._excluded[regex] = {
"index": 0,
"compilable": iscompilable,
"error": exception,
"compiled": compiled
}
self._excluded[regex] = {"index": 0, "compilable": iscompilable, "error": exception, "compiled": compiled}
def has_entry(self, regex):
if regex in self._excluded.keys():
@ -468,10 +463,10 @@ class ExcludeDict(ExcludeList):
previous = self._excluded.pop(regex)
iscompilable, error, compiled = self.compile_re(newregex)
self._excluded[newregex] = {
"index": previous.get('index'),
"index": previous.get("index"),
"compilable": iscompilable,
"error": error,
"compiled": compiled
"compiled": compiled,
}
self._remove_compiled(regex)
if iscompilable:
@ -511,8 +506,12 @@ def ordered_keys(_dict):
if ISWINDOWS:
def has_sep(regexp):
return '\\' + sep in regexp
return "\\" + sep in regexp
else:
def has_sep(regexp):
return sep in regexp

View File

@ -131,15 +131,11 @@ def export_to_xhtml(colnames, rows):
indented = "indented"
filename = row[1]
cells = "".join(CELL_TEMPLATE.format(value=value) for value in row[2:])
rendered_rows.append(
ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells)
)
rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))
previous_group_id = row[0]
rendered_rows = "".join(rendered_rows)
# The main template can't use format because the css code uses {}
content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace(
"$rows", rendered_rows
)
content = MAIN_TEMPLATE.replace("$colheaders", colheaders).replace("$rows", rendered_rows)
folder = mkdtemp()
destpath = op.join(folder, "export.htm")
fp = open(destpath, "wt", encoding="utf-8")

View File

@ -79,16 +79,9 @@ class OperationError(FSError):
class File:
"""Represents a file and holds metadata to be used for scanning.
"""
"""Represents a file and holds metadata to be used for scanning."""
INITIAL_INFO = {
"size": 0,
"mtime": 0,
"md5": b"",
"md5partial": b"",
"md5samples": b""
}
INITIAL_INFO = {"size": 0, "mtime": 0, "md5": b"", "md5partial": b"", "md5samples": b""}
# Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of
# files, I saved 35% memory usage with "unread" files (no _read_info() call) and gains become
# even greater when we take into account read attributes (70%!). Yeah, it's worth it.
@ -108,9 +101,7 @@ class File:
try:
self._read_info(attrname)
except Exception as e:
logging.warning(
"An error '%s' was raised while decoding '%s'", e, repr(self.path)
)
logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path))
result = object.__getattribute__(self, attrname)
if result is NOT_SET:
result = self.INITIAL_INFO[attrname]
@ -192,8 +183,7 @@ class File:
# --- Public
@classmethod
def can_handle(cls, path):
"""Returns whether this file wrapper class can handle ``path``.
"""
"""Returns whether this file wrapper class can handle ``path``."""
return not path.islink() and path.isfile()
def rename(self, newname):
@ -211,8 +201,7 @@ class File:
self.path = destpath
def get_display_info(self, group, delta):
"""Returns a display-ready dict of dupe's data.
"""
"""Returns a display-ready dict of dupe's data."""
raise NotImplementedError()
# --- Properties
@ -271,9 +260,7 @@ class Folder(File):
@property
def subfolders(self):
if self._subfolders is None:
subfolders = [
p for p in self.path.listdir() if not p.islink() and p.isdir()
]
subfolders = [p for p in self.path.listdir() if not p.islink() and p.isdir()]
self._subfolders = [self.__class__(p) for p in subfolders]
return self._subfolders

View File

@ -29,8 +29,7 @@ class DeletionOptionsView:
"""
def update_msg(self, msg: str):
"""Update the dialog's prompt with ``str``.
"""
"""Update the dialog's prompt with ``str``."""
def show(self):
"""Show the dialog in a modal fashion.
@ -39,8 +38,7 @@ class DeletionOptionsView:
"""
def set_hardlink_option_enabled(self, is_enabled: bool):
"""Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`.
"""
"""Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`."""
class DeletionOptions(GUIObject):
@ -75,8 +73,7 @@ class DeletionOptions(GUIObject):
return self.view.show()
def supports_links(self):
"""Returns whether our platform supports symlinks.
"""
"""Returns whether our platform supports symlinks."""
# When on a platform that doesn't implement it, calling os.symlink() (with the wrong number
# of arguments) raises NotImplementedError, which allows us to gracefully check for the
# feature.

View File

@ -32,9 +32,7 @@ class DetailsPanel(GUIObject, DupeGuruGUIObject):
# we don't want the two sides of the table to display the stats for the same file
ref = group.ref if group is not None and group.ref is not dupe else None
data2 = self.app.get_display_info(ref, group, False)
columns = self.app.result_table.COLUMNS[
1:
] # first column is the 'marked' column
columns = self.app.result_table.COLUMNS[1:] # first column is the 'marked' column
self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]
# --- Public

View File

@ -36,9 +36,7 @@ class DirectoryNode(Node):
self._loaded = True
def update_all_states(self):
self._state = STATE_ORDER.index(
self._tree.app.directories.get_state(self._directory_path)
)
self._state = STATE_ORDER.index(self._tree.app.directories.get_state(self._directory_path))
for node in self:
node.update_all_states()

View File

@ -50,7 +50,7 @@ class ExcludeListDialogCore:
try:
self.exclude_list.add(regex)
except Exception as e:
raise(e)
raise (e)
self.exclude_list.mark(regex)
self.exclude_list_table.add(regex)

View File

@ -6,14 +6,12 @@ from .base import DupeGuruGUIObject
from hscommon.gui.table import GUITable, Row
from hscommon.gui.column import Column, Columns
from hscommon.trans import trget
tr = trget("ui")
class ExcludeListTable(GUITable, DupeGuruGUIObject):
COLUMNS = [
Column("marked", ""),
Column("regex", tr("Regular Expressions"))
]
COLUMNS = [Column("marked", ""), Column("regex", tr("Regular Expressions"))]
def __init__(self, exclude_list_dialog, app):
GUITable.__init__(self)

View File

@ -22,9 +22,7 @@ class IgnoreListDialog:
def clear(self):
if not self.ignore_list:
return
msg = tr(
"Do you really want to remove all %d items from the ignore list?"
) % len(self.ignore_list)
msg = tr("Do you really want to remove all %d items from the ignore list?") % len(self.ignore_list)
if self.app.view.ask_yes_no(msg):
self.ignore_list.Clear()
self.refresh()

View File

@ -45,9 +45,7 @@ class DupeRow(Row):
return False
ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
for key, value in dupe_info.items():
if (key not in self._delta_columns) and (
ref_info[key].lower() != value.lower()
):
if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()):
self._delta_columns.add(key)
return column_name in self._delta_columns

View File

@ -33,8 +33,7 @@ CacheRow = namedtuple("CacheRow", "id path blocks mtime")
class ShelveCache:
"""A class to cache picture blocks in a shelve backend.
"""
"""A class to cache picture blocks in a shelve backend."""
def __init__(self, db=None, readonly=False):
self.istmp = db is None
@ -81,9 +80,7 @@ class ShelveCache:
self.shelve[wrap_id(rowid)] = wrap_path(path_str)
def _compute_maxid(self):
return max(
(unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1
)
return max((unwrap_id(k) for k in self.shelve if k.startswith("id:")), default=1)
def _get_new_id(self):
self.maxid += 1

View File

@ -13,8 +13,7 @@ from .cache import string_to_colors, colors_to_string
class SqliteCache:
"""A class to cache picture blocks in a sqlite backend.
"""
"""A class to cache picture blocks in a sqlite backend."""
def __init__(self, db=":memory:", readonly=False):
# readonly is not used in the sqlite version of the cache
@ -71,18 +70,14 @@ class SqliteCache:
except sqlite.OperationalError:
logging.warning("Picture cache could not set value for key %r", path_str)
except sqlite.DatabaseError as e:
logging.warning(
"DatabaseError while setting value for key %r: %s", path_str, str(e)
)
logging.warning("DatabaseError while setting value for key %r: %s", path_str, str(e))
def _create_con(self, second_try=False):
def create_tables():
logging.debug("Creating picture cache tables.")
self.con.execute("drop table if exists pictures")
self.con.execute("drop index if exists idx_path")
self.con.execute(
"create table pictures(path TEXT, mtime INTEGER, blocks TEXT)"
)
self.con.execute("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)
@ -93,9 +88,7 @@ class SqliteCache:
except sqlite.DatabaseError as e: # corrupted db
if second_try:
raise # Something really strange is happening
logging.warning(
"Could not create picture cache because of an error: %s", str(e)
)
logging.warning("Could not create picture cache because of an error: %s", str(e))
self.con.close()
os.remove(self.dbname)
self._create_con(second_try=True)
@ -125,9 +118,7 @@ class SqliteCache:
raise ValueError(path)
def get_multiple(self, rowids):
sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(
map(str, rowids)
)
sql = "select rowid, blocks from pictures where rowid in (%s)" % ",".join(map(str, rowids))
cur = self.con.execute(sql)
return ((rowid, string_to_colors(blocks)) for rowid, blocks in cur)
@ -148,7 +139,5 @@ class SqliteCache:
continue
todelete.append(rowid)
if todelete:
sql = "delete from pictures where rowid in (%s)" % ",".join(
map(str, todelete)
)
sql = "delete from pictures where rowid in (%s)" % ",".join(map(str, todelete))
self.con.execute(sql)

View File

@ -256,9 +256,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), self.s2n(offset + 4, 4, signed)
)
value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))
else:
# Not a fraction
value_j = self.s2n(offset, typelen, signed)
@ -296,9 +294,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

@ -95,9 +95,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
picture.unicode_path,
picture.size,
)
if (
picture.size < 10 * 1024 * 1024
): # We're really running out of memory
if picture.size < 10 * 1024 * 1024: # We're really running out of memory
raise
except MemoryError:
logging.warning("Ran out of memory while preparing pictures")
@ -106,9 +104,7 @@ def prepare_pictures(pictures, cache_path, with_dimensions, j=job.nulljob):
def get_chunks(pictures):
min_chunk_count = (
multiprocessing.cpu_count() * 2
) # have enough chunks to feed all subprocesses
min_chunk_count = multiprocessing.cpu_count() * 2 # have enough chunks to feed all subprocesses
chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE
chunk_count = max(min_chunk_count, chunk_count)
chunk_size = (len(pictures) // chunk_count) + 1
@ -185,9 +181,7 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
j.set_progress(comparison_count, progress_msg)
j = j.start_subjob([3, 7])
pictures = prepare_pictures(
pictures, cache_path, with_dimensions=not match_scaled, j=j
)
pictures = prepare_pictures(pictures, cache_path, with_dimensions=not match_scaled, j=j)
j = j.start_subjob([9, 1], tr("Preparing for matching"))
cache = get_cache(cache_path)
id2picture = {}
@ -231,12 +225,8 @@ def getmatches(pictures, cache_path, threshold, match_scaled=False, j=job.nulljo
chunks,
pictures,
) # some wiggle room for the next statements
logging.warning(
"Ran out of memory when scanning! We had %d matches.", len(matches)
)
del matches[
-len(matches) // 3 :
] # some wiggle room to ensure we don't run out of memory again.
logging.warning("Ran out of memory when scanning! We had %d matches.", len(matches))
del matches[-len(matches) // 3 :] # some wiggle room to ensure we don't run out of memory again.
pool.close()
result = []
myiter = j.iter_with_progress(

View File

@ -87,11 +87,7 @@ class Scanner:
if self.size_threshold:
files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in {ScanType.Contents, ScanType.Folders}:
return engine.getmatches_by_contents(
files,
bigsize=self.big_file_size_threshold,
j=j
)
return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)
else:
j = j.start_subjob([2, 8])
kw = {}
@ -165,27 +161,13 @@ class Scanner:
toremove.add(p)
else:
last_parent_path = p
matches = [
m
for m in matches
if m.first.path not in toremove or m.second.path not in toremove
]
matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]
if not self.mix_file_kind:
matches = [
m
for m in matches
if get_file_ext(m.first.name) == get_file_ext(m.second.name)
]
matches = [
m for m in matches if m.first.path.exists() and m.second.path.exists()
]
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if m.first.path.exists() and m.second.path.exists()]
matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]
if ignore_list:
matches = [
m
for m in matches
if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))
]
matches = [m for m in matches if not ignore_list.AreIgnored(str(m.first.path), str(m.second.path))]
logging.info("Grouping matches")
groups = engine.get_groups(matches)
if self.scan_type in {
@ -194,9 +176,7 @@ class Scanner:
ScanType.FieldsNoOrder,
ScanType.Tag,
}:
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])
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
else:
# Ticket #195

View File

@ -29,9 +29,7 @@ def add_fake_files_to_directories(directories, files):
class TestCaseDupeGuru:
def test_apply_filter_calls_results_apply_filter(self, monkeypatch):
dgapp = TestApp().app
monkeypatch.setattr(
dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)
)
monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter))
dgapp.apply_filter("foo")
eq_(2, len(dgapp.results.apply_filter.calls))
call = dgapp.results.apply_filter.calls[0]
@ -41,15 +39,11 @@ class TestCaseDupeGuru:
def test_apply_filter_escapes_regexp(self, monkeypatch):
dgapp = TestApp().app
monkeypatch.setattr(
dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter)
)
monkeypatch.setattr(dgapp.results, "apply_filter", log_calls(dgapp.results.apply_filter))
dgapp.apply_filter("()[]\\.|+?^abc")
call = dgapp.results.apply_filter.calls[1]
eq_("\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc", call["filter_str"])
dgapp.apply_filter(
"(*)"
) # In "simple mode", we want the * to behave as a wilcard
dgapp.apply_filter("(*)") # In "simple mode", we want the * to behave as a wilcard
call = dgapp.results.apply_filter.calls[3]
eq_(r"\(.*\)", call["filter_str"])
dgapp.options["escape_filter_regexp"] = False
@ -70,9 +64,7 @@ class TestCaseDupeGuru:
)
# XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.
monkeypatch.setattr(app, "smart_copy", hscommon.conflict.smart_copy)
monkeypatch.setattr(
os, "makedirs", lambda path: None
) # We don't want the test to create that fake directory
monkeypatch.setattr(os, "makedirs", lambda path: None) # We don't want the test to create that fake directory
dgapp = TestApp().app
dgapp.directories.add_path(p)
[f] = dgapp.directories.get_files()
@ -320,9 +312,7 @@ class TestCaseDupeGuruWithResults:
assert groups[0].ref is objects[1]
assert groups[1].ref is objects[4]
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(
self, do_setup
):
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self, do_setup):
app = self.app
objects = self.objects
groups = self.groups
@ -404,9 +394,7 @@ class TestCaseDupeGuruWithResults:
# results table.
app = self.app
app.JOB = Job(1, lambda *args, **kw: False) # Cancels the task
add_fake_files_to_directories(
app.directories, self.objects
) # We want the scan to at least start
add_fake_files_to_directories(app.directories, self.objects) # We want the scan to at least start
app.start_scanning() # will be cancelled immediately
eq_(len(app.result_table), 0)

View File

@ -140,9 +140,7 @@ def GetTestGroups():
matches = engine.getmatches(objects) # we should have 5 matches
groups = engine.get_groups(matches) # We should have 2 groups
for g in groups:
g.prioritize(
lambda x: objects.index(x)
) # We want the dupes to be in the same order as the list is
g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is
groups.sort(key=len, reverse=True) # We want the group with 3 members to be first.
return (objects, matches, groups)

View File

@ -14,9 +14,7 @@ except ImportError:
skip("Can't import the block module, probably hasn't been compiled.")
def my_avgdiff(
first, second, limit=768, min_iter=3
): # this is so I don't have to re-write every call
def my_avgdiff(first, second, limit=768, min_iter=3): # this is so I don't have to re-write every call
return avgdiff(first, second, limit, min_iter)

View File

@ -254,7 +254,12 @@ def test_invalid_path():
def test_set_state_on_invalid_path():
d = Directories()
try:
d.set_state(Path("foobar",), DirectoryState.Normal)
d.set_state(
Path(
"foobar",
),
DirectoryState.Normal,
)
except LookupError:
assert False
@ -345,15 +350,17 @@ def test_default_path_state_override(tmpdir):
eq_(len(list(d.get_files())), 2)
class TestExcludeList():
class TestExcludeList:
def setup_method(self, method):
self.d = Directories(exclude_list=ExcludeList(union_regex=False))
def get_files_and_expect_num_result(self, num_result):
"""Calls get_files(), get the filenames only, print for debugging.
num_result is how many files are expected as a result."""
print(f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \
files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}")
print(
f"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \
files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}"
)
files = list(self.d.get_files())
files = [file.name for file in files]
print(f"FINAL FILES {files}")

View File

@ -5,6 +5,7 @@
# http://www.gnu.org/licenses/gpl-3.0.html
import io
# import os.path as op
from xml.etree import ElementTree as ET
@ -104,7 +105,7 @@ class TestCaseListEmpty:
regex1 = r"one"
regex2 = r"two"
self.exclude_list.add(regex1)
assert(regex1 in self.exclude_list)
assert regex1 in self.exclude_list
self.exclude_list.add(regex2)
self.exclude_list.mark(regex1)
self.exclude_list.mark(regex2)
@ -113,17 +114,17 @@ class TestCaseListEmpty:
compiled_files = [x for x in self.exclude_list.compiled_files]
eq_(len(compiled_files), 2)
self.exclude_list.remove(regex2)
assert(regex2 not in self.exclude_list)
assert regex2 not in self.exclude_list
eq_(len(self.exclude_list), 1)
def test_add_duplicate(self):
self.exclude_list.add(r"one")
eq_(1 , len(self.exclude_list))
eq_(1, len(self.exclude_list))
try:
self.exclude_list.add(r"one")
except Exception:
pass
eq_(1 , len(self.exclude_list))
eq_(1, len(self.exclude_list))
def test_add_not_compilable(self):
# Trying to add a non-valid regex should not work and raise exception
@ -230,13 +231,14 @@ class TestCaseListEmpty:
if compiled_re.pattern == re:
found = True
if not found:
raise(Exception(f"Default RE {re} not found in compiled list."))
raise (Exception(f"Default RE {re} not found in compiled list."))
continue
eq_(len(default_regexes), len(self.exclude_list.compiled))
class TestCaseListEmptyUnion(TestCaseListEmpty):
"""Same but with union regex"""
def setup_method(self, method):
self.app = DupeGuru()
self.app.exclude_list = ExcludeList(union_regex=True)
@ -246,7 +248,7 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
regex1 = r"one"
regex2 = r"two"
self.exclude_list.add(regex1)
assert(regex1 in self.exclude_list)
assert regex1 in self.exclude_list
self.exclude_list.add(regex2)
self.exclude_list.mark(regex1)
self.exclude_list.mark(regex2)
@ -256,7 +258,7 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
eq_(len(compiled_files), 1) # Two patterns joined together into one
assert "|" in compiled_files[0].pattern
self.exclude_list.remove(regex2)
assert(regex2 not in self.exclude_list)
assert regex2 not in self.exclude_list
eq_(len(self.exclude_list), 1)
def test_rename_regex_file_to_path(self):
@ -296,14 +298,15 @@ class TestCaseListEmptyUnion(TestCaseListEmpty):
compiled = [x for x in self.exclude_list.compiled]
assert regex not in compiled
# Need to escape both to get the same strings after compilation
compiled_escaped = set([x.encode('unicode-escape').decode() for x in compiled[0].pattern.split("|")])
default_escaped = set([x.encode('unicode-escape').decode() for x in default_regexes])
compiled_escaped = set([x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")])
default_escaped = set([x.encode("unicode-escape").decode() for x in default_regexes])
assert compiled_escaped == default_escaped
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
class TestCaseDictEmpty(TestCaseListEmpty):
"""Same, but with dictionary implementation"""
def setup_method(self, method):
self.app = DupeGuru()
self.app.exclude_list = ExcludeDict(union_regex=False)
@ -312,6 +315,7 @@ class TestCaseDictEmpty(TestCaseListEmpty):
class TestCaseDictEmptyUnion(TestCaseDictEmpty):
"""Same, but with union regex"""
def setup_method(self, method):
self.app = DupeGuru()
self.app.exclude_list = ExcludeDict(union_regex=True)
@ -321,7 +325,7 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
regex1 = r"one"
regex2 = r"two"
self.exclude_list.add(regex1)
assert(regex1 in self.exclude_list)
assert regex1 in self.exclude_list
self.exclude_list.add(regex2)
self.exclude_list.mark(regex1)
self.exclude_list.mark(regex2)
@ -331,7 +335,7 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
# two patterns joined into one
eq_(len(compiled_files), 1)
self.exclude_list.remove(regex2)
assert(regex2 not in self.exclude_list)
assert regex2 not in self.exclude_list
eq_(len(self.exclude_list), 1)
def test_rename_regex_file_to_path(self):
@ -371,8 +375,8 @@ class TestCaseDictEmptyUnion(TestCaseDictEmpty):
compiled = [x for x in self.exclude_list.compiled]
assert regex not in compiled
# Need to escape both to get the same strings after compilation
compiled_escaped = set([x.encode('unicode-escape').decode() for x in compiled[0].pattern.split("|")])
default_escaped = set([x.encode('unicode-escape').decode() for x in default_regexes])
compiled_escaped = set([x.encode("unicode-escape").decode() for x in compiled[0].pattern.split("|")])
default_escaped = set([x.encode("unicode-escape").decode() for x in default_regexes])
assert compiled_escaped == default_escaped
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
@ -382,8 +386,9 @@ def split_union(pattern_object):
return [x for x in pattern_object.pattern.split("|")]
class TestCaseCompiledList():
class TestCaseCompiledList:
"""Test consistency between union or and separate versions."""
def setup_method(self, method):
self.e_separate = ExcludeList(union_regex=False)
self.e_separate.restore_defaults()
@ -431,6 +436,7 @@ class TestCaseCompiledList():
class TestCaseCompiledDict(TestCaseCompiledList):
"""Test the dictionary version"""
def setup_method(self, method):
self.e_separate = ExcludeDict(union_regex=False)
self.e_separate.restore_defaults()

View File

@ -73,9 +73,7 @@ def test_save_to_xml():
eq_(len(root), 2)
eq_(len([c for c in root if c.tag == "file"]), 2)
f1, f2 = root[:]
subchildren = [c for c in f1 if c.tag == "file"] + [
c for c in f2 if c.tag == "file"
]
subchildren = [c for c in f1 if c.tag == "file"] + [c for c in f2 if c.tag == "file"]
eq_(len(subchildren), 3)
@ -96,9 +94,7 @@ def test_SaveThenLoad():
def test_LoadXML_with_empty_file_tags():
f = io.BytesIO()
f.write(
b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>'
)
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)

View File

@ -117,9 +117,7 @@ class TestCaseResultsWithSomeGroups:
assert d is g.ref
def test_sort_groups(self):
self.results.make_ref(
self.objects[1]
) # We want to make the 1024 sized object to go ref.
self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref.
g1, g2 = self.groups
self.results.sort_groups("size")
assert self.results.groups[0] is g2
@ -129,9 +127,7 @@ class TestCaseResultsWithSomeGroups:
assert self.results.groups[1] is g2
def test_set_groups_when_sorted(self):
self.results.make_ref(
self.objects[1]
) # We want to make the 1024 sized object to go ref.
self.results.make_ref(self.objects[1]) # We want to make the 1024 sized object to go ref.
self.results.sort_groups("size")
objects, matches, groups = GetTestGroups()
g1, g2 = groups
@ -601,9 +597,7 @@ class TestCaseResultsXML:
matches = engine.getmatches(objects) # we should have 5 matches
groups = engine.get_groups(matches) # We should have 2 groups
for g in groups:
g.prioritize(
lambda x: objects.index(x)
) # We want the dupes to be in the same order as the list is
g.prioritize(lambda x: objects.index(x)) # We want the dupes to be in the same order as the list is
app = DupeGuru()
results = Results(app)
results.groups = groups
@ -807,9 +801,7 @@ class TestCaseResultsFilter:
# Now the stats should display *2* markable dupes (instead of 1)
expected = "0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo"
eq_(expected, self.results.stat_line)
self.results.apply_filter(
None
) # Now let's make sure our unfiltered results aren't fucked up
self.results.apply_filter(None) # Now let's make sure our unfiltered results aren't fucked up
expected = "0 / 3 (0.00 B / 3.00 B) duplicates marked."
eq_(expected, self.results.stat_line)

View File

@ -150,8 +150,7 @@ def test_big_file_partial_hashes(fake_fileexists):
bigsize = 100 * 1024 * 1024 # 100MB
s.big_file_size_threshold = bigsize
f = [no("bigfoo", bigsize), no("bigbar", bigsize),
no("smallfoo", smallsize), no("smallbar", smallsize)]
f = [no("bigfoo", bigsize), no("bigbar", bigsize), no("smallfoo", smallsize), no("smallbar", smallsize)]
f[0].md5 = f[0].md5partial = f[0].md5samples = "foobar"
f[1].md5 = f[1].md5partial = f[1].md5samples = "foobar"
f[2].md5 = f[2].md5partial = "bleh"
@ -193,10 +192,8 @@ def test_content_scan_doesnt_put_md5_in_words_at_the_end(fake_fileexists):
s = Scanner()
s.scan_type = ScanType.Contents
f = [no("foo"), no("bar")]
f[0].md5 = f[0].md5partial = f[0].md5samples =\
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[1].md5 = f[1].md5partial = f[1].md5samples =\
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[0].md5 = f[0].md5partial = f[0].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
f[1].md5 = f[1].md5partial = f[1].md5samples = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
r = s.get_dupe_groups(f)
# FIXME looks like we are missing something here?
r[0]

View File

@ -11,9 +11,7 @@ from setuptools import setup, Extension
def get_parser():
parser = argparse.ArgumentParser(description="Build an arbitrary Python extension.")
parser.add_argument(
"source_files", nargs="+", help="List of source files to compile"
)
parser.add_argument("source_files", nargs="+", help="List of source files to compile")
parser.add_argument("name", nargs=1, help="Name of the resulting extension")
return parser
@ -23,7 +21,8 @@ def main():
print("Building {}...".format(args.name[0]))
ext = Extension(args.name[0], args.source_files)
setup(
script_args=["build_ext", "--inplace"], ext_modules=[ext],
script_args=["build_ext", "--inplace"],
ext_modules=[ext],
)

View File

@ -48,15 +48,13 @@ def get_unconflicted_name(name):
def is_conflicted(name):
"""Returns whether ``name`` is prepended with a bracketed number.
"""
"""Returns whether ``name`` is prepended with a bracketed number."""
return re_conflict.match(name) is not None
@pathify
def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
"""Use move() or copy() to move and copy file with the conflict management.
"""
"""Use move() or copy() to move and copy file with the conflict management."""
if dest_path.isdir() and not source_path.isdir():
dest_path = dest_path[source_path.name]
if dest_path.exists():
@ -68,14 +66,12 @@ def _smart_move_or_copy(operation, source_path: Path, dest_path: Path):
def smart_move(source_path, dest_path):
"""Same as :func:`smart_copy`, but it moves files instead.
"""
"""Same as :func:`smart_copy`, but it moves files instead."""
_smart_move_or_copy(shutil.move, source_path, dest_path)
def smart_copy(source_path, dest_path):
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution.
"""
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution."""
try:
_smart_move_or_copy(shutil.copy, source_path, dest_path)
except IOError as e:

View File

@ -16,20 +16,17 @@ class SpecialFolder:
def open_url(url):
"""Open ``url`` with the default browser.
"""
"""Open ``url`` with the default browser."""
_open_url(url)
def open_path(path):
"""Open ``path`` with its associated application.
"""
"""Open ``path`` with its associated application."""
_open_path(str(path))
def reveal_path(path):
"""Open the folder containing ``path`` with the default file browser.
"""
"""Open the folder containing ``path`` with the default file browser."""
_reveal_path(str(path))

View File

@ -149,8 +149,7 @@ class Rect:
return l1, l2, l3, l4
def scaled_rect(self, dx, dy):
"""Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side.
"""
"""Returns a rect that has the same borders at self, but grown/shrunk by dx/dy on each side."""
x, y, w, h = self
x -= dx
y -= dy
@ -159,8 +158,7 @@ class Rect:
return Rect(x, y, w, h)
def united(self, other):
"""Returns the bounding rectangle of this rectangle and `other`.
"""
"""Returns the bounding rectangle of this rectangle and `other`."""
# ul=upper left lr=lower right
ulcorner1, lrcorner1 = self.corners()
ulcorner2, lrcorner2 = other.corners()

View File

@ -80,8 +80,7 @@ class PrefAccessInterface:
"""
def set_default(self, key, value):
"""Set the value ``value`` for ``key`` in the currently running app's preference store.
"""
"""Set the value ``value`` for ``key`` in the currently running app's preference store."""
class Columns(GUIObject):
@ -140,33 +139,27 @@ class Columns(GUIObject):
# --- Public
def column_by_index(self, index):
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.
"""
"""Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``."""
return self.column_list[index]
def column_by_name(self, name):
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``.
"""
"""Return the :class:`Column` having the :attr:`~Column.name` ``name``."""
return self.coldata[name]
def columns_count(self):
"""Returns the number of columns in our set.
"""
"""Returns the number of columns in our set."""
return len(self.column_list)
def column_display(self, colname):
"""Returns display name for column named ``colname``, or ``''`` if there's none.
"""
"""Returns display name for column named ``colname``, or ``''`` if there's none."""
return self._get_colname_attr(colname, "display", "")
def column_is_visible(self, colname):
"""Returns visibility for column named ``colname``, or ``True`` if there's none.
"""
"""Returns visibility for column named ``colname``, or ``True`` if there's none."""
return self._get_colname_attr(colname, "visible", True)
def column_width(self, colname):
"""Returns width for column named ``colname``, or ``0`` if there's none.
"""
"""Returns width for column named ``colname``, or ``0`` if there's none."""
return self._get_colname_attr(colname, "width", 0)
def columns_to_right(self, colname):
@ -177,11 +170,7 @@ class Columns(GUIObject):
"""
column = self.coldata[colname]
index = column.ordered_index
return [
col.name
for col in self.column_list
if (col.visible and col.ordered_index > index)
]
return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]
def menu_items(self):
"""Returns a list of items convenient for quick visibility menu generation.
@ -207,8 +196,7 @@ class Columns(GUIObject):
self.set_column_order(colnames)
def reset_to_defaults(self):
"""Reset all columns' width and visibility to their default values.
"""
"""Reset all columns' width and visibility to their default values."""
self.set_column_order([col.name for col in self.column_list])
for col in self._optional_columns():
col.visible = col.default_visible
@ -216,13 +204,11 @@ class Columns(GUIObject):
self.view.restore_columns()
def resize_column(self, colname, newwidth):
"""Set column ``colname``'s width to ``newwidth``.
"""
"""Set column ``colname``'s width to ``newwidth``."""
self._set_colname_attr(colname, "width", newwidth)
def restore_columns(self):
"""Restore's column persistent attributes from the last :meth:`save_columns`.
"""
"""Restore's column persistent attributes from the last :meth:`save_columns`."""
if not (self.prefaccess and self.savename and self.coldata):
if (not self.savename) and (self.coldata):
# This is a table that will not have its coldata saved/restored. we should
@ -241,8 +227,7 @@ class Columns(GUIObject):
self.view.restore_columns()
def save_columns(self):
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`.
"""
"""Save column attributes in persistent storage for restoration in :meth:`restore_columns`."""
if not (self.prefaccess and self.savename and self.coldata):
return
for col in self.column_list:
@ -263,15 +248,13 @@ class Columns(GUIObject):
col.ordered_index = i
def set_column_visible(self, colname, visible):
"""Set the visibility of column ``colname``.
"""
"""Set the visibility of column ``colname``."""
self.table.save_edits() # the table on the GUI side will stop editing when the columns change
self._set_colname_attr(colname, "visible", visible)
self.view.set_column_visible(colname, visible)
def set_default_width(self, colname, width):
"""Set the default width or column ``colname``.
"""
"""Set the default width or column ``colname``."""
self._set_colname_attr(colname, "default_width", width)
def toggle_menu_item(self, index):
@ -289,14 +272,10 @@ class Columns(GUIObject):
# --- Properties
@property
def ordered_columns(self):
"""List of :class:`Column` in visible order.
"""
return [
col for col in sorted(self.column_list, key=lambda col: col.ordered_index)
]
"""List of :class:`Column` in visible order."""
return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]
@property
def colnames(self):
"""List of column names in visible order.
"""
"""List of column names in visible order."""
return [col.name for col in self.ordered_columns]

View File

@ -21,12 +21,10 @@ class ProgressWindowView:
"""
def show(self):
"""Show the dialog.
"""
"""Show the dialog."""
def close(self):
"""Close the dialog.
"""
"""Close the dialog."""
def set_progress(self, progress):
"""Set the progress of the progress bar to ``progress``.
@ -76,8 +74,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
self.jobid = None
def cancel(self):
"""Call for a user-initiated job cancellation.
"""
"""Call for a user-initiated job cancellation."""
# The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to
# make sure that this doesn't lead us to think that the user acually cancelled the task, so
# we verify that the job is still running.

View File

@ -27,9 +27,7 @@ class Selectable(Sequence):
self._selected_indexes = []
if not self._selected_indexes:
return
self._selected_indexes = [
index for index in self._selected_indexes if index < len(self)
]
self._selected_indexes = [index for index in self._selected_indexes if index < len(self)]
if not self._selected_indexes:
self._selected_indexes = [len(self) - 1]

View File

@ -71,8 +71,7 @@ class TextField(GUIObject):
# --- Public
def refresh(self):
"""Triggers a view :meth:`~TextFieldView.refresh`.
"""
"""Triggers a view :meth:`~TextFieldView.refresh`."""
self.view.refresh()
@property

View File

@ -55,8 +55,7 @@ class Node(MutableSequence):
# --- Public
def clear(self):
"""Clears the node of all its children.
"""
"""Clears the node of all its children."""
del self[:]
def find(self, predicate, include_self=True):
@ -103,14 +102,12 @@ class Node(MutableSequence):
@property
def children_count(self):
"""Same as ``len(self)``.
"""
"""Same as ``len(self)``."""
return len(self)
@property
def name(self):
"""Name for the node, supplied on init.
"""
"""Name for the node, supplied on init."""
return self._name
@property

View File

@ -56,8 +56,7 @@ class Job:
# ---Private
def _subjob_callback(self, progress, desc=""):
"""This is the callback passed to children jobs.
"""
"""This is the callback passed to children jobs."""
self.set_progress(progress, desc)
return True # if JobCancelled has to be raised, it will be at the highest level

View File

@ -154,9 +154,7 @@ def strings2pot(target, dest):
def allstrings2pot(lprojpath, dest, excludes=None):
allstrings = files_with_ext(lprojpath, ".strings")
if excludes:
allstrings = [
p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes
]
allstrings = [p for p in allstrings if op.splitext(op.basename(p))[0] not in excludes]
for strings_path in allstrings:
strings2pot(strings_path, dest)
@ -195,11 +193,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
# genstrings produces utf-16 files with comments. After having generated the files, we convert
# them to utf-8 and remove the comments.
ensure_empty_folder(dest_folder)
print_and_do(
'genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(
dest_folder, code_folder
)
)
print_and_do('genstrings -o "{}" `find "{}" -name *.m | xargs`'.format(dest_folder, code_folder))
for stringsfile in os.listdir(dest_folder):
stringspath = op.join(dest_folder, stringsfile)
with open(stringspath, "rt", encoding="utf-16") as fp:
@ -214,9 +208,7 @@ def generate_cocoa_strings_from_code(code_folder, dest_folder):
def generate_cocoa_strings_from_xib(xib_folder):
xibs = [
op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")
]
xibs = [op.join(xib_folder, fn) for fn in os.listdir(xib_folder) if fn.endswith(".xib")]
for xib in xibs:
dest = xib.replace(".xib", ".strings")
print_and_do("ibtool {} --generate-strings-file {}".format(xib, dest))
@ -234,10 +226,6 @@ def localize_stringsfile(stringsfile, dest_root_folder):
def localize_all_stringsfiles(src_folder, dest_root_folder):
stringsfiles = [
op.join(src_folder, fn)
for fn in os.listdir(src_folder)
if fn.endswith(".strings")
]
stringsfiles = [op.join(src_folder, fn) for fn in os.listdir(src_folder) if fn.endswith(".strings")]
for path in stringsfiles:
localize_stringsfile(path, dest_root_folder)

View File

@ -16,8 +16,7 @@ from collections import defaultdict
class Broadcaster:
"""Broadcasts messages that are received by all listeners.
"""
"""Broadcasts messages that are received by all listeners."""
def __init__(self):
self.listeners = set()
@ -39,8 +38,7 @@ class Broadcaster:
class Listener:
"""A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected.
"""
"""A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected."""
def __init__(self, broadcaster):
self.broadcaster = broadcaster
@ -57,13 +55,11 @@ class Listener:
self._bound_notifications[message].append(func)
def connect(self):
"""Connects the listener to its broadcaster.
"""
"""Connects the listener to its broadcaster."""
self.broadcaster.add_listener(self)
def disconnect(self):
"""Disconnects the listener from its broadcaster.
"""
"""Disconnects the listener from its broadcaster."""
self.broadcaster.remove_listener(self)
def dispatch(self, msg):

View File

@ -85,9 +85,7 @@ class Path(tuple):
def __getitem__(self, key):
if isinstance(key, slice):
if isinstance(key.start, Path):
equal_elems = list(
takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start))
)
equal_elems = list(takewhile(lambda pair: pair[0] == pair[1], zip(self, key.start)))
key = slice(len(equal_elems), key.stop, key.step)
if isinstance(key.stop, Path):
equal_elems = list(
@ -226,9 +224,7 @@ def pathify(f):
Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``.
"""
sig = signature(f)
pindexes = {
i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path
}
pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path}
pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path}
def path_or_none(p):
@ -236,9 +232,7 @@ def pathify(f):
@wraps(f)
def wrapped(*args, **kwargs):
args = tuple(
(path_or_none(a) if i in pindexes else a) for i, a in enumerate(args)
)
args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args))
kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()}
return f(*args, **kwargs)
@ -246,8 +240,7 @@ def pathify(f):
def log_io_error(func):
""" Catches OSError, IOError and WindowsError and log them
"""
"""Catches OSError, IOError and WindowsError and log them"""
@wraps(func)
def wrapper(path, *args, **kwargs):

View File

@ -110,22 +110,14 @@ def _visit_pyfiles(list, dirname, names):
# get extension for python source files
if "_py_ext" not in globals():
global _py_ext
_py_ext = [
triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE
][0]
_py_ext = [triple[0] for triple in imp.get_suffixes() if triple[2] == imp.PY_SOURCE][0]
# don't recurse into CVS directories
if "CVS" in names:
names.remove("CVS")
# add all *.py files to list
list.extend(
[
os.path.join(dirname, file)
for file in names
if os.path.splitext(file)[1] == _py_ext
]
)
list.extend([os.path.join(dirname, file) for file in names if os.path.splitext(file)[1] == _py_ext])
def _get_modpkg_path(dotted_name, pathlist=None):
@ -406,8 +398,7 @@ def main(source_files, outpath, keywords=None):
eater(*_token)
except tokenize.TokenError as e:
print(
"%s: %s, line %d, column %d"
% (e.args[0], filename, e.args[1][0], e.args[1][1]),
"%s: %s, line %d, column %d" % (e.args[0], filename, e.args[1][0], e.args[1][1]),
file=sys.stderr,
)
finally:

View File

@ -22,9 +22,7 @@ def tixgen(tixurl):
"""This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder
for the tix #
"""
urlpattern = tixurl.format(
"\\1"
) # will be replaced buy the content of the first group in re
urlpattern = tixurl.format("\\1") # will be replaced buy the content of the first group in re
R = re.compile(r"#(\d+)")
repl = "`#\\1 <{}>`__".format(urlpattern)
return lambda text: R.sub(repl, text)
@ -61,9 +59,7 @@ def gen(
# The format of the changelog descriptions is in markdown, but since we only use bulled list
# and links, it's not worth depending on the markdown package. A simple regexp suffice.
description = re.sub(r"\[(.*?)\]\((.*?)\)", "`\\1 <\\2>`__", description)
rendered = CHANGELOG_FORMAT.format(
version=log["version"], date=log["date_str"], description=description
)
rendered = CHANGELOG_FORMAT.format(version=log["version"], date=log["date_str"], description=description)
rendered_logs.append(rendered)
confrepl["version"] = changelog[0]["version"]
changelog_out = op.join(basepath, "changelog.rst")
@ -75,6 +71,4 @@ def gen(
try:
sphinx_build([basepath, destpath])
except SystemExit:
print(
"Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit"
)
print("Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit")

View File

@ -31,8 +31,8 @@ class FakeCursor(list):
class _ActualThread(threading.Thread):
""" We can't use this class directly because thread object are not automatically freed when
nothing refers to it, making it hang the application if not explicitely closed.
"""We can't use this class directly because thread object are not automatically freed when
nothing refers to it, making it hang the application if not explicitely closed.
"""
def __init__(self, dbname, autocommit):

View File

@ -80,9 +80,7 @@ class TestCase_move_copy:
assert self.path["baz"].exists()
assert not self.path["foo"].exists()
def test_copy_no_conflict(
self, do_setup
): # No need to duplicate the rest of the tests... Let's just test on move
def test_copy_no_conflict(self, do_setup): # No need to duplicate the rest of the tests... Let's just test on move
smart_copy(self.path + "foo", self.path + "baz")
assert self.path["baz"].exists()
assert self.path["foo"].exists()

View File

@ -128,9 +128,7 @@ def test_repeater_with_repeated_notifications():
r.connect()
listener.connect()
b.notify("hello")
b.notify(
"foo"
) # if the repeater repeated this notif, we'd get a crash on HelloListener
b.notify("foo") # if the repeater repeated this notif, we'd get a crash on HelloListener
eq_(r.hello_count, 1)
eq_(listener.hello_count, 1)
eq_(r.foo_count, 1)

View File

@ -87,8 +87,7 @@ def test_filename(force_ossep):
def test_deal_with_empty_components(force_ossep):
"""Keep ONLY a leading space, which means we want a leading slash.
"""
"""Keep ONLY a leading space, which means we want a leading slash."""
eq_("foo//bar", str(Path(("foo", "", "bar"))))
eq_("/foo/bar", str(Path(("", "foo", "bar"))))
eq_("foo/bar", str(Path("foo/bar/")))
@ -154,8 +153,7 @@ def test_path_slice(force_ossep):
def test_add_with_root_path(force_ossep):
"""if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f
"""
"""if I perform /a/b/c + /d/e/f, I want /a/b/c/d/e/f, not /a/b/c//d/e/f"""
eq_("/foo/bar", str(Path("/foo") + Path("/bar")))
@ -166,8 +164,7 @@ def test_create_with_tuple_that_have_slash_inside(force_ossep, monkeypatch):
def test_auto_decode_os_sep(force_ossep, monkeypatch):
"""Path should decode any either / or os.sep, but always encode in os.sep.
"""
"""Path should decode any either / or os.sep, but always encode in os.sep."""
eq_(("foo\\bar", "bleh"), Path("foo\\bar/bleh"))
monkeypatch.setattr(os, "sep", "\\")
eq_(("foo", "bar/bleh"), Path("foo\\bar/bleh"))

View File

@ -44,9 +44,7 @@ def test_guicalls():
# A GUISelectableList appropriately calls its view.
sl = GUISelectableList(["foo", "bar"])
sl.view = CallLogger()
sl.view.check_gui_calls(
["refresh"]
) # Upon setting the view, we get a call to refresh()
sl.view.check_gui_calls(["refresh"]) # Upon setting the view, we get a call to refresh()
sl[1] = "baz"
sl.view.check_gui_calls(["refresh"])
sl.append("foo")

View File

@ -105,9 +105,7 @@ def test_findall_dont_include_self():
# When calling findall with include_self=False, the node itself is never evaluated.
t = tree_with_some_nodes()
del t._name # so that if the predicate is called on `t`, we crash
r = t.findall(
lambda n: not n.name.startswith("sub"), include_self=False
) # no crash
r = t.findall(lambda n: not n.name.startswith("sub"), include_self=False) # no crash
eq_(set(r), set([t[0], t[1], t[2]]))

View File

@ -105,9 +105,7 @@ def test_iterconsume():
# We just want to make sure that we return *all* items and that we're not mistakenly skipping
# one.
eq_(list(range(2500)), list(iterconsume(list(range(2500)))))
eq_(
list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False))
)
eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False)))
# --- String

View File

@ -86,9 +86,7 @@ class CallLogger:
eq_(set(self.calls), set(expected))
self.clear_calls()
def check_gui_calls_partial(
self, expected=None, not_expected=None, verify_order=False
):
def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False):
"""Checks that the expected calls have been made to 'self', then clears the log.
`expected` is an iterable of strings representing method names. Order doesn't matter.
@ -99,25 +97,17 @@ class CallLogger:
__tracebackhide__ = True
if expected is not None:
not_called = set(expected) - set(self.calls)
assert not not_called, "These calls haven't been made: {0}".format(
not_called
)
assert not not_called, "These calls haven't been made: {0}".format(not_called)
if verify_order:
max_index = 0
for call in expected:
index = self.calls.index(call)
if index < max_index:
raise AssertionError(
"The call {0} hasn't been made in the correct order".format(
call
)
)
raise AssertionError("The call {0} hasn't been made in the correct order".format(call))
max_index = index
if not_expected is not None:
called = set(not_expected) & set(self.calls)
assert not called, "These calls shouldn't have been made: {0}".format(
called
)
assert not called, "These calls shouldn't have been made: {0}".format(called)
self.clear_calls()
@ -193,27 +183,25 @@ def jointhreads():
def _unify_args(func, args, kwargs, args_to_ignore=None):
""" Unify args and kwargs in the same dictionary.
"""Unify args and kwargs in the same dictionary.
The result is kwargs with args added to it. func.func_code.co_varnames is used to determine
under what key each elements of arg will be mapped in kwargs.
The result is kwargs with args added to it. func.func_code.co_varnames is used to determine
under what key each elements of arg will be mapped in kwargs.
if you want some arguments not to be in the results, supply a list of arg names in
args_to_ignore.
if you want some arguments not to be in the results, supply a list of arg names in
args_to_ignore.
if f is a function that takes *args, func_code.co_varnames is empty, so args will be put
under 'args' in kwargs.
if f is a function that takes *args, func_code.co_varnames is empty, so args will be put
under 'args' in kwargs.
def foo(bar, baz)
_unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23}
_unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23}
def foo(bar, baz)
_unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23}
_unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23}
"""
result = kwargs.copy()
if hasattr(func, "__code__"): # built-in functions don't have func_code
args = list(args)
if (
getattr(func, "__self__", None) is not None
): # bound method, we have to add self to args list
if getattr(func, "__self__", None) is not None: # bound method, we have to add self to args list
args = [func.__self__] + args
defaults = list(func.__defaults__) if func.__defaults__ is not None else []
arg_count = func.__code__.co_argcount
@ -234,11 +222,11 @@ def _unify_args(func, args, kwargs, args_to_ignore=None):
def log_calls(func):
""" Logs all func calls' arguments under func.calls.
"""Logs all func calls' arguments under func.calls.
func.calls is a list of _unify_args() result (dict).
func.calls is a list of _unify_args() result (dict).
Mostly used for unit testing.
Mostly used for unit testing.
"""
def wrapper(*args, **kwargs):

View File

@ -110,9 +110,7 @@ def install_gettext_trans(base_folder, lang):
if not lang:
return lambda s: s
try:
return gettext.translation(
domain, localedir=base_folder, languages=[lang]
).gettext
return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext
except IOError:
return lambda s: s

View File

@ -19,8 +19,7 @@ from .path import Path, pathify, log_io_error
def nonone(value, replace_value):
"""Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise.
"""
"""Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise."""
if value is None:
return replace_value
else:
@ -28,8 +27,7 @@ def nonone(value, replace_value):
def tryint(value, default=0):
"""Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails.
"""
"""Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails."""
try:
return int(value)
except (TypeError, ValueError):
@ -37,8 +35,7 @@ def tryint(value, default=0):
def minmax(value, min_value, max_value):
"""Returns `value` or one of the min/max bounds if `value` is not between them.
"""
"""Returns `value` or one of the min/max bounds if `value` is not between them."""
return min(max(value, min_value), max_value)
@ -75,8 +72,7 @@ def flatten(iterables, start_with=None):
def first(iterable):
"""Returns the first item of ``iterable``.
"""
"""Returns the first item of ``iterable``."""
try:
return next(iter(iterable))
except StopIteration:
@ -84,14 +80,12 @@ def first(iterable):
def stripfalse(seq):
"""Returns a sequence with all false elements stripped out of seq.
"""
"""Returns a sequence with all false elements stripped out of seq."""
return [x for x in seq if x]
def extract(predicate, iterable):
"""Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both.
"""
"""Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both."""
wheat = []
shaft = []
for item in iterable:
@ -103,8 +97,7 @@ def extract(predicate, iterable):
def allsame(iterable):
"""Returns whether all elements of 'iterable' are the same.
"""
"""Returns whether all elements of 'iterable' are the same."""
it = iter(iterable)
try:
first_item = next(it)
@ -152,14 +145,12 @@ def iterconsume(seq, reverse=True):
def escape(s, to_escape, escape_with="\\"):
"""Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``.
"""
"""Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``."""
return "".join((escape_with + c if c in to_escape else c) for c in s)
def get_file_ext(filename):
"""Returns the lowercase extension part of filename, without the dot.
"""
"""Returns the lowercase extension part of filename, without the dot."""
pos = filename.rfind(".")
if pos > -1:
return filename[pos + 1 :].lower()
@ -168,8 +159,7 @@ def get_file_ext(filename):
def rem_file_ext(filename):
"""Returns the filename without extension.
"""
"""Returns the filename without extension."""
pos = filename.rfind(".")
if pos > -1:
return filename[:pos]
@ -217,8 +207,7 @@ def format_time(seconds, with_hours=True):
def format_time_decimal(seconds):
"""Transforms seconds in a strings like '3.4 minutes'.
"""
"""Transforms seconds in a strings like '3.4 minutes'."""
minus = seconds < 0
if minus:
seconds *= -1
@ -320,8 +309,7 @@ ONE_DAY = timedelta(1)
def iterdaterange(start, end):
"""Yields every day between ``start`` and ``end``.
"""
"""Yields every day between ``start`` and ``end``."""
date = start
while date <= end:
yield date
@ -365,8 +353,7 @@ def find_in_path(name, paths=None):
@log_io_error
@pathify
def delete_if_empty(path: Path, files_to_delete=[]):
"""Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.
"""
"""Deletes the directory at 'path' if it is empty or if it only contains files_to_delete."""
if not path.exists() or not path.isdir():
return
contents = path.listdir()
@ -411,8 +398,7 @@ def ensure_file(path):
def delete_files_with_pattern(folder_path, pattern, recursive=True):
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`.
"""
"""Delete all files (or folders) in `folder_path` that match the glob `pattern`."""
to_delete = glob.glob(op.join(folder_path, pattern))
for fn in to_delete:
if op.isdir(fn):

View File

@ -82,11 +82,7 @@ def package_debian_distribution(distribution):
copy(op.join(debskel, fn), op.join(debdest, fn))
filereplace(op.join(debskel, "control"), op.join(debdest, "control"), **debopts)
filereplace(op.join(debskel, "Makefile"), op.join(destpath, "Makefile"), **debopts)
filereplace(
op.join(debskel, "dupeguru.desktop"),
op.join(debdest, "dupeguru.desktop"),
**debopts
)
filereplace(op.join(debskel, "dupeguru.desktop"), op.join(debdest, "dupeguru.desktop"), **debopts)
changelogpath = op.join("help", "changelog")
changelog_dest = op.join(debdest, "changelog")
project_name = debopts["pkgname"]
@ -128,11 +124,7 @@ def package_arch():
copy_files_to_package(srcpath, packages, with_so=True)
shutil.copy(op.join("images", "dgse_logo_128.png"), srcpath)
debopts = json.load(open(op.join("pkg", "arch", "dupeguru.json")))
filereplace(
op.join("pkg", "arch", "dupeguru.desktop"),
op.join(srcpath, "dupeguru.desktop"),
**debopts
)
filereplace(op.join("pkg", "arch", "dupeguru.desktop"), op.join(srcpath, "dupeguru.desktop"), **debopts)
def package_source_txz():
@ -173,11 +165,7 @@ def package_windows():
version_info = version_template.read()
version_template.close()
version_info_file = open("win_version_info.txt", "w")
version_info_file.write(
version_info.format(
version_array[0], version_array[1], version_array[2], bits
)
)
version_info_file.write(version_info.format(version_array[0], version_array[1], version_array[2], bits))
version_info_file.close()
except Exception:
print("Error creating version info file, exiting...")
@ -195,9 +183,7 @@ def package_windows():
"--add-data=build/locale;locale",
"--add-data=build/help;help",
"--version-file=win_version_info.txt",
"--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(
arch
),
"--paths=C:\\Program Files (x86)\\Windows Kits\\10\\Redist\\ucrt\\DLLs\\{0}".format(arch),
"run.py",
]
)

View File

@ -6,19 +6,19 @@ import importlib
from setuptools import setup, Extension
sys.path.insert(1, op.abspath('src'))
sys.path.insert(1, op.abspath("src"))
from hscommon.build import move_all
exts = [
Extension("_block", [op.join('modules', 'block.c'), op.join('modules', 'common.c')]),
Extension("_cache", [op.join('modules', 'cache.c'), op.join('modules', 'common.c')]),
Extension("_block_qt", [op.join('modules', 'block_qt.c')]),
Extension("_block", [op.join("modules", "block.c"), op.join("modules", "common.c")]),
Extension("_cache", [op.join("modules", "cache.c"), op.join("modules", "common.c")]),
Extension("_block_qt", [op.join("modules", "block_qt.c")]),
]
setup(
script_args = ['build_ext', '--inplace'],
ext_modules = exts,
script_args=["build_ext", "--inplace"],
ext_modules=exts,
)
move_all('_block_qt*', op.join('src', 'qt', 'pe'))
move_all('_cache*', op.join('src', 'core/pe'))
move_all('_block*', op.join('src', 'core/pe'))
move_all("_block_qt*", op.join("src", "qt", "pe"))
move_all("_cache*", op.join("src", "core/pe"))
move_all("_block*", op.join("src", "core/pe"))

View File

@ -65,18 +65,10 @@ class DupeGuru(QObject):
self.recentResults.mustOpenItem.connect(self.model.load_from)
self.resultWindow = None
if self.use_tabs:
self.main_window = (
TabBarWindow(self)
if not self.prefs.tabs_default_pos
else TabWindow(self)
)
self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self)
parent_window = self.main_window
self.directories_dialog = self.main_window.createPage(
"DirectoriesDialog", app=self
)
self.main_window.addTab(
self.directories_dialog, tr("Directories"), switch=False
)
self.directories_dialog = self.main_window.createPage("DirectoriesDialog", app=self)
self.main_window.addTab(self.directories_dialog, tr("Directories"), switch=False)
self.actionDirectoriesWindow.setEnabled(False)
else: # floating windows only
self.main_window = None
@ -84,9 +76,7 @@ class DupeGuru(QObject):
parent_window = self.directories_dialog
self.progress_window = ProgressWindow(parent_window, self.model.progress_window)
self.problemDialog = ProblemDialog(
parent=parent_window, model=self.model.problem_dialog
)
self.problemDialog = ProblemDialog(parent=parent_window, model=self.model.problem_dialog)
if self.use_tabs:
self.ignoreListDialog = self.main_window.createPage(
"IgnoreListDialog",
@ -101,16 +91,10 @@ class DupeGuru(QObject):
model=self.model.exclude_list_dialog,
)
else:
self.ignoreListDialog = IgnoreListDialog(
parent=parent_window, model=self.model.ignore_list_dialog
)
self.excludeDialog = ExcludeListDialog(
app=self, parent=parent_window, model=self.model.exclude_list_dialog
)
self.ignoreListDialog = IgnoreListDialog(parent=parent_window, model=self.model.ignore_list_dialog)
self.excludeDialog = ExcludeListDialog(app=self, parent=parent_window, model=self.model.exclude_list_dialog)
self.deletionOptions = DeletionOptions(
parent=parent_window, model=self.model.deletion_options
)
self.deletionOptions = DeletionOptions(parent=parent_window, model=self.model.deletion_options)
self.about_box = AboutBox(parent_window, self)
parent_window.show()
@ -174,25 +158,19 @@ class DupeGuru(QObject):
self.model.options["mix_file_kind"] = self.prefs.mix_file_kind
self.model.options["escape_filter_regexp"] = not self.prefs.use_regexp
self.model.options["clean_empty_dirs"] = self.prefs.remove_empty_folders
self.model.options[
"ignore_hardlink_matches"
] = self.prefs.ignore_hardlink_matches
self.model.options["ignore_hardlink_matches"] = self.prefs.ignore_hardlink_matches
self.model.options["copymove_dest_type"] = self.prefs.destination_type
self.model.options["scan_type"] = self.prefs.get_scan_type(self.model.app_mode)
self.model.options["min_match_percentage"] = self.prefs.filter_hardness
self.model.options["word_weighting"] = self.prefs.word_weighting
self.model.options["match_similar_words"] = self.prefs.match_similar
threshold = (
self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
)
self.model.options["size_threshold"] = (
threshold * 1024
) # threshold is in KB. The scanner wants bytes
big_file_size_threshold = (
self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0
)
threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0
self.model.options["size_threshold"] = threshold * 1024 # threshold is in KB. The scanner wants bytes
big_file_size_threshold = self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0
self.model.options["big_file_size_threshold"] = (
big_file_size_threshold * 1024 * 1024
big_file_size_threshold
* 1024
* 1024
# threshold is in MiB. The scanner wants bytes
)
scanned_tags = set()
@ -259,9 +237,7 @@ class DupeGuru(QObject):
if self.resultWindow is not None:
if self.use_tabs:
if self.main_window.indexOfWidget(self.resultWindow) < 0:
self.main_window.addTab(
self.resultWindow, tr("Results"), switch=True
)
self.main_window.addTab(self.resultWindow, tr("Results"), switch=True)
return
self.main_window.showTab(self.resultWindow)
else:
@ -318,9 +294,7 @@ class DupeGuru(QObject):
def excludeListTriggered(self):
if self.use_tabs:
self.showTriggeredTabbedDialog(
self.excludeListDialog, tr("Exclusion Filters")
)
self.showTriggeredTabbedDialog(self.excludeListDialog, tr("Exclusion Filters"))
else: # floating windows
self.model.exclude_list_dialog.show()
@ -328,9 +302,7 @@ class DupeGuru(QObject):
"""Add tab for dialog, name the tab with desc_string, then show it."""
index = self.main_window.indexOfWidget(dialog)
# Create the tab if it doesn't exist already
if (
index < 0
): # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
if index < 0: # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):
index = self.main_window.addTab(dialog, desc_string, switch=True)
# Show the tab for that widget
self.main_window.setCurrentIndex(index)
@ -402,13 +374,9 @@ class DupeGuru(QObject):
if self.resultWindow is not None:
self.resultWindow.close()
# This is better for tabs, as it takes care of duplicate items in menu bar
self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(
None
)
self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(None)
if self.use_tabs:
self.resultWindow = self.main_window.createPage(
"ResultWindow", parent=self.main_window, app=self
)
self.resultWindow = self.main_window.createPage("ResultWindow", parent=self.main_window, app=self)
else: # We don't use a tab widget, regular floating QMainWindow
self.resultWindow = ResultWindow(self.directories_dialog, self)
self.directories_dialog._updateActionsState()
@ -426,9 +394,7 @@ class DupeGuru(QObject):
def select_dest_file(self, prompt, extension):
files = tr("{} file (*.{})").format(extension.upper(), extension)
destination, chosen_filter = QFileDialog.getSaveFileName(
self.resultWindow, prompt, "", files
)
destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, "", files)
if not destination.endswith(".{}".format(extension)):
destination = "{}.{}".format(destination, extension)
return destination

View File

@ -42,9 +42,7 @@ class DeletionOptions(QDialog):
self.linkMessageLabel = QLabel(text)
self.linkMessageLabel.setWordWrap(True)
self.verticalLayout.addWidget(self.linkMessageLabel)
self.linkTypeRadio = RadioBox(
items=[tr("Symlink"), tr("Hardlink")], spread=False
)
self.linkTypeRadio = RadioBox(items=[tr("Symlink"), tr("Hardlink")], spread=False)
self.verticalLayout.addWidget(self.linkTypeRadio)
if not self.model.supports_links():
self.linkCheckbox.setEnabled(False)

View File

@ -31,8 +31,7 @@ class DetailsDialog(QDockWidget):
self.model.view = self
self.app.willSavePrefs.connect(self.appWillSavePrefs)
# self.setAttribute(Qt.WA_DeleteOnClose)
parent.addDockWidget(
area if self._wasDocked else Qt.BottomDockWidgetArea, self)
parent.addDockWidget(area if self._wasDocked else Qt.BottomDockWidgetArea, self)
def _setupUi(self): # Virtual
pass

View File

@ -34,9 +34,11 @@ class DetailsModel(QAbstractTableModel):
row = index.row()
ignored_fields = ["Dupe Count"]
if (self.model.row(row)[0] in ignored_fields
or self.model.row(row)[1] == "---"
or self.model.row(row)[2] == "---"):
if (
self.model.row(row)[0] in ignored_fields
or self.model.row(row)[1] == "---"
or self.model.row(row)[2] == "---"
):
if role != Qt.DisplayRole:
return None
return self.model.row(row)[column]
@ -52,17 +54,9 @@ class DetailsModel(QAbstractTableModel):
return None # QVariant()
def headerData(self, section, orientation, role):
if (
orientation == Qt.Horizontal
and role == Qt.DisplayRole
and section < len(HEADER)
):
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER):
return HEADER[section]
elif (
orientation == Qt.Vertical
and role == Qt.DisplayRole
and section < self.model.row_count()
):
elif orientation == Qt.Vertical and role == Qt.DisplayRole and section < self.model.row_count():
# Read "Attribute" cell for horizontal header
return self.model.row(section)[0]
return None

View File

@ -45,9 +45,7 @@ class DirectoriesDialog(QMainWindow):
self.recentFolders = Recent(self.app, "recentFolders")
self._setupUi()
self._updateScanTypeList()
self.directoriesModel = DirectoriesModel(
self.app.model.directory_tree, view=self.treeView
)
self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView)
self.directoriesDelegate = DirectoriesDelegate()
self.treeView.setItemDelegate(self.directoriesDelegate)
self._setupColumns()
@ -170,9 +168,7 @@ class DirectoriesDialog(QMainWindow):
label = QLabel(tr("Application Mode:"), self)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label)
self.appModeRadioBox = RadioBox(
self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False
)
self.appModeRadioBox = RadioBox(self, items=[tr("Standard"), tr("Music"), tr("Picture")], spread=False)
hl.addWidget(self.appModeRadioBox)
self.verticalLayout.addLayout(hl)
hl = QHBoxLayout()
@ -181,27 +177,21 @@ class DirectoriesDialog(QMainWindow):
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(label)
self.scanTypeComboBox = QComboBox(self)
self.scanTypeComboBox.setSizePolicy(
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
)
self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
self.scanTypeComboBox.setMaximumWidth(400)
hl.addWidget(self.scanTypeComboBox)
self.showPreferencesButton = QPushButton(tr("More Options"), self.centralwidget)
self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
hl.addWidget(self.showPreferencesButton)
self.verticalLayout.addLayout(hl)
self.promptLabel = QLabel(
tr('Select folders to scan and press "Scan".'), self.centralwidget
)
self.promptLabel = QLabel(tr('Select folders to scan and press "Scan".'), self.centralwidget)
self.verticalLayout.addWidget(self.promptLabel)
self.treeView = QTreeView(self.centralwidget)
self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.treeView.setAcceptDrops(True)
triggers = (
QAbstractItemView.DoubleClicked
| QAbstractItemView.EditKeyPressed
| QAbstractItemView.SelectedClicked
QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked
)
self.treeView.setEditTriggers(triggers)
self.treeView.setDragDropOverwriteMode(True)
@ -267,9 +257,7 @@ class DirectoriesDialog(QMainWindow):
def _updateScanTypeList(self):
try:
self.scanTypeComboBox.currentIndexChanged[int].disconnect(
self.scanTypeChanged
)
self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged)
except TypeError:
# Not connected, ignore
pass
@ -299,9 +287,7 @@ class DirectoriesDialog(QMainWindow):
def addFolderTriggered(self):
title = tr("Select a folder to add to the scanning list")
flags = QFileDialog.ShowDirsOnly
dirpath = str(
QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags)
)
dirpath = str(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags))
if not dirpath:
return
self.lastAddedFolder = dirpath
@ -362,9 +348,7 @@ class DirectoriesDialog(QMainWindow):
def scanTypeChanged(self, index):
scan_options = self.app.model.SCANNER_CLASS.get_scan_options()
self.app.prefs.set_scan_type(
self.app.model.app_mode, scan_options[index].scan_type
)
self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type)
self.app._update_options()
def selectionChanged(self, selected, deselected):

View File

@ -44,9 +44,7 @@ class DirectoriesDelegate(QStyledItemDelegate):
# On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to
# fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright.
cboption.state |= QStyle.State_Enabled
QApplication.style().drawComplexControl(
QStyle.CC_ComboBox, cboption, painter
)
QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter)
painter.setBrush(option.palette.text())
rect = QRect(option.rect)
rect.setLeft(rect.left() + 4)
@ -75,9 +73,7 @@ class DirectoriesModel(TreeModel):
self.view = view
self.view.setModel(self)
self.view.selectionModel().selectionChanged[
(QItemSelection, QItemSelection)
].connect(self.selectionChanged)
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
def _createNode(self, ref, row):
return RefNode(self, None, ref, row)
@ -155,10 +151,7 @@ class DirectoriesModel(TreeModel):
# --- Events
def selectionChanged(self, selected, deselected):
newNodes = [
modelIndex.internalPointer().ref
for modelIndex in self.view.selectionModel().selectedRows()
]
newNodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]
self.model.selected_nodes = newNodes
# --- Signals

View File

@ -5,13 +5,22 @@
import re
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import (
QPushButton, QLineEdit, QVBoxLayout, QGridLayout, QDialog,
QTableView, QAbstractItemView, QSpacerItem, QSizePolicy, QHeaderView
QPushButton,
QLineEdit,
QVBoxLayout,
QGridLayout,
QDialog,
QTableView,
QAbstractItemView,
QSpacerItem,
QSizePolicy,
QHeaderView,
)
from .exclude_list_table import ExcludeListTable
from core.exclude import AlreadyThereException
from hscommon.trans import trget
tr = trget("ui")
@ -51,9 +60,7 @@ class ExcludeListDialog(QDialog):
self.testLine = QLineEdit()
self.tableView = QTableView()
triggers = (
QAbstractItemView.DoubleClicked
| QAbstractItemView.EditKeyPressed
| QAbstractItemView.SelectedClicked
QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked
)
self.tableView.setEditTriggers(triggers)
self.tableView.setSelectionMode(QTableView.ExtendedSelection)
@ -150,7 +157,9 @@ class ExcludeListDialog(QDialog):
self.table.refresh()
def display_help_message(self):
self.app.show_message(tr("""\
self.app.show_message(
tr(
"""\
These (case sensitive) python regular expressions will filter out files during scans.<br>\
Directores will also have their <strong>default state</strong> set to Excluded \
in the Directories tab if their name happens to match one of the selected regular expressions.<br>\
@ -163,4 +172,6 @@ You can test the regular expression with the "test string" button after pasting
<code>C:\\\\User\\My Pictures\\test.png</code><br><br>
Matching regular expressions will be highlighted.<br>\
If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>\
Directories and files starting with a period '.' are filtered out by default.<br><br>"""))
Directories and files starting with a period '.' are filtered out by default.<br><br>"""
)
)

View File

@ -8,15 +8,14 @@ from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor
from qtlib.column import Column
from qtlib.table import Table
from hscommon.trans import trget
tr = trget("ui")
class ExcludeListTable(Table):
"""Model for exclude list"""
COLUMNS = [
Column("marked", defaultWidth=15),
Column("regex", defaultWidth=230)
]
COLUMNS = [Column("marked", defaultWidth=15), Column("regex", defaultWidth=230)]
def __init__(self, app, view, **kwargs):
model = app.model.exclude_list_dialog.exclude_list_table # pointer to GUITable

View File

@ -56,9 +56,7 @@ class IgnoreListDialog(QDialog):
self.clearButton = QPushButton(tr("Clear"))
self.closeButton = QPushButton(tr("Close"))
self.verticalLayout.addLayout(
horizontalWrap(
[self.removeSelectedButton, self.clearButton, None, self.closeButton]
)
horizontalWrap([self.removeSelectedButton, self.clearButton, None, self.closeButton])
)
# --- model --> view

View File

@ -10,7 +10,7 @@ from qtlib.table import Table
class IgnoreListTable(Table):
""" Ignore list model"""
"""Ignore list model"""
COLUMNS = [
Column("path1", defaultWidth=230),

View File

@ -59,13 +59,9 @@ class PreferencesDialog(PreferencesDialogBase):
self.widgetsVLayout.addWidget(self.matchSimilarBox)
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
self.widgetsVLayout.addWidget(self.mixFileKindBox)
self._setupAddCheckbox(
"useRegexpBox", tr("Use regular expressions when filtering")
)
self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"))
self.widgetsVLayout.addWidget(self.useRegexpBox)
self._setupAddCheckbox(
"removeEmptyFoldersBox", tr("Remove empty folders on delete or move")
)
self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move"))
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
self._setupAddCheckbox(
"ignoreHardlinkMatches",

View File

@ -5,14 +5,13 @@
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame)
from PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame
from PyQt5.QtGui import QResizeEvent
from hscommon.trans import trget
from ..details_dialog import DetailsDialog as DetailsDialogBase
from ..details_table import DetailsTable
from .image_viewer import (
ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController)
from .image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController
tr = trget("ui")
@ -70,8 +69,7 @@ class DetailsDialog(DetailsDialogBase):
self.splitter.addWidget(self.tableView)
self.splitter.setStretchFactor(1, 1)
# Late population needed here for connections to the toolbar
self.vController.setupViewers(
self.selectedImageViewer, self.referenceImageViewer)
self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer)
# self.setCentralWidget(self.splitter) # only as QMainWindow
self.setWidget(self.splitter) # only as QDockWidget
@ -103,11 +101,11 @@ class DetailsDialog(DetailsDialogBase):
# Give the splitter a maximum height to reach. This is assuming that
# all rows below their headers have the same height
self.tableView.setMaximumHeight(
self.tableView.rowHeight(1)
* self.tableModel.model.row_count()
self.tableView.rowHeight(1) * self.tableModel.model.row_count()
+ self.tableView.verticalHeader().sectionSize(0)
# looks like the handle is taken into account by the splitter
+ self.splitter.handle(1).size().height())
+ self.splitter.handle(1).size().height()
)
DetailsDialogBase.show(self)
self.ensure_same_sizes()
self._update()
@ -138,6 +136,7 @@ class DetailsDialog(DetailsDialogBase):
class EmittingFrame(QFrame):
"""Emits a signal whenever is resized"""
resized = pyqtSignal(QResizeEvent)
def resizeEvent(self, event):

View File

@ -2,15 +2,24 @@
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from PyQt5.QtCore import (
QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent)
from PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent
from PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence
from PyQt5.QtWidgets import (
QGraphicsView, QGraphicsScene, QGraphicsPixmapItem,
QToolBar, QToolButton, QAction, QWidget, QScrollArea,
QApplication, QAbstractScrollArea, QStyle)
QGraphicsView,
QGraphicsScene,
QGraphicsPixmapItem,
QToolBar,
QToolButton,
QAction,
QWidget,
QScrollArea,
QApplication,
QAbstractScrollArea,
QStyle,
)
from hscommon.trans import trget
from hscommon.plat import ISLINUX
tr = trget("ui")
MAX_SCALE = 12.0
@ -50,8 +59,7 @@ class ViewerToolBar(QToolBar):
"actionZoomIn",
QKeySequence.ZoomIn,
QIcon.fromTheme("zoom-in")
if ISLINUX
and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_in")),
tr("Increase zoom"),
controller.zoomIn,
@ -60,8 +68,7 @@ class ViewerToolBar(QToolBar):
"actionZoomOut",
QKeySequence.ZoomOut,
QIcon.fromTheme("zoom-out")
if ISLINUX
and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_out")),
tr("Decrease zoom"),
controller.zoomOut,
@ -70,8 +77,7 @@ class ViewerToolBar(QToolBar):
"actionNormalSize",
tr("Ctrl+/"),
QIcon.fromTheme("zoom-original")
if ISLINUX
and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_original")),
tr("Normal size"),
controller.zoomNormalSize,
@ -80,12 +86,11 @@ class ViewerToolBar(QToolBar):
"actionBestFit",
tr("Ctrl+*"),
QIcon.fromTheme("zoom-best-fit")
if ISLINUX
and not self.parent.app.prefs.details_dialog_override_theme_icons
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "zoom_best_fit")),
tr("Best fit"),
controller.zoomBestFit,
)
),
]
# TODO try with QWidgetAction() instead in order to have
# the popup menu work in the toolbar (if resized below minimum height)
@ -95,13 +100,12 @@ class ViewerToolBar(QToolBar):
self.buttonImgSwap = QToolButton(self)
self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.buttonImgSwap.setIcon(
QIcon.fromTheme('view-refresh',
self.style().standardIcon(QStyle.SP_BrowserReload))
if ISLINUX
and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "exchange")))
self.buttonImgSwap.setText('Swap images')
self.buttonImgSwap.setToolTip('Swap images')
QIcon.fromTheme("view-refresh", self.style().standardIcon(QStyle.SP_BrowserReload))
if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons
else QIcon(QPixmap(":/" + "exchange"))
)
self.buttonImgSwap.setText("Swap images")
self.buttonImgSwap.setToolTip("Swap images")
self.buttonImgSwap.pressed.connect(self.controller.swapImages)
self.buttonImgSwap.released.connect(self.controller.swapImages)
@ -207,11 +211,11 @@ class BaseController(QObject):
# than the ReferenceImageViewer by one pixel, which distorts the
# scaled down pixmap for the reference, hence we'll reuse its size here.
selected_size = self._updateImage(
self.selectedPixmap, self.scaledSelectedPixmap,
self.selectedViewer, None, same_group)
self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, same_group
)
self._updateImage(
self.referencePixmap, self.scaledReferencePixmap,
self.referenceViewer, selected_size, same_group)
self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, selected_size, same_group
)
if ignore_update:
self.selectedViewer.ignore_signal = False
@ -229,12 +233,10 @@ class BaseController(QObject):
return target_size
# zoomed in state, expand
# only if not same_group, we need full update
scaledpixmap = pixmap.scaled(
target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)
else:
# best fit, keep ratio always
scaledpixmap = pixmap.scaled(
target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation)
viewer.setImage(scaledpixmap)
return target_size
@ -347,12 +349,8 @@ class BaseController(QObject):
self.selectedViewer.resetCenter()
self.referenceViewer.resetCenter()
target_size = self._updateImage(
self.selectedPixmap, self.scaledSelectedPixmap,
self.selectedViewer, None, True)
self._updateImage(
self.referencePixmap, self.scaledReferencePixmap,
self.referenceViewer, target_size, True)
target_size = self._updateImage(self.selectedPixmap, self.scaledSelectedPixmap, self.selectedViewer, None, True)
self._updateImage(self.referencePixmap, self.scaledReferencePixmap, self.referenceViewer, target_size, True)
self.centerViews()
self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)
@ -402,6 +400,7 @@ class BaseController(QObject):
class QWidgetController(BaseController):
"""Specialized version for QWidget-based viewers."""
def __init__(self, parent):
super().__init__(parent)
@ -430,6 +429,7 @@ class QWidgetController(BaseController):
class ScrollAreaController(BaseController):
"""Specialized version fro QLabel-based viewers."""
def __init__(self, parent):
super().__init__(parent)
@ -442,10 +442,8 @@ class ScrollAreaController(BaseController):
super().updateBothImages(same_group)
if not self.referenceViewer.isEnabled():
return
self.referenceViewer._horizontalScrollBar.setValue(
self.selectedViewer._horizontalScrollBar.value())
self.referenceViewer._verticalScrollBar.setValue(
self.selectedViewer._verticalScrollBar.value())
self.referenceViewer._horizontalScrollBar.setValue(self.selectedViewer._horizontalScrollBar.value())
self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value())
@pyqtSlot(QPoint)
def onDraggedMouse(self, delta):
@ -518,6 +516,7 @@ class ScrollAreaController(BaseController):
class GraphicsViewController(BaseController):
"""Specialized version fro QGraphicsView-based viewers."""
def __init__(self, parent):
super().__init__(parent)
@ -625,10 +624,8 @@ class GraphicsViewController(BaseController):
if ignore_update:
self.selectedViewer.ignore_signal = True
self._updateFitImage(
self.selectedPixmap, self.selectedViewer)
self._updateFitImage(
self.referencePixmap, self.referenceViewer)
self._updateFitImage(self.selectedPixmap, self.selectedViewer)
self._updateFitImage(self.referencePixmap, self.referenceViewer)
if ignore_update:
self.selectedViewer.ignore_signal = False
@ -699,6 +696,7 @@ class GraphicsViewController(BaseController):
class QWidgetImageViewer(QWidget):
"""Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation."""
# FIXME: panning while zoomed-in is broken (due to delta not interpolated right?
mouseDragged = pyqtSignal(QPointF)
mouseWheeled = pyqtSignal(float)
@ -720,15 +718,13 @@ class QWidgetImageViewer(QWidget):
self.setMouseTracking(False)
def __repr__(self):
return f'{self._instance_name}'
return f"{self._instance_name}"
def connectMouseSignals(self):
if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect(
self.controller.onDraggedMouse)
self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)
if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect(
self.controller.scaleImagesBy)
self._wheelConnection = self.mouseWheeled.connect(self.controller.scaleImagesBy)
def disconnectMouseSignals(self):
if self._dragConnection:
@ -746,7 +742,7 @@ class QWidgetImageViewer(QWidget):
painter.drawPixmap(self._rect.topLeft(), self._pixmap)
def resetCenter(self):
""" Resets origin """
"""Resets origin"""
# Make sure we are not still panning around
self._mousePanningDelta = QPointF()
self.update()
@ -783,8 +779,7 @@ class QWidgetImageViewer(QWidget):
event.ignore()
return
self._mousePanningDelta += (
event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale
self._lastMouseClickPoint = event.pos()
if self._drag:
self.mouseDragged.emit(self._mousePanningDelta)
@ -860,6 +855,7 @@ class QWidgetImageViewer(QWidget):
class ScalablePixmap(QWidget):
"""Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer."""
def __init__(self, parent):
super().__init__(parent)
self._pixmap = QPixmap()
@ -881,6 +877,7 @@ class ScalablePixmap(QWidget):
class ScrollAreaImageViewer(QScrollArea):
"""Implementation using a pixmap container in a simple scroll area."""
mouseDragged = pyqtSignal(QPoint)
mouseWheeled = pyqtSignal(float, QPointF)
@ -921,7 +918,7 @@ class ScrollAreaImageViewer(QScrollArea):
self.setVisible(True)
def __repr__(self):
return f'{self._instance_name}'
return f"{self._instance_name}"
def toggleScrollBars(self, forceOn=False):
if not self.prefs.details_dialog_viewers_show_scrollbars:
@ -938,11 +935,9 @@ class ScrollAreaImageViewer(QScrollArea):
def connectMouseSignals(self):
if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect(
self.controller.onDraggedMouse)
self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)
if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect(
self.controller.onMouseWheel)
self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)
def disconnectMouseSignals(self):
if self._dragConnection:
@ -955,10 +950,8 @@ class ScrollAreaImageViewer(QScrollArea):
def connectScrollBars(self):
"""Only call once controller is connected."""
# Cyclic connections are handled by Qt
self._verticalScrollBar.valueChanged.connect(
self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)
def contextMenuEvent(self, event):
"""Block parent's (main window) context menu on right click."""
@ -987,7 +980,7 @@ class ScrollAreaImageViewer(QScrollArea):
event.ignore()
return
if self._drag:
delta = (event.pos() - self._lastMouseClickPoint)
delta = event.pos() - self._lastMouseClickPoint
self._lastMouseClickPoint = event.pos()
self.mouseDragged.emit(delta)
super().mouseMoveEvent(event)
@ -1064,35 +1057,29 @@ class ScrollAreaImageViewer(QScrollArea):
"""After scaling, no mouse position, default to center."""
# scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())
self._horizontalScrollBar.setValue(
int(factor * self._horizontalScrollBar.value()
+ ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)))
int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))
)
self._verticalScrollBar.setValue(
int(factor * self._verticalScrollBar.value()
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2)))
int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))
)
def adjustScrollBarsScaled(self, delta):
"""After scaling with the mouse, update relative to mouse position."""
self._horizontalScrollBar.setValue(
self._horizontalScrollBar.value() + delta.x())
self._verticalScrollBar.setValue(
self._verticalScrollBar.value() + delta.y())
self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())
self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())
def adjustScrollBarsAuto(self):
"""After panning, update accordingly."""
self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - self._mousePanningDelta.y())
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())
def adjustScrollBarCentered(self):
"""Just center in the middle."""
self._horizontalScrollBar.setValue(
int(self._horizontalScrollBar.maximum() / 2))
self._verticalScrollBar.setValue(
int(self._verticalScrollBar.maximum() / 2))
self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.maximum() / 2))
self._verticalScrollBar.setValue(int(self._verticalScrollBar.maximum() / 2))
def resetCenter(self):
""" Resets origin """
"""Resets origin"""
self._mousePanningDelta = QPoint()
self.current_scale = 1.0
# self.scaleAt(1.0)
@ -1127,6 +1114,7 @@ class ScrollAreaImageViewer(QScrollArea):
class GraphicsViewViewer(QGraphicsView):
"""Re-Implementation a full-fledged GraphicsView but is a bit buggy."""
mouseDragged = pyqtSignal()
mouseWheeled = pyqtSignal(float, QPointF)
@ -1178,11 +1166,9 @@ class GraphicsViewViewer(QGraphicsView):
def connectMouseSignals(self):
if not self._dragConnection:
self._dragConnection = self.mouseDragged.connect(
self.controller.syncCenters)
self._dragConnection = self.mouseDragged.connect(self.controller.syncCenters)
if not self._wheelConnection:
self._wheelConnection = self.mouseWheeled.connect(
self.controller.onMouseWheel)
self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)
def disconnectMouseSignals(self):
if self._dragConnection:
@ -1195,10 +1181,8 @@ class GraphicsViewViewer(QGraphicsView):
def connectScrollBars(self):
"""Only call once controller is connected."""
# Cyclic connections are handled by Qt
self._verticalScrollBar.valueChanged.connect(
self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(
self.controller.onHScrollBarChanged, Qt.UniqueConnection)
self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)
self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)
def toggleScrollBars(self, forceOn=False):
if not self.prefs.details_dialog_viewers_show_scrollbars:
@ -1298,7 +1282,7 @@ class GraphicsViewViewer(QGraphicsView):
self.centerOn(self._centerPoint)
def resetCenter(self):
""" Resets origin """
"""Resets origin"""
self._mousePanningDelta = QPointF()
self.current_scale = 1.0
@ -1345,10 +1329,8 @@ class GraphicsViewViewer(QGraphicsView):
def adjustScrollBarsScaled(self, delta):
"""After scaling with the mouse, update relative to mouse position."""
self._horizontalScrollBar.setValue(
self._horizontalScrollBar.value() + delta.x())
self._verticalScrollBar.setValue(
self._verticalScrollBar.value() + delta.y())
self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())
self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())
def sizeHint(self):
return self.viewport().rect().size()
@ -1356,15 +1338,13 @@ class GraphicsViewViewer(QGraphicsView):
def adjustScrollBarsFactor(self, factor):
"""After scaling, no mouse position, default to center."""
self._horizontalScrollBar.setValue(
int(factor * self._horizontalScrollBar.value()
+ ((factor - 1) * self._horizontalScrollBar.pageStep() / 2)))
int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))
)
self._verticalScrollBar.setValue(
int(factor * self._verticalScrollBar.value()
+ ((factor - 1) * self._verticalScrollBar.pageStep() / 2)))
int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))
)
def adjustScrollBarsAuto(self):
"""After panning, update accordingly."""
self.horizontalScrollBar().setValue(
self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.verticalScrollBar().setValue(
self.verticalScrollBar().value() - self._mousePanningDelta.y())
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())
self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())

View File

@ -30,13 +30,15 @@ class File(PhotoBase):
image = QImage(str(self.path))
image = image.convertToFormat(QImage.Format_RGB888)
if type(orientation) == str:
logging.warning("Orientation for file '%s' was a str '%s', not an int.",
str(self.path), orientation)
logging.warning("Orientation for file '%s' was a str '%s', not an int.", str(self.path), orientation)
try:
orientation = int(orientation)
except Exception as e:
logging.exception("Skipping transformation because could not \
convert str to int. %s", e)
logging.exception(
"Skipping transformation because could not \
convert str to int. %s",
e,
)
return getblocks(image, block_count_per_side)
# MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for
# duplicate scanning. The transforms seems to work fine (if I try to save the image after

View File

@ -21,19 +21,13 @@ class PreferencesDialog(PreferencesDialogBase):
def _setupPreferenceWidgets(self):
self._setupFilterHardnessBox()
self.widgetsVLayout.addLayout(self.filterHardnessHLayout)
self._setupAddCheckbox(
"matchScaledBox", tr("Match pictures of different dimensions")
)
self._setupAddCheckbox("matchScaledBox", tr("Match pictures of different dimensions"))
self.widgetsVLayout.addWidget(self.matchScaledBox)
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"))
self.widgetsVLayout.addWidget(self.mixFileKindBox)
self._setupAddCheckbox(
"useRegexpBox", tr("Use regular expressions when filtering")
)
self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"))
self.widgetsVLayout.addWidget(self.useRegexpBox)
self._setupAddCheckbox(
"removeEmptyFoldersBox", tr("Remove empty folders on delete or move")
)
self._setupAddCheckbox("removeEmptyFoldersBox", tr("Remove empty folders on delete or move"))
self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)
self._setupAddCheckbox(
"ignoreHardlinkMatches",
@ -52,45 +46,37 @@ class PreferencesDialog(PreferencesDialogBase):
def _setupDisplayPage(self):
super()._setupDisplayPage()
self._setupAddCheckbox("details_dialog_override_theme_icons",
tr("Override theme icons in viewer toolbar"))
self._setupAddCheckbox("details_dialog_override_theme_icons", tr("Override theme icons in viewer toolbar"))
self.details_dialog_override_theme_icons.setToolTip(
tr("Use our own internal icons instead of those provided by the theme engine"))
tr("Use our own internal icons instead of those provided by the theme engine")
)
# Prevent changing this on platforms where themes are unpredictable
self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True)
# Insert this right after the vertical title bar option
index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar)
self.details_groupbox_layout.insertWidget(
index + 1, self.details_dialog_override_theme_icons)
self._setupAddCheckbox("details_dialog_viewers_show_scrollbars",
tr("Show scrollbars in image viewers"))
self.details_groupbox_layout.insertWidget(index + 1, self.details_dialog_override_theme_icons)
self._setupAddCheckbox("details_dialog_viewers_show_scrollbars", tr("Show scrollbars in image viewers"))
self.details_dialog_viewers_show_scrollbars.setToolTip(
tr("When the image displayed doesn't fit the viewport, \
show scrollbars to span the view around"))
self.details_groupbox_layout.insertWidget(
index + 2, self.details_dialog_viewers_show_scrollbars)
tr(
"When the image displayed doesn't fit the viewport, \
show scrollbars to span the view around"
)
)
self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)
def _load(self, prefs, setchecked, section):
setchecked(self.matchScaledBox, prefs.match_scaled)
self.cacheTypeRadio.selected_index = (
1 if prefs.picture_cache_type == "shelve" else 0
)
self.cacheTypeRadio.selected_index = 1 if prefs.picture_cache_type == "shelve" else 0
# Update UI state based on selected scan type
scan_type = prefs.get_scan_type(AppMode.Picture)
fuzzy_scan = scan_type == ScanType.FuzzyBlock
self.filterHardnessSlider.setEnabled(fuzzy_scan)
setchecked(self.details_dialog_override_theme_icons,
prefs.details_dialog_override_theme_icons)
setchecked(self.details_dialog_viewers_show_scrollbars,
prefs.details_dialog_viewers_show_scrollbars)
setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons)
setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)
def _save(self, prefs, ischecked):
prefs.match_scaled = ischecked(self.matchScaledBox)
prefs.picture_cache_type = (
"shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
)
prefs.details_dialog_override_theme_icons =\
ischecked(self.details_dialog_override_theme_icons)
prefs.details_dialog_viewers_show_scrollbars =\
ischecked(self.details_dialog_viewers_show_scrollbars)
prefs.picture_cache_type = "shelve" if self.cacheTypeRadio.selected_index == 1 else "sqlite"
prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons)
prefs.details_dialog_viewers_show_scrollbars = ischecked(self.details_dialog_viewers_show_scrollbars)

View File

@ -20,9 +20,7 @@ class Preferences(PreferencesBase):
get = self.get_value
self.filter_hardness = get("FilterHardness", self.filter_hardness)
self.mix_file_kind = get("MixFileKind", self.mix_file_kind)
self.ignore_hardlink_matches = get(
"IgnoreHardlinkMatches", self.ignore_hardlink_matches
)
self.ignore_hardlink_matches = get("IgnoreHardlinkMatches", self.ignore_hardlink_matches)
self.use_regexp = get("UseRegexp", self.use_regexp)
self.remove_empty_folders = get("RemoveEmptyFolders", self.remove_empty_folders)
self.debug_mode = get("DebugMode", self.debug_mode)
@ -34,37 +32,36 @@ class Preferences(PreferencesBase):
self.tableFontSize = get("TableFontSize", self.tableFontSize)
self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font)
self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled",
self.details_dialog_titlebar_enabled)
self.details_dialog_vertical_titlebar = get("DetailsDialogVerticalTitleBar",
self.details_dialog_vertical_titlebar)
self.details_dialog_titlebar_enabled = get("DetailsDialogTitleBarEnabled", self.details_dialog_titlebar_enabled)
self.details_dialog_vertical_titlebar = get(
"DetailsDialogVerticalTitleBar", self.details_dialog_vertical_titlebar
)
# On Windows and MacOS, use internal icons by default
self.details_dialog_override_theme_icons =\
get("DetailsDialogOverrideThemeIcons",
self.details_dialog_override_theme_icons) if ISLINUX else True
self.details_table_delta_foreground_color =\
get("DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color)
self.details_dialog_viewers_show_scrollbars =\
get("DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars)
self.result_table_ref_foreground_color =\
get("ResultTableRefForegroundColor", self.result_table_ref_foreground_color)
self.result_table_ref_background_color =\
get("ResultTableRefBackgroundColor", self.result_table_ref_background_color)
self.result_table_delta_foreground_color =\
get("ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color)
self.resultWindowIsMaximized = get(
"ResultWindowIsMaximized", self.resultWindowIsMaximized
self.details_dialog_override_theme_icons = (
get("DetailsDialogOverrideThemeIcons", self.details_dialog_override_theme_icons) if ISLINUX else True
)
self.details_table_delta_foreground_color = get(
"DetailsTableDeltaForegroundColor", self.details_table_delta_foreground_color
)
self.details_dialog_viewers_show_scrollbars = get(
"DetailsDialogViewersShowScrollbars", self.details_dialog_viewers_show_scrollbars
)
self.result_table_ref_foreground_color = get(
"ResultTableRefForegroundColor", self.result_table_ref_foreground_color
)
self.result_table_ref_background_color = get(
"ResultTableRefBackgroundColor", self.result_table_ref_background_color
)
self.result_table_delta_foreground_color = get(
"ResultTableDeltaForegroundColor", self.result_table_delta_foreground_color
)
self.resultWindowIsMaximized = get("ResultWindowIsMaximized", self.resultWindowIsMaximized)
self.resultWindowRect = self.get_rect("ResultWindowRect", self.resultWindowRect)
self.mainWindowIsMaximized = get(
"MainWindowIsMaximized", self.mainWindowIsMaximized
)
self.mainWindowIsMaximized = get("MainWindowIsMaximized", self.mainWindowIsMaximized)
self.mainWindowRect = self.get_rect("MainWindowRect", self.mainWindowRect)
self.directoriesWindowRect = self.get_rect(
"DirectoriesWindowRect", self.directoriesWindowRect
)
self.directoriesWindowRect = self.get_rect("DirectoriesWindowRect", self.directoriesWindowRect)
self.recentResults = get("RecentResults", self.recentResults)
self.recentFolders = get("RecentFolders", self.recentFolders)

View File

@ -79,12 +79,8 @@ class PrioritizeDialog(QDialog):
super().__init__(parent, flags, **kwargs)
self._setupUi()
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.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
)
@ -112,12 +108,8 @@ class PrioritizeDialog(QDialog):
self.categoryCombobox = QComboBox()
self.criteriaListView = QListView()
self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.addCriteriaButton = QPushButton(
self.style().standardIcon(QStyle.SP_ArrowRight), ""
)
self.removeCriteriaButton = QPushButton(
self.style().standardIcon(QStyle.SP_ArrowLeft), ""
)
self.addCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowRight), "")
self.removeCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowLeft), "")
self.prioritizationListView = QListView()
self.prioritizationListView.setAcceptDrops(True)
self.prioritizationListView.setDragEnabled(True)

View File

@ -295,9 +295,7 @@ class ResultWindow(QMainWindow):
if menu.actions():
menu.clear()
self._column_actions = []
for index, (display, visible) in enumerate(
self.app.model.result_table.columns.menu_items()
):
for index, (display, visible) in enumerate(self.app.model.result_table.columns.menu_items()):
action = menu.addAction(display)
action.setCheckable(True)
action.setChecked(visible)

View File

@ -34,15 +34,11 @@ class PreferencesDialog(PreferencesDialogBase):
self.verticalLayout_4 = QVBoxLayout(self.widget)
self._setupAddCheckbox("wordWeightingBox", tr("Word weighting"), self.widget)
self.verticalLayout_4.addWidget(self.wordWeightingBox)
self._setupAddCheckbox(
"matchSimilarBox", tr("Match similar words"), self.widget
)
self._setupAddCheckbox("matchSimilarBox", tr("Match similar words"), self.widget)
self.verticalLayout_4.addWidget(self.matchSimilarBox)
self._setupAddCheckbox("mixFileKindBox", tr("Can mix file kind"), self.widget)
self.verticalLayout_4.addWidget(self.mixFileKindBox)
self._setupAddCheckbox(
"useRegexpBox", tr("Use regular expressions when filtering"), self.widget
)
self._setupAddCheckbox("useRegexpBox", tr("Use regular expressions when filtering"), self.widget)
self.verticalLayout_4.addWidget(self.useRegexpBox)
self._setupAddCheckbox(
"removeEmptyFoldersBox",
@ -51,17 +47,13 @@ class PreferencesDialog(PreferencesDialogBase):
)
self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox)
self.horizontalLayout_2 = QHBoxLayout()
self._setupAddCheckbox(
"ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget
)
self._setupAddCheckbox("ignoreSmallFilesBox", tr("Ignore files smaller than"), self.widget)
self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)
self.sizeThresholdSpinBox = QSpinBox(self.widget)
sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())
self.sizeThresholdSpinBox.setSizePolicy(sizePolicy)
self.sizeThresholdSpinBox.setMaximumSize(QSize(100, 16777215))
self.sizeThresholdSpinBox.setRange(0, 1000000)
@ -96,9 +88,7 @@ class PreferencesDialog(PreferencesDialogBase):
self.widget,
)
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.widgetsVLayout.addWidget(self.widget)
self._setupBottomPart()

View File

@ -19,6 +19,7 @@ from .directories_dialog import DirectoriesDialog
from .result_window import ResultWindow
from .ignore_list_dialog import IgnoreListDialog
from .exclude_list_dialog import ExcludeListDialog
tr = trget("ui")
@ -135,16 +136,15 @@ class TabWindow(QMainWindow):
action.setEnabled(True)
self.app.directories_dialog.actionShowResultsWindow.setEnabled(
False if page_type == "ResultWindow"
else self.app.resultWindow is not None)
False if page_type == "ResultWindow" else self.app.resultWindow is not None
)
self.app.actionIgnoreList.setEnabled(
True if self.app.ignoreListDialog is not None
and not page_type == "IgnoreListDialog" else False)
self.app.actionDirectoriesWindow.setEnabled(
False if page_type == "DirectoriesDialog" else True)
True if self.app.ignoreListDialog is not None and not page_type == "IgnoreListDialog" else False
)
self.app.actionDirectoriesWindow.setEnabled(False if page_type == "DirectoriesDialog" else True)
self.app.actionExcludeList.setEnabled(
True if self.app.excludeListDialog is not None
and not page_type == "ExcludeListDialog" else False)
True if self.app.excludeListDialog is not None and not page_type == "ExcludeListDialog" else False
)
self.previous_widget_actions = active_widget.specific_actions
self.last_index = current_index
@ -176,8 +176,7 @@ class TabWindow(QMainWindow):
index = self.tabWidget.addTab(page, title)
# index = self.tabWidget.insertTab(-1, page, title)
if isinstance(page, DirectoriesDialog):
self.tabWidget.tabBar().setTabButton(
index, QTabBar.RightSide, None)
self.tabWidget.tabBar().setTabButton(index, QTabBar.RightSide, None)
if switch:
self.setCurrentIndex(index)
return index
@ -250,6 +249,7 @@ class TabWindow(QMainWindow):
class TabBarWindow(TabWindow):
"""Implementation which uses a separate QTabBar and QStackedWidget.
The Tab bar is placed next to the menu bar to save real estate."""
def __init__(self, app, **kwargs):
super().__init__(app, **kwargs)
@ -286,8 +286,7 @@ class TabBarWindow(TabWindow):
self.tabBar.insertTab(stack_index, title)
if isinstance(page, DirectoriesDialog):
self.tabBar.setTabButton(
stack_index, QTabBar.RightSide, None)
self.tabBar.setTabButton(stack_index, QTabBar.RightSide, None)
if switch: # switch to the added tab immediately upon creation
self.setTabIndex(stack_index)
return stack_index

View File

@ -25,12 +25,7 @@ tr = trget("qtlib")
class AboutBox(QDialog):
def __init__(self, parent, app, **kwargs):
flags = (
Qt.CustomizeWindowHint
| Qt.WindowTitleHint
| Qt.WindowSystemMenuHint
| Qt.MSWindowsFixedSizeDialogHint
)
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
super().__init__(parent, flags, **kwargs)
self.app = app
self._setupUi()
@ -39,9 +34,7 @@ class AboutBox(QDialog):
self.buttonBox.rejected.connect(self.reject)
def _setupUi(self):
self.setWindowTitle(
tr("About {}").format(QCoreApplication.instance().applicationName())
)
self.setWindowTitle(tr("About {}").format(QCoreApplication.instance().applicationName()))
self.resize(400, 290)
sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@ -61,9 +54,7 @@ class AboutBox(QDialog):
self.nameLabel.setText(QCoreApplication.instance().applicationName())
self.verticalLayout.addWidget(self.nameLabel)
self.versionLabel = QLabel(self)
self.versionLabel.setText(
tr("Version {}").format(QCoreApplication.instance().applicationVersion())
)
self.versionLabel.setText(tr("Version {}").format(QCoreApplication.instance().applicationVersion()))
self.verticalLayout.addWidget(self.versionLabel)
self.label_3 = QLabel(self)
self.verticalLayout.addWidget(self.label_3)

View File

@ -62,9 +62,7 @@ class Columns:
# See moneyguru #14 and #15. This was added in order to allow automatic resizing of columns.
for column in self.model.column_list:
if column.resizeToFit:
self._headerView.setSectionResizeMode(
column.logical_index, QHeaderView.ResizeToContents
)
self._headerView.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)
# --- Public
def setColumnsWidth(self, widths):

View File

@ -34,9 +34,7 @@ class ErrorReportDialog(QDialog):
self._setupUi()
name = QCoreApplication.applicationName()
version = QCoreApplication.applicationVersion()
errorText = "Application Name: {}\nVersion: {}\n\n{}".format(
name, version, error
)
errorText = "Application Name: {}\nVersion: {}\n\n{}".format(name, version, error)
# Under windows, we end up with an error report without linesep if we don't mangle it
errorText = errorText.replace("\n", os.linesep)
self.errorTextEdit.setPlainText(errorText)

View File

@ -102,20 +102,14 @@ class SearchEdit(ClearableEdit):
if not bool(self.text()) and self.inactiveText and not self.hasFocus():
panel = QStyleOptionFrame()
self.initStyleOption(panel)
textRect = self.style().subElementRect(
QStyle.SE_LineEditContents, panel, self
)
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
leftMargin = 2
rightMargin = self._clearButton.iconSize().width()
textRect.adjust(leftMargin, 0, -rightMargin, 0)
painter = QPainter(self)
disabledColor = (
self.palette().brush(QPalette.Disabled, QPalette.Text).color()
)
disabledColor = self.palette().brush(QPalette.Disabled, QPalette.Text).color()
painter.setPen(disabledColor)
painter.drawText(
textRect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText
)
painter.drawText(textRect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText)
# --- Event Handlers
def _returnPressed(self):
@ -123,6 +117,4 @@ class SearchEdit(ClearableEdit):
self.searchChanged.emit()
# --- Signals
searchChanged = (
pyqtSignal()
) # Emitted when return is pressed or when the test is cleared
searchChanged = pyqtSignal() # Emitted when return is pressed or when the test is cleared

View File

@ -76,15 +76,11 @@ class ComboboxModel(SelectableList):
class ListviewModel(SelectableList):
def __init__(self, model, view, **kwargs):
super().__init__(model, view, **kwargs)
self.view.selectionModel().selectionChanged[
(QItemSelection, QItemSelection)
].connect(self.selectionChanged)
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
# --- Override
def _updateSelection(self):
newIndexes = [
modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()
]
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
if newIndexes != self.model.selected_indexes:
self.model.select(newIndexes)
@ -92,14 +88,10 @@ class ListviewModel(SelectableList):
newSelection = QItemSelection()
for index in self.model.selected_indexes:
newSelection.select(self.createIndex(index, 0), self.createIndex(index, 0))
self.view.selectionModel().select(
newSelection, QItemSelectionModel.ClearAndSelect
)
self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect)
if len(newSelection.indexes()):
currentIndex = newSelection.indexes()[0]
self.view.selectionModel().setCurrentIndex(
currentIndex, QItemSelectionModel.Current
)
self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current)
self.view.scrollTo(currentIndex)
# --- Events

View File

@ -29,22 +29,16 @@ class Table(QAbstractTableModel):
self.view.setModel(self)
self.model.view = self
if hasattr(self.model, "columns"):
self.columns = Columns(
self.model.columns, self.COLUMNS, view.horizontalHeader()
)
self.columns = Columns(self.model.columns, self.COLUMNS, view.horizontalHeader())
self.view.selectionModel().selectionChanged[
(QItemSelection, QItemSelection)
].connect(self.selectionChanged)
self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)
def _updateModelSelection(self):
# Takes the selection on the view's side and update the model with it.
# an _updateViewSelection() call will normally result in an _updateModelSelection() call.
# to avoid infinite loops, we check that the selection will actually change before calling
# model.select()
newIndexes = [
modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()
]
newIndexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]
if newIndexes != self.model.selected_indexes:
self.model.select(newIndexes)
@ -53,17 +47,11 @@ class Table(QAbstractTableModel):
newSelection = QItemSelection()
columnCount = self.columnCount(QModelIndex())
for index in self.model.selected_indexes:
newSelection.select(
self.createIndex(index, 0), self.createIndex(index, columnCount - 1)
)
self.view.selectionModel().select(
newSelection, QItemSelectionModel.ClearAndSelect
)
newSelection.select(self.createIndex(index, 0), self.createIndex(index, columnCount - 1))
self.view.selectionModel().select(newSelection, QItemSelectionModel.ClearAndSelect)
if len(newSelection.indexes()):
currentIndex = newSelection.indexes()[0]
self.view.selectionModel().setCurrentIndex(
currentIndex, QItemSelectionModel.Current
)
self.view.selectionModel().setCurrentIndex(currentIndex, QItemSelectionModel.Current)
self.view.scrollTo(currentIndex)
# --- Data Model methods

View File

@ -84,9 +84,7 @@ class DummyNode(TreeNode):
class TreeModel(QAbstractItemModel, NodeContainer):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._dummyNodes = (
set()
) # dummy nodes' reference have to be kept to avoid segfault
self._dummyNodes = set() # dummy nodes' reference have to be kept to avoid segfault
# --- Private
def _createDummyNode(self, parent, row):
@ -98,8 +96,7 @@ class TreeModel(QAbstractItemModel, NodeContainer):
return DummyNode(self, parent, row)
def _lastIndex(self):
"""Index of the very last item in the tree.
"""
"""Index of the very last item in the tree."""
currentIndex = QModelIndex()
rowCount = self.rowCount(currentIndex)
while rowCount > 0: