Merge branch 'master' into dev

This commit is contained in:
glubsy 2020-12-29 20:10:42 +01:00
commit c1d94d6771
24 changed files with 74 additions and 92 deletions

View File

@ -5,11 +5,6 @@ install:
script: tox script: tox
matrix: matrix:
include: include:
- os: "linux"
dist: "xenial"
python: "3.8"
script:
- tox -e style
- os: "linux" - os: "linux"
dist: "xenial" dist: "xenial"
python: "3.6" python: "3.6"
@ -17,18 +12,16 @@ matrix:
dist: "xenial" dist: "xenial"
python: "3.7" python: "3.7"
- os: "linux" - os: "linux"
dist: "xenial" dist: "focal"
python: "3.8" python: "3.8"
- os: "linux"
dist: "focal"
python: "3.9"
- os: "windows" - os: "windows"
language: shell language: shell
python: "3.7" python: "3.8"
env: "PATH=/c/python37:/c/python37/Scripts:$PATH" env: "PATH=/c/python38:/c/python38/Scripts:$PATH"
before_install: before_install:
- choco install python --version=3.7.6 - choco install python --version=3.8.6
- choco install make - cp /c/python38/python.exe /c/python38/python3.exe
- cp /c/python37/python.exe /c/python37/python3.exe script: tox -e py38
before_script:
- pip3 install -r requirements-windows.txt
- python3 build.py
script:
- tox -e WINDOWS

View File

@ -1,6 +1,8 @@
To know who contributed to dupeGuru, you can look at the commit log, but not all contributions To know who contributed to dupeGuru, you can look at the commit log, but not all contributions
result in a commit. This file lists contributors who don't necessarily appear in the commit log. result in a commit. This file lists contributors who don't necessarily appear in the commit log.
* Jason Cho, Exchange icon
* schollidesign (https://findicons.com/pack/1035/human_o2), Zoom-in, Zoom-out, Zoom-best-fit, Zoom-original icons
* Jérôme Cantin, Main icon * Jérôme Cantin, Main icon
* Gregor Tätzner, German localization * Gregor Tätzner, German localization
* Frank Weber, German localization * Frank Weber, German localization

View File

@ -21,7 +21,7 @@ To build with a different python version 3.5 vs 3.7 or 32 bit vs 64 bit specify
$ cd <dupeGuru directory> $ cd <dupeGuru directory>
$ py -3.7 -m venv .\env $ py -3.7 -m venv .\env
$ .\env\Scripts\activate $ .\env\Scripts\activate
$ pip install -r requirements.txt -r requirements-windows.txt $ pip install -r requirements.txt
$ python build.py $ python build.py
$ python run.py $ python run.py
@ -37,8 +37,6 @@ Then the following execution of the makefile should work. Pass the correct valu
$ make PYTHON='py -3.7' $ make PYTHON='py -3.7'
$ make run $ make run
NOTE: Install PyQt5 & cx-Freeze with requirements-windows.txt into the venv before running the packaging scripts in the section below.
### Generate Windows Installer Packages ### Generate Windows Installer Packages
You need to use the respective x86 or x64 version of python to build the 32 bit and 64 bit versions. The build scripts will automatically detect the python architecture for you. When using build.py make sure the resulting python works before continuing to package.py. NOTE: package.py looks for the 'makensis' executable in the default location for a 64 bit windows system. Run the following in the respective virtual environment. You need to use the respective x86 or x64 version of python to build the 32 bit and 64 bit versions. The build scripts will automatically detect the python architecture for you. When using build.py make sure the resulting python works before continuing to package.py. NOTE: package.py looks for the 'makensis' executable in the default location for a 64 bit windows system. Run the following in the respective virtual environment.

View File

@ -54,6 +54,12 @@ def parse_args():
dest="normpo", dest="normpo",
help="Normalize all PO files (do this before commit).", help="Normalize all PO files (do this before commit).",
) )
parser.add_option(
"--modules",
action="store_true",
dest="modules",
help="Build the python modules.",
)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
return options return options
@ -182,6 +188,8 @@ def main():
build_mergepot() build_mergepot()
elif options.normpo: elif options.normpo:
build_normpo() build_normpo()
elif options.modules:
build_pe_modules()
else: else:
build_normal() build_normal()

View File

