mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-03-13 03:51:38 +00:00
Compare commits
20 Commits
4.1.1
...
0db66baace
| Author | SHA1 | Date | |
|---|---|---|---|
| 0db66baace | |||
| e3828ae2ca | |||
|
|
23c59787e5 | ||
| 2f8d603251 | |||
|
|
a51f263632 | ||
| 4641bd6ec9 | |||
|
|
a6f83ad3d7 | ||
|
|
ab8750eedb | ||
|
|
22033211d6 | ||
| 0b46ca2222 | |||
| 72e0f76242 | |||
|
|
65c1d463f8 | ||
| e6c791ab0a | |||
|
|
78f5088101 | ||
|
|
095df5eb95 | ||
|
|
f1ae478433 | ||
|
|
c4dcfd3d4b | ||
| 0840104edf | |||
|
|
6b4b436251 | ||
|
|
d18b8c10ec |
@@ -770,6 +770,8 @@ class DupeGuru(Broadcaster):
|
|||||||
for group in self.results.groups:
|
for group in self.results.groups:
|
||||||
if group.prioritize(key_func=sort_key):
|
if group.prioritize(key_func=sort_key):
|
||||||
count += 1
|
count += 1
|
||||||
|
if count:
|
||||||
|
self.results.refresh_required = True
|
||||||
self._results_changed()
|
self._results_changed()
|
||||||
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(
|
msg = tr("{} duplicate groups were changed by the re-prioritization.").format(
|
||||||
count
|
count
|
||||||
|
|||||||
@@ -108,18 +108,9 @@ class Directories:
|
|||||||
found_files = []
|
found_files = []
|
||||||
# print(f"len of files: {len(files)} {files}")
|
# print(f"len of files: {len(files)} {files}")
|
||||||
for f in files:
|
for f in files:
|
||||||
found = False
|
if not self._exclude_list.is_excluded(root, f):
|
||||||
for expr in self._exclude_list.compiled_files:
|
found_files.append(fs.get_file(rootPath + f,
|
||||||
if expr.match(f):
|
fileclasses=fileclasses))
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
for expr in self._exclude_list.compiled_paths:
|
|
||||||
if expr.match(root + os.sep + f):
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
found_files.append(fs.get_file(rootPath + f, fileclasses=fileclasses))
|
|
||||||
found_files = [f for f in found_files if f is not None]
|
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
|
# 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
|
# why we have this line below. In fact, there only one case: Bundle files under
|
||||||
|
|||||||
@@ -26,8 +26,19 @@ def getwords(s):
|
|||||||
# We decompose the string so that ascii letters with accents can be part of the word.
|
# We decompose the string so that ascii letters with accents can be part of the word.
|
||||||
s = normalize("NFD", s)
|
s = normalize("NFD", s)
|
||||||
s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", " ").lower()
|
s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", " ").lower()
|
||||||
|
# logging.debug(f"DEBUG chars for: {s}\n"
|
||||||
|
# f"{[c for c in s if ord(c) != 32]}\n"
|
||||||
|
# f"{[ord(c) for c in s if ord(c) != 32]}")
|
||||||
|
# HACK We shouldn't ignore non-ascii characters altogether. Any Unicode char
|
||||||
|
# above common european characters that cannot be "sanitized" (ie. stripped
|
||||||
|
# of their accents, etc.) are preserved as is. The arbitrary limit is
|
||||||
|
# obtained from this one: ord("\u037e") GREEK QUESTION MARK
|
||||||
s = "".join(
|
s = "".join(
|
||||||
c for c in s if c in string.ascii_letters + string.digits + string.whitespace
|
c for c in s
|
||||||
|
if (ord(c) <= 894
|
||||||
|
and c in string.ascii_letters + string.digits + string.whitespace
|
||||||
|
)
|
||||||
|
or ord(c) > 894
|
||||||
)
|
)
|
||||||
return [_f for _f in s.split(" ") if _f] # remove empty elements
|
return [_f for _f in s.split(" ") if _f] # remove empty elements
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class ExcludeList(Markable):
|
|||||||
yield self.is_marked(regex), regex
|
yield self.is_marked(regex), regex
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return self.isExcluded(item)
|
return self.has_entry(item)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""Returns the total number of regexes regardless of mark status."""
|
"""Returns the total number of regexes regardless of mark status."""
|
||||||
@@ -173,7 +173,9 @@ class ExcludeList(Markable):
|
|||||||
[x for x in self._excluded_compiled if not has_sep(x.pattern)]
|
[x for x in self._excluded_compiled if not has_sep(x.pattern)]
|
||||||
self._cached_compiled_paths =\
|
self._cached_compiled_paths =\
|
||||||
[x for x in self._excluded_compiled if has_sep(x.pattern)]
|
[x for x in self._excluded_compiled if has_sep(x.pattern)]
|
||||||
|
self._dirty = False
|
||||||
return
|
return
|
||||||
|
|
||||||
marked_count = [x for marked, x in self if marked]
|
marked_count = [x for marked, x in self if marked]
|
||||||
# If there is no item, the compiled Pattern will be '' and match everything!
|
# If there is no item, the compiled Pattern will be '' and match everything!
|
||||||
if not marked_count:
|
if not marked_count:
|
||||||
@@ -197,14 +199,14 @@ class ExcludeList(Markable):
|
|||||||
else:
|
else:
|
||||||
self._cached_compiled_union_paths =\
|
self._cached_compiled_union_paths =\
|
||||||
(re.compile('|'.join(paths_marked)),)
|
(re.compile('|'.join(paths_marked)),)
|
||||||
|
self._dirty = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def compiled(self):
|
def compiled(self):
|
||||||
"""Should be used by other classes to retrieve the up-to-date list of patterns."""
|
"""Should be used by other classes to retrieve the up-to-date list of patterns."""
|
||||||
if self._use_union:
|
if self._use_union:
|
||||||
if self._dirty:
|
if self._dirty:
|
||||||
self.build_compiled_caches(True)
|
self.build_compiled_caches(self._use_union)
|
||||||
self._dirty = False
|
|
||||||
return self._cached_compiled_union_all
|
return self._cached_compiled_union_all
|
||||||
return self._excluded_compiled
|
return self._excluded_compiled
|
||||||
|
|
||||||
@@ -215,8 +217,7 @@ class ExcludeList(Markable):
|
|||||||
The interface should be expected to be a generator, even if it returns only
|
The interface should be expected to be a generator, even if it returns only
|
||||||
one item (one Pattern in the union case)."""
|
one item (one Pattern in the union case)."""
|
||||||
if self._dirty:
|
if self._dirty:
|
||||||
self.build_compiled_caches(True if self._use_union else False)
|
self.build_compiled_caches(self._use_union)
|
||||||
self._dirty = False
|
|
||||||
return self._cached_compiled_union_files if self._use_union\
|
return self._cached_compiled_union_files if self._use_union\
|
||||||
else self._cached_compiled_files
|
else self._cached_compiled_files
|
||||||
|
|
||||||
@@ -224,8 +225,7 @@ class ExcludeList(Markable):
|
|||||||
def compiled_paths(self):
|
def compiled_paths(self):
|
||||||
"""Returns patterns with only separators in them, for more precise filtering."""
|
"""Returns patterns with only separators in them, for more precise filtering."""
|
||||||
if self._dirty:
|
if self._dirty:
|
||||||
self.build_compiled_caches(True if self._use_union else False)
|
self.build_compiled_caches(self._use_union)
|
||||||
self._dirty = False
|
|
||||||
return self._cached_compiled_union_paths if self._use_union\
|
return self._cached_compiled_union_paths if self._use_union\
|
||||||
else self._cached_compiled_paths
|
else self._cached_compiled_paths
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ class ExcludeList(Markable):
|
|||||||
def add(self, regex, forced=False):
|
def add(self, regex, forced=False):
|
||||||
"""This interface should throw exceptions if there is an error during
|
"""This interface should throw exceptions if there is an error during
|
||||||
regex compilation"""
|
regex compilation"""
|
||||||
if self.isExcluded(regex):
|
if self.has_entry(regex):
|
||||||
# This exception should never be ignored
|
# This exception should never be ignored
|
||||||
raise AlreadyThereException()
|
raise AlreadyThereException()
|
||||||
if regex in forbidden_regexes:
|
if regex in forbidden_regexes:
|
||||||
@@ -256,12 +256,27 @@ class ExcludeList(Markable):
|
|||||||
"""Returns the number of marked regexes only."""
|
"""Returns the number of marked regexes only."""
|
||||||
return len([x for marked, x in self if marked])
|
return len([x for marked, x in self if marked])
|
||||||
|
|
||||||
def isExcluded(self, regex):
|
def has_entry(self, regex):
|
||||||
for item in self._excluded:
|
for item in self._excluded:
|
||||||
if regex == item[0]:
|
if regex == item[0]:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def is_excluded(self, dirname, filename):
|
||||||
|
"""Return True if the file or the absolute path to file is supposed to be
|
||||||
|
filtered out, False otherwise."""
|
||||||
|
matched = False
|
||||||
|
for expr in self.compiled_files:
|
||||||
|
if expr.fullmatch(filename):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if not matched:
|
||||||
|
for expr in self.compiled_paths:
|
||||||
|
if expr.fullmatch(dirname + sep + filename):
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
return matched
|
||||||
|
|
||||||
def remove(self, regex):
|
def remove(self, regex):
|
||||||
for item in self._excluded:
|
for item in self._excluded:
|
||||||
if item[0] == regex:
|
if item[0] == regex:
|
||||||
@@ -286,7 +301,9 @@ class ExcludeList(Markable):
|
|||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
return
|
return
|
||||||
if is_compilable and was_marked:
|
if is_compilable:
|
||||||
|
self._add_compiled(newregex)
|
||||||
|
if was_marked:
|
||||||
# Not marked by default when added, add it back
|
# Not marked by default when added, add it back
|
||||||
self.mark(newregex)
|
self.mark(newregex)
|
||||||
|
|
||||||
@@ -300,7 +317,7 @@ class ExcludeList(Markable):
|
|||||||
if regex not in default_regexes:
|
if regex not in default_regexes:
|
||||||
self.unmark(regex)
|
self.unmark(regex)
|
||||||
for default_regex in default_regexes:
|
for default_regex in default_regexes:
|
||||||
if not self.isExcluded(default_regex):
|
if not self.has_entry(default_regex):
|
||||||
self.add(default_regex)
|
self.add(default_regex)
|
||||||
self.mark(default_regex)
|
self.mark(default_regex)
|
||||||
|
|
||||||
@@ -399,9 +416,9 @@ class ExcludeDict(ExcludeList):
|
|||||||
if self._use_union:
|
if self._use_union:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self._excluded_compiled.add(self._excluded[regex]["compiled"])
|
self._excluded_compiled.add(self._excluded.get(regex).get("compiled"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Exception while adding regex {regex} to compiled set: {e}")
|
logging.error(f"Exception while adding regex {regex} to compiled set: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def is_compilable(self, regex):
|
def is_compilable(self, regex):
|
||||||
@@ -425,7 +442,7 @@ class ExcludeDict(ExcludeList):
|
|||||||
"compiled": compiled
|
"compiled": compiled
|
||||||
}
|
}
|
||||||
|
|
||||||
def isExcluded(self, regex):
|
def has_entry(self, regex):
|
||||||
if regex in self._excluded.keys():
|
if regex in self._excluded.keys():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -451,13 +468,15 @@ class ExcludeDict(ExcludeList):
|
|||||||
previous = self._excluded.pop(regex)
|
previous = self._excluded.pop(regex)
|
||||||
iscompilable, error, compiled = self.compile_re(newregex)
|
iscompilable, error, compiled = self.compile_re(newregex)
|
||||||
self._excluded[newregex] = {
|
self._excluded[newregex] = {
|
||||||
"index": previous["index"],
|
"index": previous.get('index'),
|
||||||
"compilable": iscompilable,
|
"compilable": iscompilable,
|
||||||
"error": error,
|
"error": error,
|
||||||
"compiled": compiled
|
"compiled": compiled
|
||||||
}
|
}
|
||||||
self._remove_compiled(regex)
|
self._remove_compiled(regex)
|
||||||
if was_marked and iscompilable:
|
if iscompilable:
|
||||||
|
self._add_compiled(newregex)
|
||||||
|
if was_marked:
|
||||||
self.mark(newregex)
|
self.mark(newregex)
|
||||||
|
|
||||||
def save_to_xml(self, outfile):
|
def save_to_xml(self, outfile):
|
||||||
@@ -492,8 +511,8 @@ def ordered_keys(_dict):
|
|||||||
|
|
||||||
|
|
||||||
if ISWINDOWS:
|
if ISWINDOWS:
|
||||||
def has_sep(x):
|
def has_sep(regexp):
|
||||||
return '\\' + sep in x
|
return '\\' + sep in regexp
|
||||||
else:
|
else:
|
||||||
def has_sep(x):
|
def has_sep(regexp):
|
||||||
return sep in x
|
return sep in regexp
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
# from hscommon.trans import tr
|
# from hscommon.trans import tr
|
||||||
from .exclude_list_table import ExcludeListTable
|
from .exclude_list_table import ExcludeListTable
|
||||||
|
from core.exclude import has_sep
|
||||||
|
from os import sep
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
@@ -30,9 +32,10 @@ class ExcludeListDialogCore:
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def rename_selected(self, newregex):
|
def rename_selected(self, newregex):
|
||||||
"""Renames the selected regex to ``newregex``.
|
"""Rename the selected regex to ``newregex``.
|
||||||
If there's more than one selected row, the first one is used.
|
If there is more than one selected row, the first one is used.
|
||||||
:param str newregex: The regex to rename the row's regex to.
|
:param str newregex: The regex to rename the row's regex to.
|
||||||
|
:return bool: true if success, false if error.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
r = self.exclude_list_table.selected_rows[0]
|
r = self.exclude_list_table.selected_rows[0]
|
||||||
@@ -52,17 +55,37 @@ class ExcludeListDialogCore:
|
|||||||
self.exclude_list_table.add(regex)
|
self.exclude_list_table.add(regex)
|
||||||
|
|
||||||
def test_string(self, test_string):
|
def test_string(self, test_string):
|
||||||
"""Sets property on row to highlight if its regex matches test_string supplied."""
|
"""Set the highlight property on each row when its regex matches the
|
||||||
|
test_string supplied. Return True if any row matched."""
|
||||||
matched = False
|
matched = False
|
||||||
for row in self.exclude_list_table.rows:
|
for row in self.exclude_list_table.rows:
|
||||||
compiled_regex = self.exclude_list.get_compiled(row.regex)
|
compiled_regex = self.exclude_list.get_compiled(row.regex)
|
||||||
if compiled_regex and compiled_regex.match(test_string):
|
|
||||||
matched = True
|
if self.is_match(test_string, compiled_regex):
|
||||||
row.highlight = True
|
row.highlight = True
|
||||||
|
matched = True
|
||||||
else:
|
else:
|
||||||
row.highlight = False
|
row.highlight = False
|
||||||
return matched
|
return matched
|
||||||
|
|
||||||
|
def is_match(self, test_string, compiled_regex):
|
||||||
|
# This method is like an inverted version of ExcludeList.is_excluded()
|
||||||
|
if not compiled_regex:
|
||||||
|
return False
|
||||||
|
matched = False
|
||||||
|
|
||||||
|
# Test only the filename portion of the path
|
||||||
|
if not has_sep(compiled_regex.pattern) and sep in test_string:
|
||||||
|
filename = test_string.rsplit(sep, 1)[1]
|
||||||
|
if compiled_regex.fullmatch(filename):
|
||||||
|
matched = True
|
||||||
|
return matched
|
||||||
|
|
||||||
|
# Test the entire path + filename
|
||||||
|
if compiled_regex.fullmatch(test_string):
|
||||||
|
matched = True
|
||||||
|
return matched
|
||||||
|
|
||||||
def reset_rows_highlight(self):
|
def reset_rows_highlight(self):
|
||||||
for row in self.exclude_list_table.rows:
|
for row in self.exclude_list_table.rows:
|
||||||
row.highlight = False
|
row.highlight = False
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ExcludeListTable(GUITable, DupeGuruGUIObject):
|
|||||||
return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0
|
return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0
|
||||||
|
|
||||||
def _do_delete(self):
|
def _do_delete(self):
|
||||||
self.dalog.exclude_list.remove(self.selected_row.regex)
|
self.dialog.exclude_list.remove(self.selected_row.regex)
|
||||||
|
|
||||||
# --- Override
|
# --- Override
|
||||||
def add(self, regex):
|
def add(self, regex):
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class DupeRow(Row):
|
|||||||
# table.DELTA_COLUMNS are always "delta"
|
# table.DELTA_COLUMNS are always "delta"
|
||||||
self._delta_columns = self.table.DELTA_COLUMNS.copy()
|
self._delta_columns = self.table.DELTA_COLUMNS.copy()
|
||||||
dupe_info = self.data
|
dupe_info = self.data
|
||||||
|
if self._group.ref is None:
|
||||||
|
return False
|
||||||
ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
|
ref_info = self._group.ref.get_display_info(group=self._group, delta=False)
|
||||||
for key, value in dupe_info.items():
|
for key, value in dupe_info.items():
|
||||||
if (key not in self._delta_columns) and (
|
if (key not in self._delta_columns) and (
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class Results(Markable):
|
|||||||
self.app = app
|
self.app = app
|
||||||
self.problems = [] # (dupe, error_msg)
|
self.problems = [] # (dupe, error_msg)
|
||||||
self.is_modified = False
|
self.is_modified = False
|
||||||
|
self.refresh_required = False
|
||||||
|
|
||||||
def _did_mark(self, dupe):
|
def _did_mark(self, dupe):
|
||||||
self.__marked_size += dupe.size
|
self.__marked_size += dupe.size
|
||||||
@@ -94,8 +95,9 @@ class Results(Markable):
|
|||||||
|
|
||||||
# ---Private
|
# ---Private
|
||||||
def __get_dupe_list(self):
|
def __get_dupe_list(self):
|
||||||
if self.__dupes is None:
|
if self.__dupes is None or self.refresh_required:
|
||||||
self.__dupes = flatten(group.dupes for group in self.groups)
|
self.__dupes = flatten(group.dupes for group in self.groups)
|
||||||
|
self.refresh_required = False
|
||||||
if None in self.__dupes:
|
if None in self.__dupes:
|
||||||
# This is debug logging to try to figure out #44
|
# This is debug logging to try to figure out #44
|
||||||
logging.warning(
|
logging.warning(
|
||||||
|
|||||||
@@ -473,6 +473,24 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
|
|||||||
assert "file_ending_with_subdir" not in files
|
assert "file_ending_with_subdir" not in files
|
||||||
assert "file_which_shouldnt_match" in files
|
assert "file_which_shouldnt_match" in files
|
||||||
|
|
||||||
|
# This should match the directory only
|
||||||
|
regex6 = r".*/subdir.*"
|
||||||
|
if ISWINDOWS:
|
||||||
|
regex6 = r".*\\.*subdir.*"
|
||||||
|
self.d._exclude_list.rename(regex5, regex6)
|
||||||
|
self.d._exclude_list.remove(regex1)
|
||||||
|
assert regex1 not in self.d._exclude_list
|
||||||
|
assert regex5 not in self.d._exclude_list
|
||||||
|
assert self.d._exclude_list.error(regex6) is None
|
||||||
|
# This still should not be affected
|
||||||
|
eq_(self.d.get_state(p1["$Recycle.Bin"]["subdir"]), DirectoryState.Normal)
|
||||||
|
files = self.get_files_and_expect_num_result(5)
|
||||||
|
# These files are under the "/subdir" directory
|
||||||
|
assert "somesubdirfile.png" not in files
|
||||||
|
assert "unwanted_subdirfile.gif" not in files
|
||||||
|
# This file under "subdar" directory should not be filtered out
|
||||||
|
assert "file_ending_with_subdir" in files
|
||||||
|
|
||||||
def test_japanese_unicode(self, tmpdir):
|
def test_japanese_unicode(self, tmpdir):
|
||||||
p1 = Path(str(tmpdir))
|
p1 = Path(str(tmpdir))
|
||||||
p1["$Recycle.Bin"].mkdir()
|
p1["$Recycle.Bin"].mkdir()
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ class TestCasegetwords:
|
|||||||
eq_(["a", "b", "c", "d"], getwords("a b c d"))
|
eq_(["a", "b", "c", "d"], getwords("a b c d"))
|
||||||
eq_(["a", "b", "c", "d"], getwords(" a b c d "))
|
eq_(["a", "b", "c", "d"], getwords(" a b c d "))
|
||||||
|
|
||||||
|
def test_unicode(self):
|
||||||
|
eq_(["e", "c", "0", "a", "o", "u", "e", "u"], getwords("é ç 0 à ö û è ¤ ù"))
|
||||||
|
eq_(["02", "君のこころは輝いてるかい?", "国木田花丸", "solo", "ver"], getwords("02 君のこころは輝いてるかい? 国木田花丸 Solo Ver"))
|
||||||
|
|
||||||
def test_splitter_chars(self):
|
def test_splitter_chars(self):
|
||||||
eq_(
|
eq_(
|
||||||
[chr(i) for i in range(ord("a"), ord("z") + 1)],
|
[chr(i) for i in range(ord("a"), ord("z") + 1)],
|
||||||
@@ -85,7 +89,7 @@ class TestCasegetwords:
|
|||||||
eq_(["foo", "bar"], getwords("FOO BAR"))
|
eq_(["foo", "bar"], getwords("FOO BAR"))
|
||||||
|
|
||||||
def test_decompose_unicode(self):
|
def test_decompose_unicode(self):
|
||||||
eq_(getwords("foo\xe9bar"), ["fooebar"])
|
eq_(["fooebar"], getwords("foo\xe9bar"))
|
||||||
|
|
||||||
|
|
||||||
class TestCasegetfields:
|
class TestCasegetfields:
|
||||||
|
|||||||
@@ -188,6 +188,28 @@ class TestCaseListEmpty:
|
|||||||
self.exclude_list.rename(regex_renamed_compilable, regex_compilable)
|
self.exclude_list.rename(regex_renamed_compilable, regex_compilable)
|
||||||
eq_(self.exclude_list.is_marked(regex_compilable), True)
|
eq_(self.exclude_list.is_marked(regex_compilable), True)
|
||||||
|
|
||||||
|
def test_rename_regex_file_to_path(self):
|
||||||
|
regex = r".*/one.*"
|
||||||
|
if ISWINDOWS:
|
||||||
|
regex = r".*\\one.*"
|
||||||
|
regex2 = r".*one.*"
|
||||||
|
self.exclude_list.add(regex)
|
||||||
|
self.exclude_list.mark(regex)
|
||||||
|
compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled]
|
||||||
|
files_re = [x.pattern for x in self.exclude_list.compiled_files]
|
||||||
|
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
|
||||||
|
assert regex in compiled_re
|
||||||
|
assert regex not in files_re
|
||||||
|
assert regex in paths_re
|
||||||
|
self.exclude_list.rename(regex, regex2)
|
||||||
|
compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled]
|
||||||
|
files_re = [x.pattern for x in self.exclude_list.compiled_files]
|
||||||
|
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
|
||||||
|
assert regex not in compiled_re
|
||||||
|
assert regex2 in compiled_re
|
||||||
|
assert regex2 in files_re
|
||||||
|
assert regex2 not in paths_re
|
||||||
|
|
||||||
def test_restore_default(self):
|
def test_restore_default(self):
|
||||||
"""Only unmark previously added regexes and mark the pre-defined ones"""
|
"""Only unmark previously added regexes and mark the pre-defined ones"""
|
||||||
regex = r"one"
|
regex = r"one"
|
||||||
@@ -213,6 +235,73 @@ class TestCaseListEmpty:
|
|||||||
eq_(len(default_regexes), len(self.exclude_list.compiled))
|
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)
|
||||||
|
self.exclude_list = self.app.exclude_list
|
||||||
|
|
||||||
|
def test_add_mark_and_remove_regex(self):
|
||||||
|
regex1 = r"one"
|
||||||
|
regex2 = r"two"
|
||||||
|
self.exclude_list.add(regex1)
|
||||||
|
assert(regex1 in self.exclude_list)
|
||||||
|
self.exclude_list.add(regex2)
|
||||||
|
self.exclude_list.mark(regex1)
|
||||||
|
self.exclude_list.mark(regex2)
|
||||||
|
eq_(len(self.exclude_list), 2)
|
||||||
|
eq_(len(self.exclude_list.compiled), 1)
|
||||||
|
compiled_files = [x for x in self.exclude_list.compiled_files]
|
||||||
|
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)
|
||||||
|
eq_(len(self.exclude_list), 1)
|
||||||
|
|
||||||
|
def test_rename_regex_file_to_path(self):
|
||||||
|
regex = r".*/one.*"
|
||||||
|
if ISWINDOWS:
|
||||||
|
regex = r".*\\one.*"
|
||||||
|
regex2 = r".*one.*"
|
||||||
|
self.exclude_list.add(regex)
|
||||||
|
self.exclude_list.mark(regex)
|
||||||
|
eq_(len([x for x in self.exclude_list]), 1)
|
||||||
|
compiled_re = [x.pattern for x in self.exclude_list.compiled]
|
||||||
|
files_re = [x.pattern for x in self.exclude_list.compiled_files]
|
||||||
|
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
|
||||||
|
assert regex in compiled_re
|
||||||
|
assert regex not in files_re
|
||||||
|
assert regex in paths_re
|
||||||
|
self.exclude_list.rename(regex, regex2)
|
||||||
|
eq_(len([x for x in self.exclude_list]), 1)
|
||||||
|
compiled_re = [x.pattern for x in self.exclude_list.compiled]
|
||||||
|
files_re = [x.pattern for x in self.exclude_list.compiled_files]
|
||||||
|
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
|
||||||
|
assert regex not in compiled_re
|
||||||
|
assert regex2 in compiled_re
|
||||||
|
assert regex2 in files_re
|
||||||
|
assert regex2 not in paths_re
|
||||||
|
|
||||||
|
def test_restore_default(self):
|
||||||
|
"""Only unmark previously added regexes and mark the pre-defined ones"""
|
||||||
|
regex = r"one"
|
||||||
|
self.exclude_list.add(regex)
|
||||||
|
self.exclude_list.mark(regex)
|
||||||
|
self.exclude_list.restore_defaults()
|
||||||
|
eq_(len(default_regexes), self.exclude_list.marked_count)
|
||||||
|
# added regex shouldn't be marked
|
||||||
|
eq_(self.exclude_list.is_marked(regex), False)
|
||||||
|
# added regex shouldn't be in compiled list either
|
||||||
|
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])
|
||||||
|
assert compiled_escaped == default_escaped
|
||||||
|
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
||||||
|
|
||||||
|
|
||||||
class TestCaseDictEmpty(TestCaseListEmpty):
|
class TestCaseDictEmpty(TestCaseListEmpty):
|
||||||
"""Same, but with dictionary implementation"""
|
"""Same, but with dictionary implementation"""
|
||||||
def setup_method(self, method):
|
def setup_method(self, method):
|
||||||
@@ -221,6 +310,73 @@ class TestCaseDictEmpty(TestCaseListEmpty):
|
|||||||
self.exclude_list = self.app.exclude_list
|
self.exclude_list = self.app.exclude_list
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaseDictEmptyUnion(TestCaseDictEmpty):
|
||||||
|
"""Same, but with union regex"""
|
||||||
|
def setup_method(self, method):
|
||||||
|
self.app = DupeGuru()
|
||||||
|
self.app.exclude_list = ExcludeDict(union_regex=True)
|
||||||
|
self.exclude_list = self.app.exclude_list
|
||||||
|
|
||||||
|
def test_add_mark_and_remove_regex(self):
|
||||||
|
regex1 = r"one"
|
||||||
|
regex2 = r"two"
|
||||||
|
self.exclude_list.add(regex1)
|
||||||
|
assert(regex1 in self.exclude_list)
|
||||||
|
self.exclude_list.add(regex2)
|
||||||
|
self.exclude_list.mark(regex1)
|
||||||
|
self.exclude_list.mark(regex2)
|
||||||
|
eq_(len(self.exclude_list), 2)
|
||||||
|
eq_(len(self.exclude_list.compiled), 1)
|
||||||
|
compiled_files = [x for x in self.exclude_list.compiled_files]
|
||||||
|
# two patterns joined into one
|
||||||
|
eq_(len(compiled_files), 1)
|
||||||
|
self.exclude_list.remove(regex2)
|
||||||
|
assert(regex2 not in self.exclude_list)
|
||||||
|
eq_(len(self.exclude_list), 1)
|
||||||
|
|
||||||
|
def test_rename_regex_file_to_path(self):
|
||||||
|
regex = r".*/one.*"
|
||||||
|
if ISWINDOWS:
|
||||||
|
regex = r".*\\one.*"
|
||||||
|
regex2 = r".*one.*"
|
||||||
|
self.exclude_list.add(regex)
|
||||||
|
self.exclude_list.mark(regex)
|
||||||
|
marked_re = [x for marked, x in self.exclude_list if marked]
|
||||||
|
eq_(len(marked_re), 1)
|
||||||
|
compiled_re = [x.pattern for x in self.exclude_list.compiled]
|
||||||
|
files_re = [x.pattern for x in self.exclude_list.compiled_files]
|
||||||
|
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
|
||||||
|
assert regex in compiled_re
|
||||||
|
assert regex not in files_re
|
||||||
|
assert regex in paths_re
|
||||||
|
self.exclude_list.rename(regex, regex2)
|
||||||
|
compiled_re = [x.pattern for x in self.exclude_list.compiled]
|
||||||
|
files_re = [x.pattern for x in self.exclude_list.compiled_files]
|
||||||
|
paths_re = [x.pattern for x in self.exclude_list.compiled_paths]
|
||||||
|
assert regex not in compiled_re
|
||||||
|
assert regex2 in compiled_re
|
||||||
|
assert regex2 in files_re
|
||||||
|
assert regex2 not in paths_re
|
||||||
|
|
||||||
|
def test_restore_default(self):
|
||||||
|
"""Only unmark previously added regexes and mark the pre-defined ones"""
|
||||||
|
regex = r"one"
|
||||||
|
self.exclude_list.add(regex)
|
||||||
|
self.exclude_list.mark(regex)
|
||||||
|
self.exclude_list.restore_defaults()
|
||||||
|
eq_(len(default_regexes), self.exclude_list.marked_count)
|
||||||
|
# added regex shouldn't be marked
|
||||||
|
eq_(self.exclude_list.is_marked(regex), False)
|
||||||
|
# added regex shouldn't be in compiled list either
|
||||||
|
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])
|
||||||
|
assert compiled_escaped == default_escaped
|
||||||
|
eq_(len(default_regexes), len(compiled[0].pattern.split("|")))
|
||||||
|
|
||||||
|
|
||||||
def split_union(pattern_object):
|
def split_union(pattern_object):
|
||||||
"""Returns list of strings for each union pattern"""
|
"""Returns list of strings for each union pattern"""
|
||||||
return [x for x in pattern_object.pattern.split("|")]
|
return [x for x in pattern_object.pattern.split("|")]
|
||||||
|
|||||||
2
macos.md
2
macos.md
@@ -11,7 +11,7 @@
|
|||||||
1. Install Xcode if desired
|
1. Install Xcode if desired
|
||||||
2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc`
|
2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc`
|
||||||
with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take
|
with `export PATH="/opt/homebrew/bin:$PATH"`. Will need to reload terminal or source the file to take
|
||||||
affect.
|
effect.
|
||||||
3. Install qt5 with `brew`. If you are using a version of macos without system python 3.6+ then you will
|
3. Install qt5 with `brew`. If you are using a version of macos without system python 3.6+ then you will
|
||||||
also need to install that via brew or with pyenv.
|
also need to install that via brew or with pyenv.
|
||||||
|
|
||||||
|
|||||||
@@ -271,6 +271,9 @@ class DupeGuru(QObject):
|
|||||||
self.willSavePrefs.emit()
|
self.willSavePrefs.emit()
|
||||||
self.prefs.save()
|
self.prefs.save()
|
||||||
self.model.save()
|
self.model.save()
|
||||||
|
# Workaround for #857, hide() or close().
|
||||||
|
if self.details_dialog is not None:
|
||||||
|
self.details_dialog.close()
|
||||||
QApplication.quit()
|
QApplication.quit()
|
||||||
|
|
||||||
# --- Signals
|
# --- Signals
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class DetailsDialog(QDockWidget):
|
|||||||
if not self.titleBarWidget(): # default title bar
|
if not self.titleBarWidget(): # default title bar
|
||||||
self.setTitleBarWidget(QWidget()) # disables title bar
|
self.setTitleBarWidget(QWidget()) # disables title bar
|
||||||
# Windows (and MacOS?) users cannot move a floating window which
|
# Windows (and MacOS?) users cannot move a floating window which
|
||||||
# has not native decoration so we force it to dock for now
|
# has no native decoration so we force it to dock for now
|
||||||
if not ISLINUX:
|
if not ISLINUX:
|
||||||
self.setFloating(False)
|
self.setFloating(False)
|
||||||
elif self.titleBarWidget() is not None: # title bar is disabled
|
elif self.titleBarWidget() is not None: # title bar is disabled
|
||||||
|
|||||||
@@ -116,31 +116,32 @@ class ExcludeListDialog(QDialog):
|
|||||||
if not input_text:
|
if not input_text:
|
||||||
self.reset_input_style()
|
self.reset_input_style()
|
||||||
return
|
return
|
||||||
# if at least one row matched, we know whether table is highlighted or not
|
# If at least one row matched, we know whether table is highlighted or not
|
||||||
self._row_matched = self.model.test_string(input_text)
|
self._row_matched = self.model.test_string(input_text)
|
||||||
self.table.refresh()
|
self.table.refresh()
|
||||||
|
|
||||||
|
# Test the string currently in the input text box as well
|
||||||
input_regex = self.inputLine.text()
|
input_regex = self.inputLine.text()
|
||||||
if not input_regex:
|
if not input_regex:
|
||||||
self.reset_input_style()
|
self.reset_input_style()
|
||||||
return
|
return
|
||||||
|
compiled = None
|
||||||
try:
|
try:
|
||||||
compiled = re.compile(input_regex)
|
compiled = re.compile(input_regex)
|
||||||
except re.error:
|
except re.error:
|
||||||
self.reset_input_style()
|
self.reset_input_style()
|
||||||
return
|
return
|
||||||
match = compiled.match(input_text)
|
if self.model.is_match(input_text, compiled):
|
||||||
if match:
|
|
||||||
self._input_styled = True
|
|
||||||
self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);")
|
self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);")
|
||||||
|
self._input_styled = True
|
||||||
else:
|
else:
|
||||||
self.reset_input_style()
|
self.reset_input_style()
|
||||||
|
|
||||||
def reset_input_style(self):
|
def reset_input_style(self):
|
||||||
"""Reset regex input line background"""
|
"""Reset regex input line background"""
|
||||||
if self._input_styled:
|
if self._input_styled:
|
||||||
self._input_styled = False
|
|
||||||
self.inputLine.setStyleSheet(self.styleSheet())
|
self.inputLine.setStyleSheet(self.styleSheet())
|
||||||
|
self._input_styled = False
|
||||||
|
|
||||||
def reset_table_style(self):
|
def reset_table_style(self):
|
||||||
if self._row_matched:
|
if self._row_matched:
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ tr = trget("ui")
|
|||||||
class DetailsDialog(DetailsDialogBase):
|
class DetailsDialog(DetailsDialogBase):
|
||||||
def __init__(self, parent, app):
|
def __init__(self, parent, app):
|
||||||
self.vController = None
|
self.vController = None
|
||||||
self.app = app
|
|
||||||
super().__init__(parent, app)
|
super().__init__(parent, app)
|
||||||
|
|
||||||
def _setupUi(self):
|
def _setupUi(self):
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class Preferences(PreferencesBase):
|
|||||||
self.details_dialog_override_theme_icons = False if not ISLINUX else True
|
self.details_dialog_override_theme_icons = False if not ISLINUX else True
|
||||||
self.details_dialog_viewers_show_scrollbars = True
|
self.details_dialog_viewers_show_scrollbars = True
|
||||||
self.result_table_ref_foreground_color = QColor(Qt.blue)
|
self.result_table_ref_foreground_color = QColor(Qt.blue)
|
||||||
self.result_table_ref_background_color = QColor(Qt.darkGray)
|
self.result_table_ref_background_color = QColor(Qt.lightGray)
|
||||||
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
|
self.result_table_delta_foreground_color = QColor(255, 142, 40) # orange
|
||||||
self.resultWindowIsMaximized = False
|
self.resultWindowIsMaximized = False
|
||||||
self.resultWindowRect = None
|
self.resultWindowRect = None
|
||||||
|
|||||||
Reference in New Issue
Block a user