@ -12,6 +12,7 @@ from os import sep
import logging import logging
import functools import functools
from hscommon.util import FileOrPath from hscommon.util import FileOrPath
from hscommon.plat import ISWINDOWS
import time import time
default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP
@ -19,10 +20,10 @@ default_regexes = [r"^thumbs\.db$", # Obsolete after WindowsXP
r"^\.DS_Store$", # MacOS metadata r"^\.DS_Store$", # MacOS metadata
r"^\.Trash\-.*", # Linux trash directories r"^\.Trash\-.*", # Linux trash directories
r"^\$Recycle\.Bin$", # Windows r"^\$Recycle\.Bin$", # Windows
r"^\..*", # Hidden files r"^\..*", # Hidden files on Unix-like
] ]
# These are too broad # These are too broad
forbidden_regexes = [r".*", r"\/.*", r".*\/.*", r".*\..*"] forbidden_regexes = [r".*", r"\/.*", r".*\/.*", r".*\\\\.*", r".*\..*"]
def timer(func): def timer(func):
@ -169,9 +170,9 @@ class ExcludeList(Markable):
def build_compiled_caches(self, union=False): def build_compiled_caches(self, union=False):
if not union: if not union:
self._cached_compiled_files =\ self._cached_compiled_files =\
[x for x in self._excluded_compiled if sep not in 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 sep in x.pattern] [x for x in self._excluded_compiled if has_sep(x.pattern)]
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!
@ -184,13 +185,13 @@ class ExcludeList(Markable):
# the same regardless of whether the client asked for union or not # the same regardless of whether the client asked for union or not
self._cached_compiled_union_all =\ self._cached_compiled_union_all =\
(re.compile('|'.join(marked_count)),) (re.compile('|'.join(marked_count)),)
files_marked = [x for x in marked_count if sep not in x] files_marked = [x for x in marked_count if not has_sep(x)]
if not files_marked: if not files_marked:
self._cached_compiled_union_files = tuple() self._cached_compiled_union_files = tuple()
else: else:
self._cached_compiled_union_files =\ self._cached_compiled_union_files =\
(re.compile('|'.join(files_marked)),) (re.compile('|'.join(files_marked)),)
paths_marked = [x for x in marked_count if sep in x] paths_marked = [x for x in marked_count if has_sep(x)]
if not paths_marked: if not paths_marked:
self._cached_compiled_union_paths = tuple() self._cached_compiled_union_paths = tuple()
else: else:
@ -488,3 +489,11 @@ def ordered_keys(_dict):
list_of_items.sort(key=lambda x: x[1].get("index")) list_of_items.sort(key=lambda x: x[1].get("index"))
for item in list_of_items: for item in list_of_items:
yield item[0] yield item[0]
if ISWINDOWS:
def has_sep(x):
return '\\' + sep in x
else:
def has_sep(x):
return sep in x

View File

@ -12,6 +12,7 @@ import shutil
from pytest import raises from pytest import raises
from hscommon.path import Path from hscommon.path import Path
from hscommon.testutil import eq_ from hscommon.testutil import eq_
from hscommon.plat import ISWINDOWS
from ..fs import File from ..fs import File
from ..directories import ( from ..directories import (
@ -428,7 +429,10 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
assert "unwanted_subdirfile.gif" not in files assert "unwanted_subdirfile.gif" not in files
assert "unwanted_subdarfile.png" not in files assert "unwanted_subdarfile.png" not in files
regex3 = r".*Recycle\.Bin\/.*unwanted.*subdirfile.*" if ISWINDOWS:
regex3 = r".*Recycle\.Bin\\.*unwanted.*subdirfile.*"
else:
regex3 = r".*Recycle\.Bin\/.*unwanted.*subdirfile.*"
self.d._exclude_list.rename(regex2, regex3) self.d._exclude_list.rename(regex2, regex3)
assert self.d._exclude_list.error(regex3) is None assert self.d._exclude_list.error(regex3) is None
# print(f"get_folders(): {[x for x in self.d.get_folders()]}") # print(f"get_folders(): {[x for x in self.d.get_folders()]}")
@ -516,6 +520,8 @@ files: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled
self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.Normal) self.d.set_state(p1["foobar"][".hidden_dir"], DirectoryState.Normal)
# The files should still be filtered # The files should still be filtered
files = self.get_files_and_expect_num_result(1) files = self.get_files_and_expect_num_result(1)
eq_(len(self.d._exclude_list.compiled_paths), 0)
eq_(len(self.d._exclude_list.compiled_files), 1)
assert ".hidden_file.txt" not in files assert ".hidden_file.txt" not in files
assert ".hidden_subfile.png" not in files assert ".hidden_subfile.png" not in files
assert "foobar.jpg" in files assert "foobar.jpg" in files

View File

@ -11,6 +11,7 @@ from xml.etree import ElementTree as ET
# from pytest import raises # from pytest import raises
from hscommon.testutil import eq_ from hscommon.testutil import eq_
from hscommon.plat import ISWINDOWS
from .base import DupeGuru from .base import DupeGuru
from ..exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException from ..exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException
@ -245,8 +246,11 @@ class TestCaseCompiledList():
assert expr.pattern in exprs assert expr.pattern in exprs
def test_compiled_files(self): def test_compiled_files(self):
# test is separator is indeed checked properly to yield the output # is path separator checked properly to yield the output
regex1 = r"test/one/sub" if ISWINDOWS:
regex1 = r"test\\one\\sub"
else:
regex1 = r"test/one/sub"
self.e_separate.add(regex1) self.e_separate.add(regex1)
self.e_separate.mark(regex1) self.e_separate.mark(regex1)
self.e_union.add(regex1) self.e_union.add(regex1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -3,5 +3,5 @@
"longname": "dupeGuru", "longname": "dupeGuru",
"execname": "dupeguru", "execname": "dupeguru",
"arch": "any", "arch": "any",
"iconpath": "/usr/share/dupeguru/dgse_logo_128.png" "iconpath": "dupeguru"
} }

View File

@ -8,4 +8,5 @@ all:
chmod +x src/run.py chmod +x src/run.py
cp -R src/ "$(CURDIR)/debian/{pkgname}/usr/share/{execname}" cp -R src/ "$(CURDIR)/debian/{pkgname}/usr/share/{execname}"
cp "$(CURDIR)/debian/{execname}.desktop" "$(CURDIR)/debian/{pkgname}/usr/share/applications" cp "$(CURDIR)/debian/{execname}.desktop" "$(CURDIR)/debian/{pkgname}/usr/share/applications"
ln -s "/usr/share/{execname}/dgse_logo_128.png" "$(CURDIR)/debian/{pkgname}/usr/pixmaps/{execname}.png"
ln -s "/usr/share/{execname}/run.py" "$(CURDIR)/debian/{pkgname}/usr/bin/{execname}" ln -s "/usr/share/{execname}/run.py" "$(CURDIR)/debian/{pkgname}/usr/bin/{execname}"

View File

@ -3,5 +3,5 @@
"longname": "dupeGuru", "longname": "dupeGuru",
"execname": "dupeguru", "execname": "dupeguru",
"arch": "any", "arch": "any",
"iconpath": "/usr/share/dupeguru/dgse_logo_128.png" "iconpath": "dupeguru"
} }

View File

@ -1 +1 @@
3.0 (native) 3.0 (native)

View File

@ -80,9 +80,9 @@ class ExcludeListDialog(QDialog):
gridlayout.addWidget(self.testLine, 6, 0) gridlayout.addWidget(self.testLine, 6, 0)
layout.addLayout(gridlayout) layout.addLayout(gridlayout)
self.inputLine.setPlaceholderText("Type a python regular expression here...") self.inputLine.setPlaceholderText(tr("Type a python regular expression here..."))
self.inputLine.setFocus() self.inputLine.setFocus()
self.testLine.setPlaceholderText("Type a file system path or filename here...") self.testLine.setPlaceholderText(tr("Type a file system path or filename here..."))
self.testLine.setClearButtonEnabled(True) self.testLine.setClearButtonEnabled(True)
# --- model --> view # --- model --> view
@ -118,6 +118,8 @@ class ExcludeListDialog(QDialog):
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)
# FIXME There is a bug on Windows (7) where the table rows don't get
# repainted until the table receives a mouse click event.
self.tableView.update() self.tableView.update()
input_regex = self.inputLine.text() input_regex = self.inputLine.text()
@ -132,7 +134,7 @@ class ExcludeListDialog(QDialog):
match = compiled.match(input_text) match = compiled.match(input_text)
if match: if match:
self._input_styled = True self._input_styled = True
self.inputLine.setStyleSheet("background-color: rgb(10, 120, 10);") self.inputLine.setStyleSheet("background-color: rgb(10, 200, 10);")
else: else:
self.reset_input_style() self.reset_input_style()
@ -160,5 +162,6 @@ Example: if you want to filter out .PNG files from the "My Pictures" directory o
<code>.*My\\sPictures\\\\.*\\.png</code><br><br>\ <code>.*My\\sPictures\\\\.*\\.png</code><br><br>\
You can test the regular expression with the test string feature by pasting a fake path in it:<br>\ You can test the regular expression with the test string feature by pasting a fake path in it:<br>\
<code>C:\\\\User\\My Pictures\\test.png</code><br><br> <code>C:\\\\User\\My Pictures\\test.png</code><br><br>
Matching regular expressions will be highlighted.<br><br> Matching regular expressions will be highlighted.<br>\
If there is at least one highlight, the path 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

@ -7,6 +7,8 @@ from PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor
from qtlib.column import Column from qtlib.column import Column
from qtlib.table import Table from qtlib.table import Table
from hscommon.trans import trget
tr = trget("ui")
class ExcludeListTable(Table): class ExcludeListTable(Table):
@ -31,7 +33,7 @@ class ExcludeListTable(Table):
if role == Qt.CheckStateRole and row.markable: if role == Qt.CheckStateRole and row.markable:
return Qt.Checked if row.marked else Qt.Unchecked return Qt.Checked if row.marked else Qt.Unchecked
if role == Qt.ToolTipRole and not row.markable: if role == Qt.ToolTipRole and not row.markable:
return "Compilation error: " + row.get_cell_value("error") return tr("Compilation error: ") + row.get_cell_value("error")
if role == Qt.DecorationRole and not row.markable: if role == Qt.DecorationRole and not row.markable:
return QIcon.fromTheme("dialog-error", QIcon(":/error")) return QIcon.fromTheme("dialog-error", QIcon(":/error"))
return None return None
@ -41,7 +43,7 @@ class ExcludeListTable(Table):
return QFont(self.view.font()) return QFont(self.view.font())
elif role == Qt.BackgroundRole and column.name == "regex": elif role == Qt.BackgroundRole and column.name == "regex":
if row.highlight: if row.highlight:
return QColor(10, 120, 10) # green return QColor(10, 200, 10) # green
elif role == Qt.EditRole: elif role == Qt.EditRole:
if column.name == "regex": if column.name == "regex":
return row.data[column.name] return row.data[column.name]

View File

@ -19,14 +19,8 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details")) self.setWindowTitle(tr("Details"))
self.resize(502, 295) self.resize(502, 295)
self.setMinimumSize(QSize(250, 250)) self.setMinimumSize(QSize(250, 250))
# self.verticalLayout = QVBoxLayout(self)
# self.verticalLayout.setSpacing(0)
# self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.tableView = DetailsTable(self) self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True) self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False) self.tableView.setShowGrid(False)
# self.verticalLayout.addWidget(self.tableView)
# self.centralWidget = QWidget()
# self.centralWidget.setLayout(self.verticalLayout)
self.setWidget(self.tableView) self.setWidget(self.tableView)

View File

@ -128,11 +128,7 @@ class DetailsDialog(DetailsDialogBase):
# This works when expanding but it's ugly: # This works when expanding but it's ugly:
if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width(): if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width():
# print(f"""Before selected size: {self.selectedImageViewer.size()}\n""",
# f"""Before reference size: {self.referenceImageViewer.size()}""")
self.selectedImageViewer.resize(self.referenceImageViewer.size()) self.selectedImageViewer.resize(self.referenceImageViewer.size())
# print(f"""After selected size: {self.selectedImageViewer.size()}\n""",
# f"""After reference size: {self.referenceImageViewer.size()}""")
# model --> view # model --> view
def refresh(self): def refresh(self):

View File

@ -19,14 +19,8 @@ class DetailsDialog(DetailsDialogBase):
self.setWindowTitle(tr("Details")) self.setWindowTitle(tr("Details"))
self.resize(502, 186) self.resize(502, 186)
self.setMinimumSize(QSize(200, 0)) self.setMinimumSize(QSize(200, 0))
# self.verticalLayout = QVBoxLayout()
# self.verticalLayout.setSpacing(0)
# self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.tableView = DetailsTable(self) self.tableView = DetailsTable(self)
self.tableView.setAlternatingRowColors(True) self.tableView.setAlternatingRowColors(True)
self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
self.tableView.setShowGrid(False) self.tableView.setShowGrid(False)
# self.verticalLayout.addWidget(self.tableView)
# self.centralWidget = QWidget()
# self.centralWidget.setLayout(self.verticalLayout)
self.setWidget(self.tableView) self.setWidget(self.tableView)

View File

@ -230,8 +230,6 @@ class TabWindow(QMainWindow):
# menu or shortcut. But this is useless if we don't have a button # menu or shortcut. But this is useless if we don't have a button
# set up to make a close request anyway. This check could be removed. # set up to make a close request anyway. This check could be removed.
return return
# current_widget.close() # seems unnecessary
# self.tabWidget.widget(index).hide()
self.removeTab(index) self.removeTab(index)
@pyqtSlot() @pyqtSlot()

View File

@ -69,21 +69,6 @@ class AboutBox(QDialog):
self.verticalLayout.addWidget(self.label_3) self.verticalLayout.addWidget(self.label_3)
self.label_3.setText(tr("Licensed under GPLv3")) self.label_3.setText(tr("Licensed under GPLv3"))
self.label = QLabel(self) self.label = QLabel(self)
self.label_4 = QLabel(self)
self.label_4.setWordWrap(True)
self.label_4.setTextFormat(Qt.RichText)
self.label_4.setOpenExternalLinks(True)
self.label_4.setText(tr(
"""<img src=":/exchange" alt="Exchange" width="16" height="16"> icon
made by <a href="http://jasoncho.ca/"> Jason Cho</a> (used with permission).
<br>
<img src=":/zoom_in" alt="Zoom In" width="16" height="16">
<img src=":/zoom_out" alt="Zoom Out" width="16" height="16">
<img src=":/zoom_best_fit" alt="Zoomt Best Fit" width="16" height="16">
<img src=":/zoom_original" alt="Zoom Original" width="16" height="16">
icons made by <a href="https://findicons.com/pack/1035/human_o2">schollidesign</a>
(licensed under GPL)."""))
self.verticalLayout.addWidget(self.label_4)
font = QFont() font = QFont()
font.setWeight(75) font.setWeight(75)
font.setBold(True) font.setBold(True)

View File

@ -121,8 +121,10 @@ class Preferences(QObject):
self._settings.setValue(name, normalize_for_serialization(value)) self._settings.setValue(name, normalize_for_serialization(value))
def saveGeometry(self, name, widget): def saveGeometry(self, name, widget):
# We save geometry under a 5-sized int array: first item is a flag for whether the widget # We save geometry under a 7-sized int array: first item is a flag
# is maximized and the other 4 are (x, y, w, h). # for whether the widget is maximized, second item is a flag for whether
# the widget is docked, third item is a Qt::DockWidgetArea enum value,
# and the other 4 are (x, y, w, h).
m = 1 if widget.isMaximized() else 0 m = 1 if widget.isMaximized() else 0
d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0 d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0
area = widget.parent.dockWidgetArea(widget) if d else 0 area = widget.parent.dockWidgetArea(widget) if d else 0

View File

@ -2,3 +2,4 @@ pytest>=5,<6
flake8 flake8
tox-travis tox-travis
black black
pyinstaller>=4.0,<5.0; sys_platform == 'win32'

View File

@ -2,4 +2,6 @@ Send2Trash>=1.3.0
sphinx>=1.2.2 sphinx>=1.2.2
polib>=1.0.4 polib>=1.0.4
hsaudiotag3k>=1.1.3 hsaudiotag3k>=1.1.3
distro>=1.5.0 distro>=1.5.0
PyQt5 >=5.4,<6.0; sys_platform == 'win32'
pywin32>=200; sys_platform == 'win32'

22
tox.ini
View File

@ -1,35 +1,19 @@
[tox] [tox]
envlist = style,py36,py37,py38 envlist = py36,py37,py38,py39
skipsdist = True skipsdist = True
skip_missing_interpreters = True skip_missing_interpreters = True
[testenv] [testenv]
whitelist_externals =
make
setenv = setenv =
PYTHON="{envpython}" PYTHON="{envpython}"
commands = commands =
make modules python build.py --modules
flake8
{posargs:py.test core hscommon} {posargs:py.test core hscommon}
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-extra.txt -r{toxinidir}/requirements-extra.txt
[testenv:style]
deps =
{[testenv]deps}
flake8
commands =
flake8
[testenv:WINDOWS]
deps =
{[testenv]deps}
-r{toxinidir}/requirements-windows.txt
commands =
python build.py
python package.py
[flake8] [flake8]
exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg exclude = .tox,env,build,cocoalib,cocoa,help,./qt/dg_rc.py,cocoa/run_template.py,./pkg
max-line-length = 120 max-line-length = 120