MTGProxyPrinter

Changes On Branch port_pyside6
Login

Changes On Branch port_pyside6

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch port_pyside6 Excluding Merge-Ins

This is equivalent to a diff from cf0e6f4403 to 8b1031c310

2024-10-18
15:54
Tests: Simplified asserts in test_document.py check-in: c9928b0eb7 user: thomas tags: trunk
2024-10-16
11:48
Merge with trunk Leaf check-in: 8b1031c310 user: thomas tags: port_pyside6
2024-10-09
13:08
Clarify the scope of the default card language option in the settings. Add a note about card language of imported deck lists. check-in: cf0e6f4403 user: thomas tags: trunk
2024-10-03
14:45
Implement asynchronous card database migrations with progress reporting. Implements [f460c3e0caf7b1dd] check-in: 40729f514a user: thomas tags: trunk
2024-09-14
12:57
Release v0.29.1 check-in: 45b2a6f419 user: thomas tags: port_pyside6

Changes to README.md.

86
87
88
89
90
91
92
93

94
95
96
97
98
99
100
86
87
88
89
90
91
92

93
94
95
96
97
98
99
100







-
+







- Python >= 3.8

These external libraries are used in the code. They can be installed from PyPI.

- `platformdirs`
- `ijson`
- `pint`
- `PyQt5`
- `PySide6`
- `delegateto`
- `PyHamcrest`
- `cx_Freeze` (Stand-alone bundles only. Used by the installer for Windows®-based platforms.)

### System libraries

- `SQLite3` >= 3.35.0

Changes to build_MTGProxyPrinter_packages.bat.

1
2
3
4
5


6
7
8
9
10
11
12
1
2
3


4
5
6
7
8
9
10
11
12



-
-
+
+







:: Generate an application bundle using cx_Freeze

:: Create or activate the build environment
IF EXIST "venv" (
  call venv\Scripts\activate.bat
IF EXIST "venv-PySide6" (
  call venv-PySide6\Scripts\activate.bat
) ELSE (
  call create_development_environment.bat
)

:: Create a platform-dependent, portable build in the build directory
:: and an MSI-based installer in the dist directory.
:: Also creates a cross-platform Python sdist and wheel package in the dist directory.

Changes to build_MTGProxyPrinter_packages.sh.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







#!/bin/bash
ENVIRONMENT_NAME="venv"
ENVIRONMENT_NAME="venv-PySide6"
# Generate an application bundle using cx_Freeze for Linux.

if [ ! -e "${ENVIRONMENT_NAME}" ]; then
  ./create_development_environment.sh
fi

source "${ENVIRONMENT_NAME}/bin/activate"

Changes to create_development_environment.bat.

1

2
3

4
5
6
7
8
9
10

1
2

3
4
5
6
7
8
9
10
-
+

-
+







python -m venv venv
python -m venv venv-PySide6

call venv\Scripts\activate.bat
call venv-PySide6\Scripts\activate.bat

python -m pip install --upgrade pip
python -m pip install wheel
python -m pip install "pip-tools >= 7"
python -m piptools compile -o requirements.txt pyproject.toml
python -m piptools compile --extra dev -o requirements-dev.txt pyproject.toml
python -m piptools compile --extra package -o requirements-package.txt pyproject.toml

Changes to create_development_environment.sh.

1
2

3
4
5
6
7
8
9
1

2
3
4
5
6
7
8
9

-
+







#!/bin/bash
ENVIRONMENT_NAME="venv"
ENVIRONMENT_NAME="venv-PySide6"

if [ -e "${ENVIRONMENT_NAME}" ]; then
  echo "Removing already existing virtual environment."
  rm -r "${ENVIRONMENT_NAME}"
fi

python -m venv "${ENVIRONMENT_NAME}"

Changes to doc/ThirdPartyLicenses.md.

507
508
509
510
511
512
513
514

515
516

517
518

519
520

521
522
523
524
525
526
527
528
529
507
508
509
510
511
512
513

514
515

516
517

518


519


520
521
522
523
524
525
526







-
+

-
+

-
+
-
-
+
-
-







whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

 

# PyQt5
# Qt6, PySide6

Copyright (c) [Riverbank Computing Limited](https://www.riverbankcomputing.com/).
Qt and PySide6 are available under the GNU Lesser General Public License version 3.

The included copy is licensed under the terms of the 
The Qt Toolkit is Copyright (C) 2018 The Qt Company Ltd. and other contributors.
GNU General Public License version 3 as published by the Free Software Foundation.

Contact: https://www.qt.io/licensing/
See LICENSE.md (when viewing from the source code archive) or the License tab in the
MTGProxyPrinter About dialog for details.

 

# cx_Freeze

Note: This section only applies to application bundles and installers created 
using cx_Freeze, like the stand-alone installer provided for Windows®-based platforms.  

Changes to mtg_proxy_printer/__main__.py.

16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
16
17
18
19
20
21
22

23

24
25
26
27
28
29
30







-
+
-







# Import and implicitly load the settings first, before importing any modules that pull in GUI classes.
import mtg_proxy_printer.settings

import os
import platform
import sys

from PyQt5.QtCore import Qt, QTimer
from PySide6.QtCore import QTimer
from PyQt5.QtWidgets import QApplication

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.argument_parser import parse_args
import mtg_proxy_printer.logger
from mtg_proxy_printer.application import Application
import mtg_proxy_printer.natsort

56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

75
76
77
78
79
55
56
57
58
59
60
61



62
63
64
65
66
67
68
69

70
71
72
73
74
75







-
-
-








-
+






def main():
    global _app
    arguments = parse_args()
    mtg_proxy_printer.app_dirs.migrate_from_old_appdirs()
    mtg_proxy_printer.logger.configure_root_logger()
    handle_ssl_certificates()
    # According to https://doc.qt.io/qt-5/qt.html#ApplicationAttribute-enum,
    # Qt.AA_EnableHighDpiScaling has to be set prior to creating the QApplication instance
    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
    _app = Application(arguments, sys.argv)
    if arguments.test_exit_on_launch:
        logger.info("Skipping startup tasks, because immediate application exit was requested.")
        QTimer.singleShot(0, _app.main_window.on_action_quit_triggered)
    else:
        logger.debug("Enqueueing startup tasks.")
        _app.enqueue_startup_tasks(arguments)
    logger.debug("Initialisation done. Starting event loop.")
    _app.exec_()
    _app.exec()
    logger.debug("Left event loop.")


if __name__ == "__main__":
    main()

Changes to mtg_proxy_printer/application.py.

19
20
21
22
23
24
25
26
27
28



29
30
31
32
33
34
35
19
20
21
22
23
24
25



26
27
28
29
30
31
32
33
34
35







-
-
-
+
+
+







import pathlib
import platform
import shutil
import sys
from tempfile import mkdtemp
import typing

from PyQt5.QtCore import pyqtSlot as Slot, Qt, QTimer, QStringListModel, QThreadPool, QTranslator, QLocale, QLibraryInfo
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from PySide6.QtCore import Slot, QTimer, QStringListModel, QThreadPool, QTranslator, QLocale, QLibraryInfo
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon

from mtg_proxy_printer.argument_parser import Namespace
from mtg_proxy_printer import meta_data
import mtg_proxy_printer.model.carddb
import mtg_proxy_printer.carddb_migrations
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.imagedb
54
55
56
57
58
59
60
61
62
63
64
65
66
67







68
69
70
71
72
73
74
54
55
56
57
58
59
60






61
62
63
64
65
66
67
68
69
70
71
72
73
74
75







-
-
-
-
-
-

+
+
+
+
+
+
+








class Application(QApplication):

    def __init__(self, args: Namespace, argv: typing.List[str] = None):
        if argv is None:
            argv = sys.argv
        logger.info(f"Starting MTGProxyPrinter version {meta_data.__version__}")
        if not os.getenv("QT_QPA_PLUGIN") and "-platform" not in argv and platform.system() == "Windows":
            logger.info("Running on Windows without explicit platform override. Enabling dark mode rendering.")
            # The explicit set platform and parameters overwrite the environment, so set these options iff neither
            # present as parameters nor environment variables.
            argv.append("-platform")
            argv.append("windows:darkmode=1")
        super().__init__(argv)
        # Note: The sub-expression '"-style" not in argv' breaks, if "-style" is passed as a value for another
        # argument or as a positional argument.
        # (For example, if the user wants to load a document with file name "-style" via command line argument.)
        if platform.system() == "Windows" and "-style" not in argv and not os.getenv("QT_STYLE_OVERRIDE"):
            logger.info("Running on Windows without explicit style set. Use 'fusion', which supports dark mode.")
            # Set a dark-mode compatible style, if on Windows and the user does not override the style.
            self.setStyle("fusion")
        # Used by the with_database_write_lock decorator to not start un-frozen,
        # waiting tasks when the application is about to exit
        self.should_run = True
        self.args = args
        self._setup_translations()
        self._setup_icons()
        self.language_model = self._create_language_model()  # TODO: Can this be removed?
217
218
219
220
221
222
223
224

225
226
227
228
229
230
231
218
219
220
221
222
223
224

225
226
227
228
229
230
231
232







-
+







        logger.info(
            f"Loading localisations. System locale: {system_locale.name()}, selected locale: {locale.name()}. "
            f"Possible display languages are: {locale.uiLanguages()}")
        path = ":" if mtg_proxy_printer.ui.common.HAS_COMPILED_RESOURCES \
            else str(pathlib.Path(mtg_proxy_printer.__file__).parent / "resources")
        path += "/translations"
        logger.debug(f"Locale search path is '{path}'")
        self._load_translator(locale, "qtbase", QLibraryInfo.location(QLibraryInfo.LibraryLocation.TranslationsPath))
        self._load_translator(locale, "qtbase", QLibraryInfo.location(QLibraryInfo.LibraryPath.TranslationsPath))
        self._load_translator(locale, "mtgproxyprinter", path)

    def _load_translator(self, locale: QLocale, component: str, path: str):
        translator = QTranslator(self)
        if translator.load(locale, component, '_', path):
            logger.debug(f"{component} translation loaded successfully, installing it")
            self.installTranslator(translator)
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
255
256
257
258
259
260
261


262
263
264
265
266
267
268







-
-







                theme_search_paths = QIcon.themeSearchPaths()
                theme_search_paths.append(mtg_proxy_printer.ui.common.ICON_PATH_PREFIX)
                QIcon.setThemeSearchPaths(theme_search_paths)
            QIcon.setThemeName("breeze")
        else:
            logger.debug(f"Using system-provided icon theme '{fallback_icon_theme}'")

        self.setAttribute(Qt.AA_UseHighDpiPixmaps)

    @Slot()
    def quit(self):
        logger.info("About to exit.")
        self.should_run = False
        self.main_window.hide()
        self.main_window.close()
        self.closeAllWindows()

Changes to mtg_proxy_printer/card_info_downloader.py.

24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38







-
+







import socket
import typing
import urllib.error
import urllib.parse
import urllib.request

import ijson
from PyQt5.QtCore import pyqtSignal as Signal, QObject, Qt, QThreadPool
from PySide6.QtCore import Signal, QObject, Qt, QThreadPool

from mtg_proxy_printer.downloader_base import DownloaderBase
from mtg_proxy_printer.model.carddb import CardDatabase, SCHEMA_NAME, with_database_write_lock
from mtg_proxy_printer.sqlite_helpers import cached_dedent
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
import mtg_proxy_printer.metered_file
from mtg_proxy_printer.logger import get_logger

Changes to mtg_proxy_printer/carddb_migrations.py.

28
29
30
31
32
33
34
35

36
37
38
39
40
41
42
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42







-
+







import time
import typing
import urllib.error
import urllib.parse
from textwrap import dedent
from typing import List, Dict, Union, Tuple, Any, Generator, Callable

from PyQt5.QtCore import QCoreApplication, Qt
from PySide6.QtCore import QCoreApplication, Qt

try:
    from typing import LiteralString
except AttributeError:
    from typing_extensions import LiteralString

from mtg_proxy_printer.progress_meter import ProgressMeter

Changes to mtg_proxy_printer/decklist_downloader.py.

24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38







-
+







import urllib.parse
from io import StringIO
import platform
import re
import typing

import ijson
from PyQt5.QtGui import QValidator
from PySide6.QtGui import QValidator

from mtg_proxy_printer.downloader_base import DownloaderBase
from mtg_proxy_printer.decklist_parser.common import ParserBase
from mtg_proxy_printer.decklist_parser.csv_parsers import ScryfallCSVParser, TappedOutCSVParser
from mtg_proxy_printer.decklist_parser.re_parsers import MTGArenaParser, MagicWorkstationDeckDataFormatParser, \
    XMageParser
from mtg_proxy_printer.logger import get_logger

Changes to mtg_proxy_printer/decklist_parser/common.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from abc import abstractmethod
import typing

from PyQt5.QtCore import QObject, pyqtSignal as Signal
from PySide6.QtCore import QObject, Signal

from mtg_proxy_printer.model.carddb import Card, CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.imagedb import ImageDatabase
import mtg_proxy_printer.settings
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

Changes to mtg_proxy_printer/decklist_parser/csv_parsers.py.

14
15
16
17
18
19
20
21

22
23
24
25
26
27
28
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28







-
+







# along with this program. If not, see <http://www.gnu.org/licenses/>.

import abc
import collections
import csv
import typing

from PyQt5.QtCore import QObject, QCoreApplication
from PySide6.QtCore import QObject, QCoreApplication

from mtg_proxy_printer.model.carddb import Card, CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.imagedb import ImageDatabase

from .common import ParsedDeck, ParserBase
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)

Changes to mtg_proxy_printer/decklist_parser/re_parsers.py.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
from collections import Counter
import re
import typing

from PyQt5.QtCore import QObject, QCoreApplication
from PySide6.QtCore import QObject, QCoreApplication

from mtg_proxy_printer.decklist_parser.common import ParsedDeck, ParserBase
from mtg_proxy_printer.model.carddb import Card, CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger
170
171
172
173
174
175
176
177
178
179




180
181
182
183
184
185
186
170
171
172
173
174
175
176



177
178
179
180
181
182
183
184
185
186
187







-
-
-
+
+
+
+









class MagicWorkstationDeckDataFormatParser(GenericRegularExpressionDeckParser):

    @staticmethod
    def supported_file_types() -> typing.Dict[str, typing.List[str]]:
        return {
        QCoreApplication.translate(
            "MagicWorkstationDeckDataFormatParser", "Magic Workstation Deck Data Format"): ["mwDeck"],
    }
            QCoreApplication.translate(
                "MagicWorkstationDeckDataFormatParser", "Magic Workstation Deck Data Format"): ["mwDeck"],
        }

    PREFIXES_TO_SKIP = frozenset({"//"})

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase, parent: QObject = None):
        super().__init__(
            card_db, image_db,
            re.compile(r"(SB: {2})?(?P<copies>\d+) \[(?P<set_code>\w+)?] (?P<name>.+)"), parent
        )
249
250
251
252
253
254
255
256


257
258
259
260
261
262
263
250
251
252
253
254
255
256

257
258
259
260
261
262
263
264
265







-
+
+







    A parser for XMage deck files (file extension ".dck").
    """

    @staticmethod
    def supported_file_types() -> typing.Dict[str, typing.List[str]]:
        return {
            QCoreApplication.translate("XMageParser", "XMage Deck file"): ["dck"],
        }
            }

    PREFIXES_TO_SKIP = frozenset(("NAME", "LAYOUT"))

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase, parent: QObject = None):
        super().__init__(
            card_db, image_db,
            re.compile(r"(SB: )?(?P<copies>\d+) \[(?P<set_code>\w+):(?P<collector_number>[^]]+)] (?P<name>.+)"), parent
        )

Changes to mtg_proxy_printer/document_controller/_interface.py.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29







-
+








from abc import abstractmethod
from functools import partial
import itertools
import operator
import typing

from PyQt5.QtCore import QCoreApplication
from PySide6.QtCore import QCoreApplication

from mtg_proxy_printer.units_and_sizes import StringList

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document

try:

Changes to mtg_proxy_printer/document_controller/move_cards.py.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import functools
import itertools
import typing

from PyQt5.QtCore import QModelIndex
from PySide6.QtCore import QModelIndex

from ._interface import DocumentAction, IllegalStateError, Self
from mtg_proxy_printer.logger import get_logger

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document_page import Page
    from mtg_proxy_printer.model.document import Document

Changes to mtg_proxy_printer/document_controller/replace_card.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import functools
import typing

from PyQt5.QtCore import Qt
from PySide6.QtCore import Qt

from mtg_proxy_printer.model.carddb import Card
from mtg_proxy_printer.model.card_list import PageColumns
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document_page import CardContainer
    from mtg_proxy_printer.model.document import Document

Changes to mtg_proxy_printer/document_controller/shuffle_document.py.

18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
18
19
20
21
22
23
24

25
26
27
28
29
30
31
32







-
+







    from random import randbytes
except ImportError:
    # Compatibility with Py 3.8
    from secrets import token_bytes as randbytes

import typing

from PyQt5.QtCore import Qt, QModelIndex
from PySide6.QtCore import Qt, QModelIndex

from ._interface import DocumentAction, IllegalStateError, Self
from mtg_proxy_printer.model.carddb import Card
from mtg_proxy_printer.model.card_list import PageColumns
from mtg_proxy_printer.model.document_page import CardContainer
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.units_and_sizes import PageType

Changes to mtg_proxy_printer/downloader_base.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import gzip

from PyQt5.QtCore import QObject, pyqtSignal as Signal
from PySide6.QtCore import QObject, Signal

import mtg_proxy_printer.http_file
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

# Offer accepting gzip, as that is supported by the Scryfall server and reduces network data use by 80-90%

Changes to mtg_proxy_printer/http_file.py.

22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36







-
+







import socket
import time
import typing
from typing import List, Optional, Dict
import urllib.error
import urllib.request

from PyQt5.QtCore import QObject, pyqtSignal as Signal
from PySide6.QtCore import QObject, Signal
import delegateto

from mtg_proxy_printer.meta_data import USER_AGENT
from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger

Changes to mtg_proxy_printer/meta_data.py.

10
11
12
13
14
15
16
17

18
19
20
21
22
10
11
12
13
14
15
16

17
18
19
20
21
22







-
+





# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

PROGRAMNAME = "MTGProxyPrinter"
__version__ = "0.29.1"
__version__ = "0.29.1+PySide6"
COPYRIGHT = "(C) 2020-2024 Thomas Hess"
HOME_PAGE = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter"

DOWNLOAD_WEB_PAGE = f"{HOME_PAGE}/uv/download.html"
USER_AGENT = f"{PROGRAMNAME}/{__version__} ({HOME_PAGE})"

Changes to mtg_proxy_printer/metered_file.py.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


from typing import Iterable, List, Optional, BinaryIO, Union
from io import BufferedIOBase

from PyQt5.QtCore import QObject, pyqtSignal as Signal
from PySide6.QtCore import QObject, Signal
from delegateto import delegate

from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger
__all__ = [

Changes to mtg_proxy_printer/missing_images_manager.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing

from PyQt5.QtCore import QObject, pyqtSignal as Signal, pyqtSlot as Slot
from PySide6.QtCore import QObject, Signal, Slot

from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [

Changes to mtg_proxy_printer/model/card_list.py.

13
14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
13
14
15
16
17
18
19


20
21
22
23
24
25
26
27
28







-
-
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import enum
import itertools
import typing

from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, pyqtSlot as Slot, pyqtSignal as Signal, QItemSelection
from PyQt5.QtGui import QIcon
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Slot, Signal, QItemSelection
from PySide6.QtGui import QIcon

from mtg_proxy_printer.model.carddb import Card, CardIdentificationData, CardDatabase, AnyCardType
from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)
del get_logger
CardList = typing.List[Card]
95
96
97
98
99
100
101
102

103
104
105
106
107
108
109
95
96
97
98
99
100
101

102
103
104
105
106
107
108
109







-
+







                return self.tr("Front") if card.is_front else self.tr("Back")
        if card.is_oversized:
            if role == ItemDataRole.ToolTipRole:
                return self.tr("Beware: Potentially oversized card!\nThis card may not fit in your deck.")
            elif role == ItemDataRole.DecorationRole:
                return self._oversized_icon

    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
    def flags(self, index: QModelIndex) -> ItemFlag:
        flags = super().flags(index)
        if index.column() in self.EDITABLE_COLUMNS:
            flags |= ItemFlag.ItemIsEditable
        return flags

    def setData(self, index: QModelIndex, value: typing.Any, role: ItemDataRole = ItemDataRole.EditRole) -> bool:
        column = index.column()

Changes to mtg_proxy_printer/model/carddb.py.

20
21
22
23
24
25
26
27
28
29



30
31
32
33
34
35
36
20
21
22
23
24
25
26



27
28
29
30
31
32
33
34
35
36







-
-
-
+
+
+







import itertools
import functools
import pathlib
import sqlite3
import threading
import typing

from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QPixmap, QColor, QTransform, QPainter, QColorConstants
from PyQt5.QtCore import Qt, QPoint, QRect, QSize, QPointF, QObject, pyqtSignal as Signal, pyqtSlot as Slot
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QPixmap, QColor, QTransform, QPainter, QColorConstants
from PySide6.QtCore import Qt, QPoint, QRect, QSize, QPointF, QObject, Signal, Slot

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.imagedb import CacheContent

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.natsort import natural_sorted
import mtg_proxy_printer.meta_data
148
149
150
151
152
153
154
155

156
157
158
159
160
161
162
148
149
150
151
152
153
154

155
156
157
158
159
160
161
162







-
+







        sample_area = self.image_file.copy(QRect(
            QPoint(
                round(self.image_file.width() * corner.value[0]),
                round(self.image_file.height() * corner.value[1])),
            QSize(10, 10)
        ))
        average_color = sample_area.scaled(
            1, 1, transformMode=Qt.TransformationMode.SmoothTransformation).toImage().pixelColor(0, 0)
            1, 1, mode=Qt.TransformationMode.SmoothTransformation).toImage().pixelColor(0, 0)
        return average_color

    def display_string(self):
        return f'"{self.name}" [{self.set.code.upper()}:{self.collector_number}]'

    @property
    def set_code(self):
225
226
227
228
229
230
231
232

233
234
235
236
237
238
239
225
226
227
228
229
230
231

232
233
234
235
236
237
238
239







-
+







        # Cards thus can’t be scaled using a singular factor of sqrt(2) on both axis.
        # The scaled cards get a bit compressed horizontally.
        vertical_scaling_factor = card_size.width() / card_size.height()
        horizontal_scaling_factor = card_size.height()/(card_size.width()*2)
        combined_image = QPixmap(card_size)
        combined_image.fill(QColor.fromRgb(255, 255, 255, 0))  # Fill with fully transparent white
        painter = QPainter(combined_image)
        painter.setRenderHints(RenderHint.SmoothPixmapTransform | RenderHint.HighQualityAntialiasing)
        painter.setRenderHints(RenderHint.SmoothPixmapTransform | RenderHint.Antialiasing)
        transformation = QTransform()
        transformation.rotate(90)
        transformation.scale(horizontal_scaling_factor, vertical_scaling_factor)
        painter.setTransform(transformation)
        painter.drawPixmap(QPointF(card_size.width(), -card_size.height()), self.back.image_file)
        painter.drawPixmap(QPointF(0, -card_size.height()), self.front.image_file)

Changes to mtg_proxy_printer/model/document.py.

19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
19
20
21
22
23
24
25

26
27
28
29
30
31
32
33







-
+







import itertools
import math
import pathlib
import sys
import textwrap
import typing

from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, pyqtSlot as Slot, pyqtSignal as Signal, \
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Slot, Signal, \
    QPersistentModelIndex

import mtg_proxy_printer.sqlite_helpers
from mtg_proxy_printer.model.document_page import CardContainer, Page, PageList
from mtg_proxy_printer.units_and_sizes import PageType
from mtg_proxy_printer.model.carddb import AnyCardType, CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card_list import PageColumns

Changes to mtg_proxy_printer/model/document_loader.py.

22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
22
23
24
25
26
27
28


29
30
31
32
33
34
35
36
37







-
-
+
+







import pathlib
import sqlite3
import textwrap
import typing
from unittest.mock import patch

import pint
from PyQt5.QtGui import QPageLayout, QPageSize
from PyQt5.QtCore import QObject, pyqtSignal as Signal, QThreadPool, QMarginsF, QSizeF, Qt
from PySide6.QtGui import QPageLayout, QPageSize
from PySide6.QtCore import QObject, Signal, QThreadPool, QMarginsF, QSizeF, Qt
from hamcrest import assert_that, all_of, instance_of, greater_than_or_equal_to, matches_regexp, is_in, \
    has_properties, is_, any_of

try:
    from hamcrest import contains_exactly
except ImportError:
    # Compatibility with PyHamcrest < 1.10

Changes to mtg_proxy_printer/model/imagedb.py.

22
23
24
25
26
27
28
29
30


31
32
33
34
35
36
37
22
23
24
25
26
27
28


29
30
31
32
33
34
35
36
37







-
-
+
+







import shutil
import socket
import string
import threading
import typing
import urllib.error

from PyQt5.QtCore import QObject, pyqtSignal as Signal, pyqtSlot as Slot, QModelIndex, Qt, QThreadPool
from PyQt5.QtGui import QPixmap, QColorConstants
from PySide6.QtCore import QObject, Signal, Slot, QModelIndex, Qt, QThreadPool
from PySide6.QtGui import QPixmap, QColorConstants

if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.document import Document

from mtg_proxy_printer.model.carddb import with_database_write_lock
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
from mtg_proxy_printer.document_controller.replace_card import ActionReplaceCard

Changes to mtg_proxy_printer/model/string_list.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing

from PyQt5.QtCore import QAbstractListModel, Qt, QObject, QModelIndex
from PySide6.QtCore import QAbstractListModel, Qt, QObject, QModelIndex

from mtg_proxy_printer.model.carddb import MTGSet


__all__ = [
    "PrettySetListModel",
]

Changes to mtg_proxy_printer/natsort.py.

16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30







-
+







"""
Natural sorting for lists or other iterables of strings.
"""

import re
import typing

from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
from PySide6.QtCore import QSortFilterProxyModel, QModelIndex

__all__ = [
    "natural_sorted",
    "str_less_than",
    "NaturallySortedSortFilterProxyModel",
]

Changes to mtg_proxy_printer/print.py.

12
13
14
15
16
17
18
19
20
21



22
23
24
25
26
27
28
12
13
14
15
16
17
18



19
20
21
22
23
24
25
26
27
28







-
-
-
+
+
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import math
from pathlib import Path

from PyQt5.QtCore import QObject, QMarginsF, QSizeF, pyqtSlot as Slot, QPersistentModelIndex
from PyQt5.QtGui import QPainter, QPdfWriter, QPageSize
from PyQt5.QtPrintSupport import QPrinter
from PySide6.QtCore import QObject, QMarginsF, QSizeF, Slot, QPersistentModelIndex
from PySide6.QtGui import QPainter, QPdfWriter, QPageSize
from PySide6.QtPrintSupport import QPrinter

import mtg_proxy_printer.meta_data
from mtg_proxy_printer.settings import settings
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_loader import PageLayoutSettings
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene
from mtg_proxy_printer.logger import get_logger
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
59
60
61
62
63
64
65

66
67
68
69
70
71
72







-







            f"Setting page layout failed! "
            f"Layout: page_size={page_layout.pageSize().size(QPageSize.Unit.Millimeter)}, "
            f"orientation={page_layout.orientation()}, "
            f"margins={layout.margin_left, layout.margin_top, layout.margin_right, layout.margin_bottom}")
    # magnitude returns a float by default, so round to int to avoid a TypeError
    printer.setResolution(round(mtg_proxy_printer.units_and_sizes.RESOLUTION.magnitude))
    # Disable duplex printing by default
    printer.setDoubleSidedPrinting(False)
    printer.setDuplex(QPrinter.DuplexMode.DuplexNone)
    printer.setOutputFormat(QPrinter.OutputFormat.NativeFormat)
    if RenderMode.IMPLICIT_MARGINS not in renderer.render_mode:
        printer.setFullPage(True)
    return printer


Changes to mtg_proxy_printer/printing_filter_updater.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import sqlite3
import typing

from PyQt5.QtCore import QObject, pyqtSignal as Signal, Qt, QCoreApplication
from PySide6.QtCore import QObject, Signal, Qt, QCoreApplication

import mtg_proxy_printer.settings
if typing.TYPE_CHECKING:
    from mtg_proxy_printer.model.carddb import CardDatabase
    from mtg_proxy_printer.ui.main_window import MainWindow
from mtg_proxy_printer.model.carddb import SCHEMA_NAME, with_database_write_lock
from mtg_proxy_printer.sqlite_helpers import cached_dedent, open_database

Changes to mtg_proxy_printer/resources/ui/central_widget/columnar.ui.

22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36







-
+







       <verstretch>1</verstretch>
      </sizepolicy>
     </property>
     <property name="acceptDrops">
      <bool>false</bool>
     </property>
     <property name="renderHints">
      <set>QPainter::Antialiasing|QPainter::HighQualityAntialiasing</set>
      <set>QPainter::Antialiasing</set>
     </property>
    </widget>
   </item>
   <item row="4" column="2" rowspan="2">
    <widget class="QTableView" name="page_card_table_view">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">

Changes to mtg_proxy_printer/resources/ui/central_widget/grouped.ui.

22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36







-
+







       <verstretch>10</verstretch>
      </sizepolicy>
     </property>
     <property name="acceptDrops">
      <bool>false</bool>
     </property>
     <property name="renderHints">
      <set>QPainter::Antialiasing|QPainter::HighQualityAntialiasing</set>
      <set>QPainter::Antialiasing</set>
     </property>
    </widget>
   </item>
   <item row="1" column="0" rowspan="7" colspan="2">
    <widget class="QListView" name="document_view">
     <property name="sizePolicy">
      <sizepolicy hsizetype="Expanding" vsizetype="Expanding">

Changes to mtg_proxy_printer/resources/ui/document_settings_dialog.ui.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1
2
3
4








5
6
7
8
9
10
11




-
-
-
-
-
-
-
-







<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>DocumentSettingsDialog</class>
 <widget class="QDialog" name="DocumentSettingsDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>501</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Set Document settings</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="PageConfigContainer" name="page_config_container" native="true"/>
   </item>
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
32
33
34
35
36
37
38










39
40
41
42
43
44










45
46
47







-
-
-
-
-
-
-
-
-
-






-
-
-
-
-
-
-
-
-
-



 <resources/>
 <connections>
  <connection>
   <sender>button_box</sender>
   <signal>accepted()</signal>
   <receiver>DocumentSettingsDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>button_box</sender>
   <signal>rejected()</signal>
   <receiver>DocumentSettingsDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>

Changes to mtg_proxy_printer/runner.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing

from PyQt5.QtCore import QRunnable, QObject, pyqtSignal as Signal
from PySide6.QtCore import QRunnable, QObject, Signal

from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [
    "Runnable",

Changes to mtg_proxy_printer/settings.py.

17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31







-
+







import math
import pathlib
import re
import typing
import tokenize

import pint
from PyQt5.QtCore import QStandardPaths
from PySide6.QtCore import QStandardPaths

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.meta_data
import mtg_proxy_printer.natsort
from mtg_proxy_printer.units_and_sizes import CardSizes, ConfigParser, SectionProxy, unit_registry, T, QuantityT, UnitT

__all__ = [

Changes to mtg_proxy_printer/ui/add_card.py.

11
12
13
14
15
16
17
18
19
20



21
22
23
24
25
26
27
11
12
13
14
15
16
17



18
19
20
21
22
23
24
25
26
27







-
-
-
+
+
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from typing import Union, Type, Optional

from PyQt5.QtCore import QStringListModel, pyqtSlot as Slot, pyqtSignal as Signal, Qt, QItemSelectionModel, QItemSelection
from PyQt5.QtWidgets import QWidget, QDialogButtonBox
from PyQt5.QtGui import QIcon
from PySide6.QtCore import QStringListModel, Slot, Signal, Qt, QItemSelectionModel, QItemSelection
from PySide6.QtWidgets import QWidget, QDialogButtonBox
from PySide6.QtGui import QIcon

from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
import mtg_proxy_printer.model.string_list
import mtg_proxy_printer.model.carddb
import mtg_proxy_printer.model.document
import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.common import load_ui_from_file

Changes to mtg_proxy_printer/ui/cache_cleanup_wizard.py.

17
18
19
20
21
22
23
24
25
26



27
28
29
30
31
32
33
17
18
19
20
21
22
23



24
25
26
27
28
29
30
31
32
33







-
-
-
+
+
+







import datetime
import enum
import functools
import math
import pathlib
import typing

from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex, QObject, QBuffer, QIODevice, QItemSelectionModel, QSize
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtWidgets import QWidget, QWizard, QWizardPage
from PySide6.QtCore import QAbstractTableModel, Qt, QModelIndex, QObject, QBuffer, QIODevice, QItemSelectionModel, QSize
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtWidgets import QWidget, QWizard, QWizardPage

import mtg_proxy_printer.settings
from mtg_proxy_printer.natsort import NaturallySortedSortFilterProxyModel
from mtg_proxy_printer.model.carddb import CardDatabase, Card, MTGSet
from mtg_proxy_printer.model.imagedb import ImageDatabase, CacheContent as ImageCacheContent, ImageKey
from mtg_proxy_printer.ui.common import load_ui_from_file, format_size, WizardBase
from mtg_proxy_printer.units_and_sizes import OptStr
59
60
61
62
63
64
65
66

67
68
69
70
71
72
73
59
60
61
62
63
64
65

66
67
68
69
70
71
72
73







-
+







    source = QPixmap(str(path))
    pixmap = source.scaled(
        source.width() // scaling_factor, source.height() // scaling_factor,
        Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
    buffer = QBuffer()
    buffer.open(QIODevice.OpenModeFlag.WriteOnly)
    pixmap.save(buffer, "PNG", quality=100)
    image = buffer.data().toBase64().data().decode()
    image = buffer.data().toBase64().toStdString()
    card_name = f'<p style="text-align:center">{card_name}</p><br>' if card_name else ""
    tooltip_text = f'{card_name}<img src="data:image/png;base64,{image}">'
    return tooltip_text


class KnownCardColumns(enum.IntEnum):
    Name = 0

Changes to mtg_proxy_printer/ui/central_widget.py.

15
16
17
18
19
20
21
22

23
24
25


26
27
28
29
30
31
32
15
16
17
18
19
20
21

22
23


24
25
26
27
28
29
30
31
32







-
+

-
-
+
+








import functools
import math
import operator
import pathlib
from typing import Union, Type, Optional

from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot, QPersistentModelIndex, QItemSelectionModel, \
from PySide6.QtCore import Signal, Slot, QPersistentModelIndex, QItemSelectionModel, \
    QModelIndex, QPoint, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QAction, QMenu, QInputDialog, QFileDialog
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QWidget, QMenu, QInputDialog, QFileDialog

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.settings
from mtg_proxy_printer.model.card_list import PageColumns
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.carddb import CardDatabase, Card, CardList, CheckCard, AnyCardType, AnyCardTypeForTypeCheck
from mtg_proxy_printer.model.imagedb import ImageDatabase

Changes to mtg_proxy_printer/ui/common.py.

13
14
15
16
17
18
19
20
21
22



23
24

25
26
27
28
29
30
31
13
14
15
16
17
18
19



20
21
22


23
24
25
26
27
28
29
30







-
-
-
+
+
+
-
-
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import pathlib
import platform
import typing

from PyQt5.QtCore import QFile, QUrl, QObject, QSize, QCoreApplication
from PyQt5.QtWidgets import QLabel, QWizard, QWidget, QGraphicsColorizeEffect, QTextEdit
from PyQt5.QtGui import QIcon
from PySide6.QtCore import QFile, QUrl, QObject, QSize, QCoreApplication
from PySide6.QtWidgets import QLabel, QWizard, QWidget, QGraphicsColorizeEffect, QTextEdit
from PySide6.QtGui import QIcon
# noinspection PyUnresolvedReferences
from PyQt5 import uic
from PySide6.QtUiTools import loadUiType

from mtg_proxy_printer.logger import get_logger
logger = get_logger(__name__)
del get_logger

__all__ = [
    "RESOURCE_PATH_PREFIX",
90
91
92
93
94
95
96
97

98
99
100
101
102
103
104
105
106
107

108



109
110
111
112
113
114
115
89
90
91
92
93
94
95

96

97
98
99
100
101
102
103
104
105
106

107
108
109
110
111
112
113
114
115
116







-
+
-









+
-
+
+
+







    if not display_text:
        display_text = str(path)
    label.setText(f"""<a href="{url.path(QUrl.FullyEncoded):s}">{display_text:s}</a>""")


def load_ui_from_file(name: str):
    """
    Returns the Ui class type from uic.loadUiType(), loading the ui file with the given name.
    Returns the Ui class type as returned by PySide6.QtUiTools.loadUiType(), loading the ui file with the given name.

    :param name: Path to the UI file
    :return: class implementing the requested Ui
    :raises FileNotFoundError: If the given ui file does not exist
    """
    file_path = f"{RESOURCE_PATH_PREFIX}/ui/{name}.ui"
    if not QFile.exists(file_path):
        error_message = f"UI file not found: {file_path}"
        logger.error(error_message)
        raise FileNotFoundError(error_message)
    try:
    base_type, _ = uic.loadUiType(file_path, from_imports=True)
        base_type, _ = loadUiType(file_path)
    except TypeError as e:
        raise RuntimeError(f"Ui compilation failed for path {file_path}") from e
    return base_type

def load_icon(name: str) -> QIcon:
    file_path = f"{RESOURCE_PATH_PREFIX}/icons/{name}"
    if not QFile.exists(file_path):
        error_message = f"Icon not found: {file_path}"
        logger.error(error_message)

Changes to mtg_proxy_printer/ui/compiled_resources.pyi.

10
11
12
13
14
15
16
17

18
19
20
21
22
23
24
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24







-
+







# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
Declares the interface created by the PyQt5 resource compiler.
Declares the interface created by the PySide6 resource compiler.
This is only used for type hinting.
"""

import typing

qt_version: typing.List[int] = ...
rcc_version: int = ...

Changes to mtg_proxy_printer/ui/deck_import_wizard.py.

18
19
20
21
22
23
24
25

26
27
28



29
30
31
32
33
34
35
18
19
20
21
22
23
24

25
26


27
28
29
30
31
32
33
34
35
36







-
+

-
-
+
+
+







import math
import pathlib
import re
import typing
import urllib.error
import urllib.parse

from PyQt5.QtCore import pyqtSlot as Slot, pyqtSignal as Signal, pyqtProperty as Property, QStringListModel, Qt, \
from PySide6.QtCore import Slot, Signal, Property, QStringListModel, Qt, SIGNAL, \
    QItemSelection, QSize, QUrl
from PyQt5.QtGui import QValidator, QIcon, QDesktopServices
from PyQt5.QtWidgets import QWizard, QFileDialog, QMessageBox, QWizardPage, QWidget, QRadioButton
from PySide6.QtGui import QValidator, QIcon, QDesktopServices
from PySide6.QtWidgets import QWizard, QFileDialog, QMessageBox, QWizardPage, QWidget, QRadioButton


from mtg_proxy_printer.units_and_sizes import SectionProxy
import mtg_proxy_printer.settings
from mtg_proxy_printer.decklist_parser import re_parsers, common, csv_parsers
from mtg_proxy_printer.decklist_downloader import IsIdentifyingDeckUrlValidator, AVAILABLE_DOWNLOADERS, \
    get_downloader_class, ParserBase
from mtg_proxy_printer.model.carddb import CardDatabase
116
117
118
119
120
121
122
123

124
125
126
127

128
129
130

131
132
133
134
135
136
137
117
118
119
120
121
122
123

124
125
126
127

128
129
130

131
132
133
134
135
136
137
138







-
+



-
+


-
+







        ui.deck_list_download_url_line_edit.textChanged.connect(
            lambda text: ui.deck_list_download_button.setEnabled(
                self.deck_list_url_validator.validate(text)[0] == State.Acceptable))
        supported_sites = "\n".join((downloader.APPLICABLE_WEBSITES for downloader in AVAILABLE_DOWNLOADERS.values()))
        ui.deck_list_download_url_line_edit.setToolTip(
            self.tr("Supported websites:\n{supported_sites}").format(supported_sites=supported_sites))
        ui.translate_deck_list_target_language.setModel(language_model)
        self.registerField("deck_list*", ui.deck_list, "plainText", ui.deck_list.textChanged)
        self.registerField("deck_list*", ui.deck_list)
        self.registerField("print-guessing-enable", ui.print_guessing_enable)
        self.registerField("print-guessing-prefer-already-downloaded", ui.print_guessing_prefer_already_downloaded)
        self.registerField("translate-deck-list-enable", ui.translate_deck_list_enable)
        self.registerField("deck-list-downloaded", self, "deck_list_downloader", self.deck_list_downloader_changed)
        self.registerField("deck-list-downloaded", self, "deck_list_downloader", "deck_list_downloader_changed(str)")
        self.registerField(
            "translate-deck-list-target-language", ui.translate_deck_list_target_language,
            "currentText", ui.translate_deck_list_target_language.currentTextChanged
            "currentText", "currentTextChanged(str)"
        )
        logger.info(f"Created {self.__class__.__name__} instance.")


    @Property(str, notify=deck_list_downloader_changed)
    def deck_list_downloader(self):
        return self._deck_list_downloader
603
604
605
606
607
608
609

610
611
612
613
614
615
616
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618







+







        QWizard.WizardButton.FinishButton: "dialog-ok",
        QWizard.WizardButton.CancelButton: "dialog-cancel",
    }

    def __init__(self, card_db: CardDatabase, image_db: ImageDatabase,
                 language_model: QStringListModel, parent: QWidget = None, flags=Qt.WindowFlags()):
        super().__init__(QSize(1000, 600), parent, flags)
        self.setDefaultProperty("QPlainTextEdit", "plainText", SIGNAL("textChanged()"))
        self.card_db = card_db
        self.select_deck_parser_page = SelectDeckParserPage(card_db, image_db, self)
        self.load_list_page = LoadListPage(language_model, self)
        self.summary_page = SummaryPage(card_db, self)
        self.addPage(self.load_list_page)
        self.addPage(self.select_deck_parser_page)
        self.addPage(self.summary_page)

Changes to mtg_proxy_printer/ui/dialogs.py.

13
14
15
16
17
18
19
20
21
22
23




24
25
26
27
28
29
30
13
14
15
16
17
18
19




20
21
22
23
24
25
26
27
28
29
30







-
-
-
-
+
+
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing
import pathlib
import sys

from PyQt5.QtCore import QFile, pyqtSlot as Slot, QThreadPool, QObject, QEvent, Qt
from PyQt5.QtWidgets import QFileDialog, QWidget, QTextBrowser, QDialogButtonBox, QDialog
from PyQt5.QtGui import QIcon
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrintDialog, QPrinter
from PySide6.QtCore import QFile, Slot, QThreadPool, QObject, QEvent, Qt
from PySide6.QtWidgets import QFileDialog, QWidget, QTextBrowser, QDialogButtonBox, QDialog
from PySide6.QtGui import QIcon
from PySide6.QtPrintSupport import QPrintPreviewDialog, QPrintDialog, QPrinter

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.model.carddb import Card, CardDatabase
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.imagedb
import mtg_proxy_printer.print
import mtg_proxy_printer.settings
245
246
247
248
249
250
251
252

253
254
255
256
257
258
259
245
246
247
248
249
250
251

252
253
254
255
256
257
258
259







-
+







        file_path = self._get_file_path(":/changelog.md", "/../../doc/changelog.md")
        self._set_text_browser_with_markdown_file_content(file_path, self.ui.changelog_text_browser)

    def _set_text_browser_with_markdown_file_content(self, file_path: str, text_browser: QTextBrowser):
        file = QFile(file_path, self)
        file.open(QFile.OpenModeFlag.ReadOnly)
        try:
            content = file.readAll().data().decode("utf-8")
            content = file.readAll().toStdString()
        finally:
            file.close()
        text_browser.setMarkdown(content)


class PrintPreviewDialog(QPrintPreviewDialog):

365
366
367
368
369
370
371
372

373
365
366
367
368
369
370
371

372
373







-
+

        logger.info(f"User accepted the {self.__class__.__name__}")
        action = ActionEditDocumentSettings(self.ui.page_config_container.ui.page_config_widget.page_layout)
        self.document.apply(action)
        logger.debug("Saving settings in the document done.")

    def clear_highlight(self):
        """Clears all GUI widget highlights."""
        for item in self.findChildren((QWidget,), options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
        for item in self.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
            item.setGraphicsEffect(None)

Changes to mtg_proxy_printer/ui/item_delegates.py.

10
11
12
13
14
15
16
17
18


19
20
21
22
23
24
25
10
11
12
13
14
15
16


17
18
19
20
21
22
23
24
25







-
-
+
+







# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import typing

from PyQt5.QtCore import QModelIndex, Qt, QAbstractItemModel, QSortFilterProxyModel
from PyQt5.QtWidgets import QStyledItemDelegate, QWidget, QStyleOptionViewItem, QComboBox
from PySide6.QtCore import QModelIndex, Qt, QAbstractItemModel, QSortFilterProxyModel
from PySide6.QtWidgets import QStyledItemDelegate, QWidget, QStyleOptionViewItem, QComboBox

from mtg_proxy_printer.model.carddb import Card
from mtg_proxy_printer.model.card_list import PageColumns
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger

logger = get_logger(__name__)

Changes to mtg_proxy_printer/ui/main_window.py.

12
13
14
15
16
17
18

19
20
21
22




23
24
25
26
27
28
29
12
13
14
15
16
17
18
19




20
21
22
23
24
25
26
27
28
29
30







+
-
-
-
-
+
+
+
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import pathlib
import typing


from PyQt5.QtCore import pyqtSlot as Slot, pyqtSignal as Signal, QStringListModel, QUrl, Qt, QSize
from PyQt5.QtGui import QCloseEvent, QKeySequence, QDesktopServices, QDragEnterEvent, QDropEvent, QPixmap
from PyQt5.QtWidgets import QApplication, QMessageBox, QAction, QWidget, QMainWindow, QDialog

from PySide6.QtCore import Slot, Signal, QStringListModel, QUrl, Qt, QSize
from PySide6.QtGui import QCloseEvent, QKeySequence, QAction, QDesktopServices, QDragEnterEvent, QDropEvent, QPixmap
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget, QMainWindow, QDialog
from PySide6.QtPrintSupport import QPrintDialog

from mtg_proxy_printer.missing_images_manager import MissingImagesManager
from mtg_proxy_printer.card_info_downloader import CardInfoDownloader
from mtg_proxy_printer.model.carddb import CardDatabase, Card, MTGSet
from mtg_proxy_printer.model.imagedb import ImageDatabase
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.document_controller.compact_document import ActionCompactDocument
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
122
123
124
125
126
127
128

129
130
131
132
133
134
135







-







            (self.ui.action_undo, StandardKey.Undo),
            (self.ui.action_redo, StandardKey.Redo),
        ]
        for action, shortcut in actions_with_shortcuts:
            action.setShortcut(shortcut)

    def _setup_central_widget(self):
        self.setCentralWidget(self.ui.central_widget)
        self.ui.central_widget.set_data(self.document, self.card_database, self.image_db)

    def _setup_loading_state_connections(self):
        for widget_or_action in self._get_widgets_and_actions_disabled_in_loading_state():
            self.loading_state_changed.connect(widget_or_action.setDisabled)

    def _setup_undo_redo_actions(self, document: Document):
262
263
264
265
266
267
268

269

270
271
272
273
274
275
276
262
263
264
265
266
267
268
269

270
271
272
273
274
275
276
277







+
-
+







        action_str = self.tr(
            "printing",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
        if self._ask_user_about_compacting_document(action_str) == StandardButton.Cancel:
            return
        self.current_dialog = PrintDialog(self.document, self)
        self.current_dialog.finished.connect(self.on_dialog_finished)
        # Use the QDialog base class open() method, because QPrintDialog.open() performs additional, unwanted actions.
        self.missing_images_manager.obtain_missing_images(self.current_dialog.open)
        self.missing_images_manager.obtain_missing_images(super(QPrintDialog, self.current_dialog).open)

    @Slot()
    def on_action_print_preview_triggered(self):
        logger.info(f"User views the print preview.")
        action_str = self.tr(
            "printing",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
330
331
332
333
334
335
336
337

338
339
340
341
342
343
344
331
332
333
334
335
336
337

338
339
340
341
342
343
344
345







-
+







                self, self.tr("Download required Card data from Scryfall?"),
                self.tr(
                    "This program requires downloading additional card data from Scryfall to operate the card search.\n"
                    "Download the required data from Scryfall now?\n"
                    "Without the data, you can only print custom cards by drag&dropping "
                    "the image files onto the main window."),
                StandardButton.Yes | StandardButton.No, StandardButton.Yes) == StandardButton.Yes:
            self.ui.action_download_card_data.trigger()
            self.card_data_downloader.import_from_api()

    @Slot()
    def on_action_save_document_triggered(self):
        logger.debug("User clicked on Save")
        if self.document.save_file_path is None:
            logger.debug("No save file path set. Call 'Save as' instead.")
            self.ui.action_save_as.trigger()
425
426
427
428
429
430
431
432

433
434
435
436
437
438
439
426
427
428
429
430
431
432

433
434
435
436
437
438
439
440







-
+







                self, self.tr("New card data available"),
                self.tr(
                    "There are %n new printings available on Scryfall. Update the local data now?",
                    "", estimated_card_count),
                StandardButton.Yes | StandardButton.No, StandardButton.Yes
        ) == StandardButton.Yes:
            logger.info("User agreed to update the card data from Scryfall. Performing update")
            self.ui.action_download_card_data.trigger()
            self.card_data_downloader.import_from_api()
        else:
            # If the user declines to perform the update now, allow them to perform it later by enabling the action.
            self.ui.action_download_card_data.setEnabled(True)

    def ask_user_about_application_update_policy(self):
        """Executed on start when the application update policy setting is set to None, the default value."""
        name = mtg_proxy_printer.meta_data.PROGRAMNAME
515
516
517
518
519
520
521
522

523
524
516
517
518
519
520
521
522

523
524
525







-
+


        regular = mtg_proxy_printer.units_and_sizes.CardSizes.REGULAR
        width, height = regular.width.magnitude, regular.height.magnitude
        for url in mime_data.urls():
            pixmap = QPixmap(url.toLocalFile())
            if not pixmap.isNull():
                if pixmap.width() != width or pixmap.height() != height:
                    new_size = QSize(width, height)
                    pixmap = pixmap.scaled(new_size, transformMode=TransformationMode.SmoothTransformation)
                    pixmap = pixmap.scaled(new_size, mode=TransformationMode.SmoothTransformation)
                result.append(pixmap)
        return result

Changes to mtg_proxy_printer/ui/page_config_container.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from functools import partial

from PyQt5.QtWidgets import QWidget
from PySide6.QtWidgets import QWidget

from mtg_proxy_printer.ui.common import load_ui_from_file
from mtg_proxy_printer.logger import get_logger

try:
    from mtg_proxy_printer.ui.generated.page_config_container import Ui_PageConfigContainer
except ModuleNotFoundError:

Changes to mtg_proxy_printer/ui/page_config_preview_area.py.

12
13
14
15
16
17
18
19
20



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
12
13
14
15
16
17
18


19
20
21
22
23
24
25
26
27
28
29
30
31
32


33
34
35
36
37
38
39







-
-
+
+
+











-
-







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import enum
from unittest.mock import MagicMock

from PyQt5.QtCore import pyqtSlot as Slot, QPersistentModelIndex
from PyQt5.QtGui import QColorConstants, QPainter, QPixmap
from PySide6.QtCore import Slot, QPersistentModelIndex
from PySide6.QtGui import QColorConstants, QPainter, QPixmap
from PySide6.QtWidgets import QWidget

from mtg_proxy_printer.document_controller.page_actions import ActionNewPage
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.model.document_loader import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import CardSizes, CardSize
from mtg_proxy_printer.model.document_page import PageType
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.carddb import Card, MTGSet
from mtg_proxy_printer.ui.common import load_ui_from_file
from mtg_proxy_printer.logger import get_logger

from PyQt5.QtWidgets import QWidget

try:
    from mtg_proxy_printer.ui.generated.page_config_preview_area import Ui_PageConfigPreviewArea
except ModuleNotFoundError:
    Ui_PageConfigPreviewArea = load_ui_from_file("page_config_preview_area")

logger = get_logger(__name__)
del get_logger

Changes to mtg_proxy_printer/ui/page_config_widget.py.

14
15
16
17
18
19
20
21
22


23
24
25
26
27
28
29
14
15
16
17
18
19
20


21
22
23
24
25
26
27
28
29







-
-
+
+







# along with this program. If not, see <http://www.gnu.org/licenses/>.

import functools
from functools import partial
import math
import typing

from PyQt5.QtCore import pyqtSlot as Slot, Qt, pyqtSignal as Signal
from PyQt5.QtWidgets import QGroupBox, QWidget, QDoubleSpinBox, QCheckBox, QLineEdit
from PySide6.QtCore import Slot, Qt, Signal
from PySide6.QtWidgets import QGroupBox, QWidget, QDoubleSpinBox, QCheckBox, QLineEdit

import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.common import load_ui_from_file, BlockedSignals, highlight_widget
from mtg_proxy_printer.model.document_loader import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import CardSizes, PageType, unit_registry, ConfigParser, QuantityT

try:
58
59
60
61
62
63
64
65

66
67
68
69
70

71
72
73

74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91



92

93
94
95
96
97
98
99
58
59
60
61
62
63
64

65
66
67
68
69

70
71
72

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
102







-
+




-
+


-
+


















+
+
+
-
+








        for spinbox, _ in self._get_decimal_settings_widgets():
            layout_key = spinbox.objectName()
            spinbox.valueChanged[float].connect(
                partial(self.set_numerical_page_layout_item, page_layout, layout_key, "mm"))
            spinbox.valueChanged[float].connect(self.validate_paper_size_settings)
            spinbox.valueChanged[float].connect(self.on_page_layout_setting_changed)
            spinbox.valueChanged[float].connect(partial(self.page_layout_changed.emit, page_layout))
            spinbox.valueChanged[float].connect(lambda: self.page_layout_changed.emit(page_layout))

        for checkbox, _ in self._get_boolean_settings_widgets():
            layout_key = checkbox.objectName()
            checkbox.stateChanged.connect(partial(self.set_boolean_page_layout_item, page_layout, layout_key))
            checkbox.stateChanged.connect(partial(self.page_layout_changed.emit, page_layout))
            checkbox.stateChanged.connect(lambda: self.page_layout_changed.emit(page_layout))

        ui.document_name.textChanged.connect(partial(setattr, page_layout, "document_name"))
        ui.document_name.textChanged.connect(partial(self.page_layout_changed.emit, page_layout))
        ui.document_name.textChanged.connect(lambda: self.page_layout_changed.emit(page_layout))
        return page_layout

    @staticmethod
    def set_numerical_page_layout_item(page_layout: PageLayoutSettings, layout_key: str, unit: str, value: float):
        # Implementation note: This call is placed here, because stuffing it into a lambda defined within a while loop
        # somehow uses the wrong references and will set the attribute that was processed last in the loop.
        # This method can be used via functools.partial to reduce the signature to (float) -> None,
        # which can be connected to the valueChanged[float] signal just fine.
        # Also, functools.partial does not exhibit the same issue as the lambda expression shows.
        setattr(page_layout, layout_key, value*unit_registry.parse_units(unit))

    @staticmethod
    def set_boolean_page_layout_item(page_layout: PageLayoutSettings, layout_key: str, value: CheckState):
        # Implementation note: This call is placed here, because stuffing it into a lambda defined within a while loop
        # somehow uses the wrong references and will set the attribute that was processed last in the loop.
        # This method can be used via functools.partial to reduce the signature to (CheckState) -> None,
        # which can be connected to the stateChanged signal just fine.
        # Also, functools.partial does not exhibit the same issue as the lambda expression shows.
        #
        # PySide6 maps the QCheckBox check states to proper Python enums, but the stateChanged Qt signal carries raw
        # integers. To get the integers for comparison, the lambdas below require accessing the CheckState enum values.
        setattr(page_layout, layout_key, value == CheckState.Checked)
        setattr(page_layout, layout_key, value == CheckState.Checked.value)

    @Slot()
    def on_page_layout_setting_changed(self):
        """
        Recomputes and updates the page capacity display, whenever any page layout widget changes.
        """
        regular_capacity = self.page_layout.compute_page_card_capacity(PageType.REGULAR)

Changes to mtg_proxy_printer/ui/page_renderer.py.

13
14
15
16
17
18
19
20
21
22




23
24
25
26
27
28
29
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28
29
30







-
-
-
+
+
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import enum
import typing
from functools import partial

from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QGraphicsView, QWidget, QAction
from PyQt5.QtGui import QWheelEvent, QKeySequence, QPalette, QResizeEvent
from PySide6.QtCore import Qt, QEvent
from PySide6.QtWidgets import QGraphicsView, QWidget
from PySide6.QtGui import QWheelEvent, QKeySequence, QPalette, QResizeEvent, QAction


from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.logger import get_logger
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene

logger = get_logger(__name__)
del get_logger

Changes to mtg_proxy_printer/ui/page_scene.py.

1
2
3
4
5
6
7

8
9
10


11
12
13
14
15
16
17
1
2
3
4
5
6

7
8


9
10
11
12
13
14
15
16
17






-
+

-
-
+
+







import collections
import enum
import functools
import itertools
import typing

from PyQt5.QtCore import Qt, QSizeF, QPointF, QRectF, pyqtSignal as Signal, QObject, pyqtSlot as Slot, \
from PySide6.QtCore import Qt, QSizeF, QPointF, QRectF, Signal, QObject, Slot, \
    QPersistentModelIndex, QModelIndex, QRect, QPoint, QSize
from PyQt5.QtGui import QPen, QColorConstants, QBrush, QColor, QPalette, QFontMetrics, QPixmap, QTransform, QPolygonF
from PyQt5.QtWidgets import QGraphicsItemGroup, QGraphicsItem, QGraphicsPixmapItem, QGraphicsRectItem, \
from PySide6.QtGui import QPen, QColorConstants, QBrush, QColor, QPalette, QFontMetrics, QPixmap, QTransform, QPolygonF
from PySide6.QtWidgets import QGraphicsItemGroup, QGraphicsItem, QGraphicsPixmapItem, QGraphicsRectItem, \
    QGraphicsLineItem, QGraphicsSimpleTextItem, QGraphicsScene, QGraphicsPolygonItem

from mtg_proxy_printer.model.card_list import PageColumns
from mtg_proxy_printer.model.carddb import Card, CardCorner
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.document_loader import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import PageType, unit_registry, RESOLUTION, CardSizes, CardSize, QuantityT

Changes to mtg_proxy_printer/ui/printing_filter_widgets.py.

14
15
16
17
18
19
20
21
22
23



24
25
26
27
28
29
30
14
15
16
17
18
19
20



21
22
23
24
25
26
27
28
29
30







-
-
-
+
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import abc
from functools import partial
from typing import List, Tuple

from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QGroupBox, QWidget, QCheckBox, QPushButton
from PySide6.QtCore import QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QGroupBox, QWidget, QCheckBox, QPushButton

from mtg_proxy_printer.units_and_sizes import ConfigParser, SectionProxy
from mtg_proxy_printer.ui.common import highlight_widget

try:
    from mtg_proxy_printer.ui.generated.settings_window.format_printing_filter import Ui_FormatPrintingFilter
    from mtg_proxy_printer.ui.generated.settings_window.general_printing_filter import Ui_GeneralPrintingFilter

Changes to mtg_proxy_printer/ui/progress_bar.py.

9
10
11
12
13
14
15
16
17


18
19
20
21
22
23
24
9
10
11
12
13
14
15


16
17
18
19
20
21
22
23
24







-
-
+
+







# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from PyQt5.QtCore import pyqtSlot as Slot, Qt
from PyQt5.QtWidgets import QWidget, QLabel, QProgressBar
from PySide6.QtCore import Slot, Qt
from PySide6.QtWidgets import QWidget, QLabel, QProgressBar

try:
    from mtg_proxy_printer.ui.generated.progress_bar import Ui_ProgressBar
except ModuleNotFoundError:
    from mtg_proxy_printer.ui.common import load_ui_from_file
    Ui_ProgressBar = load_ui_from_file("progress_bar")

Changes to mtg_proxy_printer/ui/settings_window.py.

13
14
15
16
17
18
19
20
21
22



23
24
25
26
27
28
29
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28
29







-
-
-
+
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import pathlib
import typing
from functools import partial

from PyQt5.QtCore import QStringListModel, pyqtSignal as Signal, Qt, QItemSelectionModel, QEvent, QObject, QTimer
from PyQt5.QtWidgets import QDialogButtonBox, QMessageBox, QWidget, QDialog
from PyQt5.QtGui import QIcon, QStandardItemModel, QResizeEvent
from PySide6.QtCore import QStringListModel, Signal, Qt, QItemSelectionModel, QEvent, QObject, QTimer
from PySide6.QtWidgets import QDialogButtonBox, QMessageBox, QWidget, QDialog
from PySide6.QtGui import QIcon, QStandardItemModel, QResizeEvent

import mtg_proxy_printer.app_dirs
from mtg_proxy_printer.units_and_sizes import ConfigParser
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.document_controller import DocumentAction
from mtg_proxy_printer.document_controller.edit_document_settings import ActionEditDocumentSettings

Changes to mtg_proxy_printer/ui/settings_window_pages.py.

15
16
17
18
19
20
21
22
23
24



25
26
27
28
29
30
31
15
16
17
18
19
20
21



22
23
24
25
26
27
28
29
30
31







-
-
-
+
+
+








import logging
from functools import partial
import pathlib
import typing
from abc import abstractmethod

from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot, QUrl, QStandardPaths, QStringListModel, Qt, QThreadPool
from PyQt5.QtGui import QDesktopServices, QStandardItem, QIcon
from PyQt5.QtWidgets import QWidget, QCheckBox, QFileDialog, QMessageBox, QApplication, QLineEdit
from PySide6.QtCore import Signal, Slot, QUrl, QStandardPaths, QStringListModel, Qt, QThreadPool
from PySide6.QtGui import QDesktopServices, QStandardItem, QIcon
from PySide6.QtWidgets import QWidget, QCheckBox, QFileDialog, QMessageBox, QApplication, QLineEdit

import mtg_proxy_printer.app_dirs
import mtg_proxy_printer.settings
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
from mtg_proxy_printer.logger import get_logger
from mtg_proxy_printer.ui.common import highlight_widget
from mtg_proxy_printer.units_and_sizes import OptStr, ConfigParser
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
65
66
67
68
69
70
71

72
73
74
75
76
77
78







-









class PageMetadata(typing.NamedTuple):
    text: str
    icon_name: OptStr
    tooltip: OptStr = None


class Page(QWidget):
    """The base class for settings page widgets. Defines the API used by the settings window"""

    def display_item(self) -> typing.Sequence[QStandardItem]:
        data = self.display_metadata()
        item = QStandardItem(data.text)
        if data.icon_name:
102
103
104
105
106
107
108
109

110
111
112
113
114
115
116
101
102
103
104
105
106
107

108
109
110
111
112
113
114
115







-
+







    @abstractmethod
    def highlight_differing_settings(self, settings: ConfigParser):
        """Highlights GUI widgets with a state different from the given settings"""
        pass

    def clear_highlight(self):
        """Clears all GUI widget highlights."""
        for item in self.findChildren((QWidget,), options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
        for item in self.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively):  # type: QWidget
            item.setGraphicsEffect(None)


class DebugSettingsPage(Page):

    requested_card_download = Signal(pathlib.Path)

Changes to mtg_proxy_printer/units_and_sizes.py.

24
25
26
27
28
29
30
31

32
33
34
35
36
37
38
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38







-
+







    from typing_extensions import NotRequired

import pint
try:
    from pint.facets.plain.registry import QuantityT, UnitT
except ImportError:  # Compatibility with Pint 0.21 for Python 3.8 support
    QuantityT = UnitT = typing.Any
from PyQt5.QtCore import QSize
from PySide6.QtCore import QSize


def _setup_units() -> typing.Tuple[pint.UnitRegistry, QuantityT]:
    registry = pint.UnitRegistry()
    resolution = registry.parse_expression("300dots/inch")
    print_context = pint.Context("print")
    print_context.add_transformation("[length]", "[printing_unit]", lambda _, x: x*RESOLUTION)

Changes to mtg_proxy_printer/update_checker.py.

17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31







-
+







import re
import socket
import typing
import urllib.parse
import urllib.error

import ijson
from PyQt5.QtCore import QObject, pyqtSignal as Signal, QThreadPool
from PySide6.QtCore import QObject, Signal, QThreadPool

from mtg_proxy_printer.argument_parser import Namespace
import mtg_proxy_printer.meta_data
from mtg_proxy_printer import settings
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.card_info_downloader import CardInfoDatabaseImportWorker, CardInfoWorkerBase
from mtg_proxy_printer.natsort import natural_sorted, str_less_than

Changes to pyproject.toml.

29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43







-
+







]
# Minimum required ijson version is 3.1 for the use_float parameter.
# ijson adds full compatibility with Python 3.11 in version 3.2 (3.1 is really slow on Py3.11).
# and Python 3.12 support in 3.2.1. Require newer versions on those Python versions to avoid
# falling back to the slow pure Python backend.
dependencies = [
    "platformdirs >= 2.6.0",
    "PyQt5",
    "PySide6_Essentials >= 6.7.0",
    "ijson >= 3.1.0; python_version < '3.11'",
    "ijson >= 3.2.0; python_version >= '3.11'",
    "ijson >= 3.2.1; python_version >= '3.12'",
    "pint < 0.22; python_version < '3.9'",  # 0.22 dropped Py 3.8 support, thus Win7 support
    "pint >= 0.22; python_version >= '3.9'",  # Requires 0.22 for the QuantityT and UnitT types
    "delegateto == 1.5",
    "PyHamcrest >= 2",
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77
53
54
55
56
57
58
59


60
61
62
63



64


65
66
67
68
69
70
71







-
-




-
-
-
+
-
-







[project.optional-dependencies]
dev = [
    "pytest",
    "pytest-cov",
    "pytest-timeout",
    "pytest-qt >= 2.0",
    "tox >= 4.0",
    "PySide2; platform_system == 'Windows' and python_version < '3.12'",  # Used for lrelease: Compiling translations
    "PySide6_Essentials; platform_system == 'Windows' and python_version >= '3.12'",  # Used for lrelease: Compiling translations
    "pillow; platform_system == 'Windows'",  # Used to convert the app icon to .ico
]
package = [
    "build",
        # cx_Freeze 7.2 seems to somehow break loading the compiled Qt resources, but only for PyQt5.
    # Limit to 7.1 for now, until a fix is found
    "cx_Freeze >= 6.11.1, < 7.2; platform.python_implementation == 'CPython'",
    "cx_Freeze >= 6.11.1; platform.python_implementation == 'CPython'",
    "PySide2; platform_system == 'Windows' and python_version < '3.12'",  # Used for lrelease: Compiling translations
    "PySide6_Essentials; platform_system == 'Windows' and python_version >= '3.12'",  # Used for lrelease: Compiling translations
    "pillow; platform_system == 'Windows'",  # Used to convert the app icon to .ico
]


[project.urls]
Homepage = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter/index"
"Bug tracker" = "https://chiselapp.com/user/luziferius/repository/MTGProxyPrinter/ticket"
102
103
104
105
106
107
108
109

110
111
112
113
96
97
98
99
100
101
102

103
104
105
106
107







-
+




version = {attr= "mtg_proxy_printer.meta_data.__version__"}


[build-system]
requires = [
    "setuptools >=61",
    "wheel",
    "PyQt5",
    "PySide6_Essentials >= 6.7.0",
    "build",
    "tox >= 4.0",
]
build-backend = "setuptools.build_meta"

Changes to run_tests.bat.

1
2
3
4
5


6
7
8
9
10
1
2
3


4
5
6
7
8
9
10



-
-
+
+





:: Runs the unit tests

:: Create or activate the build environment
IF EXIST "venv" (
  call venv\Scripts\activate.bat
IF EXIST "venv-PySide6" (
  call venv-PySide6\Scripts\activate.bat
) ELSE (
  call create_development_environment.bat
)

tox run

Changes to run_tests.sh.

1
2
3

4
5
1
2

3
4
5


-
+


#!/bin/bash

source venv/bin/activate
source venv-PySide6/bin/activate
tox run
deactivate

Changes to scripts/clean_windows_build.bat.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25
26

27
28
29

30
31
32


33
34



35
36
37
38




39
40

41
42

43
44
45
46


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

63

64
65

66
67

68
69
70
71
72
73


74
75

76
77
78
79
80
81
82
83
84
85
86
87
88

89
90
91
92



93
94
95
96
97
98
99
100
101
102
103
104
105
9
10
11
12
13
14
15


16
17
18
19
20
21

22
23

24

25

26



27
28

29
30
31
32




33
34
35
36
37

38


39




40
41











42



43
44

45


46
47

48




49
50
51
52
53

54










55
56

57
58
59


60
61
62




63


64
65
66
67
68
69







-
-






-
+

-
+
-

-
+
-
-
-
+
+
-

+
+
+
-
-
-
-
+
+
+
+

-
+
-
-
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-

-
-
-

+
-
+
-
-
+

-
+
-
-
-
-


+
+

-
+
-
-
-
-
-
-
-
-
-
-


-
+


-
-
+
+
+
-
-
-
-

-
-






:: but WITHOUT ANY WARRANTY; without even the implied warranty of
:: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
:: GNU General Public License for more details.
::
:: You should have received a copy of the GNU General Public License
:: along with this program. If not, see <http://www.gnu.org/licenses/>.


:: Cleanup various items that bloat the application bundle. Mostly related to unused PyQt5 and Qt5 components
if "%1%"=="" (
  pushd build\exe*
) else (
  pushd "%1"
)

rmdir /S /Q PyQt5.uic.widget-plugins
pushd lib

pushd lib
pushd PySide6
del ijson\backends\python*.dll


:: Don't need the executables, like Qt6 Designer, etc.
pushd PyQt5
:: All DLLs here are also in Qt5\bin\
del *.dll
:: Delete all typing stubs
del *.exe *.pyi
del QtRemoteObjects.pyd QtSerialPort.pyd QtSensors.pyd QtNetwork.pyd QtXml.pyd QtXmlPatterns.pyd pyrcc.pyd

:: Remove unused components. Each consists of a pair of QtComponent.pyd and Qt6Component.dll
del Q*tSerialPort* Qt*DBus* Qt*Designer* Qt*JsonRpc* Qt*Labs* Qt*LanguageServer* Qt*Network*
del Qt*Qml* Qt*Quick* Qt*RemoteObjects* Qt*Sensors* Qt*Sql* Qt*Test* Qt*WebChannel*
:: The sip bindings aren't used at runtime
rmdir /S /Q bindings

pushd Qt5
del Qt*Bluetooth* Qt*Charts* Qt*Concurrent* Qt*DataVisualization* Qt*Graphs* Qt*HttpServer*
del Qt*Nfc* Qt*Positioning* Qt*Scxml* Qt*SerialBus* Qt*SerialPort* Qt*ShaderTools*
del Qt*StateMachine* Qt*Test* Qt*TextToSpeech* Qt*Web* Qt*3D*
del Qt*Help* Qt*Multimedia* Qt*Xml*

:: Unused Qsci
:: Unused audio/video codec DLLs
rmdir /S /Q qsci

del avformat-*.dll avutil-*.dll swresample-*.dll swscale-*.dll
:: Unused Qml bindings
rmdir /S /Q qml


:: Unused OpenGL bindings. The Qt6OpenGL DLLs are required and thus not removed
del QtOpenGL.pyd QtOpenGLWidgets.pyd opengl32sw.dll
::  Unused DLLs
pushd bin
del d3dcompiler_47.dll libEGL.dll libGLESv2.dll opengl32sw.dll Qt5Bluetooth.dll Qt5DBus.dll Qt5Designer.dll Qt5Help.dll
del Qt5Location.dll Qt5Multimedia.dll Qt5MultimediaWidgets.dll Qt5Network.dll Qt5Nfc.dll Qt5OpenGL.dll Qt5Positioning.dll
del Qt5PositioningQuick.dll Qt5Qml.dll Qt5QmlModels.dll Qt5QmlWorkerScript.dll Qt5Quick.dll Qt5Quick3D.dll
del Qt5Quick3DAssetImport.dll Qt5Quick3DRender.dll Qt5Quick3DRuntimeRender.dll Qt5Quick3DUtils.dll Qt5QuickControls2.dll
del Qt5QuickParticles.dll Qt5QuickShapes.dll Qt5QuickTemplates2.dll Qt5QuickTest.dll Qt5QuickWidgets.dll Qt5RemoteObjects.dll
del Qt5Sensors.dll Qt5SerialPort.dll Qt5Sql.dll Qt5Test.dll Qt5WebChannel.dll
del Qt5WebSockets.dll Qt5WebView.dll Qt5XmlPatterns.dll
del libcrypto-1_1-x64.dll libssl-1_1-x64.dll
popd


:: Unused plugins
pushd plugins

pushd translations
:: Remove duplicated Qt5 base DLLs
:: Remove translations for unused/removed components
FOR %%G IN ( printsupport platforms imageformats styles
) DO del %%G\Qt5*.dll
del assistant* designer* linguist* qtdeclarative*


:: leave translations
FOR %%G IN (
  assetimporters audio geometryloaders geoservices mediaservice playlistformats
  position renderers sceneparsers sensorgestures sensors sqldrivers texttospeech webview
) DO rmdir /S /Q %%G
popd

pushd plugins
del /Q /S tls

:: Unused translations (of unused modules)
:: leave plugins
pushd translations
del qtxmlpatterns_*.qm
del qtconnectivity_*.qm
del qtdeclarative_*.qm
del qtlocation_*.qm
del qtmultimedia_*.qm
del qtquickcontrols_*.qm
del qtquickcontrols2_*.qm
del qtserialport_*.qm
del qtwebsockets_*.qm
popd

:: leave Qt5
:: leave PySide6
popd

:: Unused extension modules
del *.pyi

del shiboken6\shiboken6*.lib
del email\architecture.rst
del QAxContainer.pyd QtBluetooth.pyd QtDBus.pyd QtDesigner.pyd QtHelp.pyd QtLocation.pyd QtMultimedia.pyd QtMultimediaWidgets.pyd
del QtOpenGL.pyd QtNfc.pyd QtPositioning.pyd QtQml.pyd QtQuick.pyd QtQuick3D.pyd QtQuickWidgets.pyd
del QtRemoteObjects.pydQtSensors.pydQtSerialPort.pyd QtSql.pyd QtTest.pyd QtTextToSpeech.pyd QtWebChannel.pyd
del QtWebSockets.pyd _QOpenGLFunctions_2_0.pyd _QOpenGLFunctions_2_1.pyd _QOpenGLFunctions_4_1_Core.pyd

:: leave PyQt5
popd
:: leave lib
popd
::leave build directory
popd


Changes to scripts/compile_resources.py.

69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91







-
+








-







def split_iterable(iterable: typing.Iterable[T], chunk_size: int, /) -> typing.List[typing.Tuple[T, ...]]:
    """Split the given iterable into chunks of size chunk_size. Does not add padding values to the last item."""
    iterable = iter(iterable)
    return list(iter(lambda: tuple(itertools.islice(iterable, chunk_size)), ()))


def compile():
    command = ("pyrcc5", "-compress", "9", str(SOURCES_PATH))  # noqa  # "pyrcc5" is a program name, not a typo
    command = ("pyside6-rcc", "--compress", "9", "--generator", "python", str(SOURCES_PATH))
    compiled = subprocess.check_output(command, universal_newlines=True)  # type: str
    # The resource compiler outputs > 15000 lines with extremely low line length.
    # Reduce the file size by removing a good percentage of those line breaks
    blocks = compiled.split("\\\n")
    chunks = split_iterable(blocks, 7)
    joined_chunks = ("".join(items) for items in chunks)
    compiled = "\\\n".join(joined_chunks)
    TARGET_PATH.write_text(compiled, "utf-8")


def clean():
    TARGET_PATH.unlink(missing_ok=True)


def main():
    args = parse_args()

Changes to scripts/compile_ui_files.py.

21
22
23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42
43
21
22
23
24
25
26
27

28
29
30
31
32
33
34


35
36
37
38
39
40
41







-




+


-
-







  like Python wheels and application bundles created via cx_Freeze.
- Creation of type hinting stubs with suffix ".pyi". These are used during development,
  to provide type hinting and autocompletion for the Ui classes defined by the UI files.
"""

import argparse
import ast
import io
import itertools
import textwrap
from pathlib import Path
import shutil
import subprocess
from typing import Tuple, NamedTuple, TypeVar, Iterable, Union, Type, List, Any, Dict, Set

import PyQt5.uic

SOURCE_ROOT = Path(__file__).parent.parent  # Checkout root directory
MAIN_PACKAGE = SOURCE_ROOT / "mtg_proxy_printer"
UI_SOURCE_PATH = MAIN_PACKAGE / "resources/ui"  # UI files live here
TARGET_PATH = MAIN_PACKAGE / "ui/generated"  # Package containing generated modules/type hinting stubs
T = TypeVar("T")
ClassRegistry = Dict[str, ast.ImportFrom]
UsedClasses = Set[str]
72
73
74
75
76
77
78






79
80
81
82
83
84
85
86
87
88
89
90


91
92

93
94

95
96
97
98
99
100


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122

123
124
125
126
127

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146

147
148
149

150
151
152
153
154
155
156
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92


93
94


95


96






97
98










99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

116
117

118
119
120
121
122
123
124
125
126
127
128
129
130
131

132

133
134
135

136
137
138
139
140
141
142
143







+
+
+
+
+
+










-
-
+
+
-
-
+
-
-
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-












+




-
+

-














-

-
+


-
+







    args = parser.parse_args()
    return args


def type_filter(any_: Iterable[Any], types: Union[Type[T], Tuple[Type[T], ...]]) -> Iterable[T]:
    return filter(lambda x: isinstance(x, types), any_)


def create_python_package(location: Path, /):
    """Creates an empty Python package at the given Path"""
    location.mkdir(parents=True, exist_ok=True)
    (location/"__init__.py").touch(exist_ok=True)


def compile_ui_files(args: Namespace, target_path: Path = TARGET_PATH, source_path: Path = UI_SOURCE_PATH):
    """
    Compiles all UI files found in source_path to Python types, storing results in target_path.

    Recursively finds UI files under source_path, replicates the found directory tree as a Python package hierarchy and
    populates it with the compiled Ui types.
    """
    if args.purge_existing and target_path.is_dir():
        shutil.rmtree(target_path)

    source_path = source_path.resolve()
    create_python_package(target_path)
    for ui_file in source_path.rglob("*.ui"):
    target_path.mkdir(exist_ok=True)

        compiled = compile_ui_file(ui_file)
    def map_to_output(directory, file_name):
        dir_path = Path(directory).relative_to(source_path)
        parent_dir = (target_path/ui_file.relative_to(source_path)).parent
        return target_path/dir_path, file_name
    import functools
    PyQt5.uic.open = functools.partial(open, encoding="utf-8")
    PyQt5.uic.compileUiDir(str(source_path), recurse=True, map=map_to_output)
    create_python_package(target_path)

        create_python_package(parent_dir)
        (parent_dir/f"{ui_file.stem}.py").write_text(compiled, "utf-8")

def create_python_package(target_dir: Path):
    """
    Creates an empty __init__.py file in target_dir and each subdirectory, recursively.
    This marks these directories as proper Python packages.
    """
    (target_dir/"__init__.py").touch(exist_ok=True)
    for entry in target_dir.rglob("*"):
        if entry.is_dir():
            (entry/"__init__.py").touch(exist_ok=True)


def create_ui_type_stubs(args: Namespace, target_path: Path = TARGET_PATH, source_path: Path = UI_SOURCE_PATH):
    """
    Creates type hinting stubs for all UI files found in source_path, storing results in target_path.

    Recursively finds UI files under source_path, replicates the found directory tree as a Python package hierarchy and
    populates it with the created type hints.
    """
    if args.purge_existing and target_path.is_dir():
        shutil.rmtree(target_path)
    class_registry = build_class_registry(MAIN_PACKAGE)
    create_python_package(target_path)
    for ui_file in source_path.rglob("*.ui"):
        compiled = compile_ui_file(ui_file)
        stub = generate_stub(compiled, ui_file, class_registry)
        parent_dir = (target_path/ui_file.relative_to(source_path)).parent
        parent_dir.mkdir(exist_ok=True)
        create_python_package(parent_dir)
        (parent_dir/f"{ui_file.stem}.pyi").write_text(stub, "utf-8")
    create_python_package(target_path)


def build_class_registry(package_path: Path) -> ClassRegistry:
    """Scan the source tree for classes and build a dict from class name to import path"""
    result: ClassRegistry = {}
    for py_file in package_path.rglob("*.py"):
        module_path = ".".join((py_file.parent.relative_to(package_path.parent) / py_file.stem).parts)
        root_node = ast.parse(py_file.read_text("utf-8"), py_file)
        for class_def in type_filter(root_node.body, ast.ClassDef):
            result[class_def.name] = ast.ImportFrom(module_path, [ast.alias(class_def.name)])
    return result


def compile_ui_file(path: Path) -> str:
    buffer = io.StringIO()
    try:
        PyQt5.uic.compileUi(path, buffer, from_imports=True)
        command = ("pyside6-uic", "--generator", "python", str(path))
    except Exception as e:
        raise RuntimeError(f"Compilation failed for file {path}") from e
    return buffer.getvalue()
    return subprocess.check_output(command, encoding="utf-8")


def generate_stub(compiled_ui: str, ui_file: Path, class_registry: ClassRegistry) -> str:
    root_node = ast.parse(compiled_ui)
    header = f"# Automatically generated type hinting stub for '{ui_file.name}'. Do not modify."
    # Keep all imports unmodified
    imports = "import typing\n\n"
211
212
213
214
215
216
217
218

219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
198
199
200
201
202
203
204

205
206
207
208
209
210
211









212
213
214
215
216
217
218







-
+






-
-
-
-
-
-
-
-
-







    return f"class {class_root.name}({base_classes}):"


def get_assignments(function_body: List[ast.stmt]) -> List[Assignment]:
    return [
        Assignment(
            assignment.targets[0].attr,
            get_assignment_type(assignment)
            assignment.value.func.id
        )
        for assignment
        in type_filter(function_body, ast.Assign)
        if hasattr(assignment.targets[0], "attr")  # Filter out local variables
    ]


def get_assignment_type(assignment: ast.Assign):
    func = assignment.value.func
    if isinstance(func, ast.Attribute):
        return f"{func.value.id}.{func.attr}"  # Qualified name: module.ClassName()
    elif isinstance(func, ast.Name):
        return func.id
    raise NotImplementedError("Unknown assignment type")


def get_function_stub(function_body: ast.FunctionDef, found_class_uses: UsedClasses):
    for index, arg in enumerate(function_body.args.args):
        if arg.arg == "self":
            continue
        found_class_uses.add(arg.arg)
        arg.annotation = ast.Constant(arg.arg)

Changes to scripts/update_translations.py.

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30
31
32

33
34
35
36
37
38
39







-










-







# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
Management script for application translations
"""

import argparse
import itertools
import pathlib
import re
import subprocess
from typing import Callable, NamedTuple


# Mapping between source locales, as provided by Crowdin, and the target, as expected/loaded by Qt.
# TODO: Investigate, how systems behave in locales requiring the country as disambiguation, like en or zh.
LOCALES = {
    "de-DE": "de",
#    "en-GB": "en_GB",
    "en-US": "en_US",
    "es-ES": "es",
    "fr-FR": "fr",
    "it-IT": "it",
    "ja-JP": "ja",
    "ko-KR": "ko",
    "pt-PT": "pt",
49
50
51
52
53
54
55

56
57
58
59
60
61
62
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61







+







    # Fetch the name of the sources .ts file from crowdin.yml.
    # (Since Python does not come with a YAML parser, use a simple RE for data extraction)
    re.search(
        r'"source":\s*"(?P<path>.+)",',
        crowdin_yml_path.read_text("utf-8")
    )["path"]
)


class Namespace(NamedTuple):
    """Mock namespace for type hinting"""
    command: Callable[["Namespace"], None]


def parse_args() -> Namespace:
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
84
85
86
87
88
89
90










91
92
93
94
95
96
97
98

99
100
101
102
103
104
105







-
-
-
-
-
-
-
-
-
-








-







        subprocess.check_output("crowdin")
    except FileNotFoundError as e:
        raise RuntimeError("The required Crowdin CLI client is not installed in the PATH, exiting.") from e


def register_new_raw_strings():
    TRANSLATIONS_DIR.mkdir(parents=True, exist_ok=True)
    # PyQt5
    package = pathlib.Path("mtg_proxy_printer")
    files = list(itertools.chain(package.rglob("*.py"), package.rglob("*.ui")))
    subprocess.call([
        "pylupdate5",
        "-noobsolete", "-verbose",
        *files,
        "-ts", SOURCES_PATH
    ])
    ''' PySide6
    subprocess.call([
        "pyside6-lupdate",
        "-source-language", "en_US",
        "-recursive", "-no-obsolete",
        "-extensions", "py,ui",
        "mtg_proxy_printer",
        "-ts", SOURCES_PATH
    ])
    '''


def upload_raw_strings(args: Namespace):
    """Updates the sources .ts from code, then uploads it to the Crowdin API"""
    register_new_raw_strings()
    verify_crowdin_cli_present()
    subprocess.call([
135
136
137
138
139
140
141
142
143
144

145
146

147
148
149
150
151
152
153
123
124
125
126
127
128
129

130

131
132

133
134
135
136
137
138
139
140







-

-
+

-
+







    try:
        subprocess.check_output(["lrelease", "-version"])
    except FileNotFoundError:
        print("lrelease not found on PATH. Falling back to the executable supplied by PySide2.")
        import sys
        exe = pathlib.Path(sys.executable)
        venv = exe.parent.parent
        lrelease5 = venv / "Lib" / "site-packages" / "PySide2" / "lrelease.exe"
        lrelease6 = venv / "Lib" / "site-packages" / "PySide6" / "lrelease.exe"
        if not lrelease5.is_file() and not lrelease6.is_file():
        if not lrelease6.is_file():
            raise RuntimeError("No fallback lrelease executable found")
        return lrelease5 if lrelease5.is_file() else lrelease6
        return lrelease6
    else:
        return "lrelease"


def compile_translations(args: Namespace):
    """Compiles .ts files into importable, binary translation files with .qm suffix."""
    lrelease = get_lrelease()

Changes to setup_cx_freeze.py.

39
40
41
42
43
44
45


46
47
48

49


50
51
52
53
54

55
56
57


58
59
60
61



62
63

64
65
66
67



68
69

70
71
72

73
74

75
76
77
78
79



80
81
82
83
84
85
86
87
88
89









90
91

92
93
94
95
96
97
98
99
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55



56



57
58




59
60
61


62




63
64
65


66



67


68





69
70
71










72
73
74
75
76
77
78
79
80


81

82
83
84
85
86
87
88







+
+


-
+

+
+


-
-
-
+
-
-
-
+
+
-
-
-
-
+
+
+
-
-
+
-
-
-
-
+
+
+
-
-
+
-
-
-
+
-
-
+
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
-









base = "Win32GUI" if sys.platform == "win32" else None

excludes  = [
    f"{main_package}.resources",  # Do not include the raw resources as individual files
    "distutils",
    "ijson.benchmark",  # Ignore the benchmark script added after ijson 3.2.3
    "importlib_metadata",
    "lib2to3",
    "pep517",
    "pytest",
    "pint.testsuite",  # Ignore the internal test suite
    "pydoc_data",
    "pytest",
    "sqlite3.test",  # Ignore the internal test suite
    "tkinter",
    "toml",
    "sqlite3.test",  # Ignore the internal test suite
    "pint.testsuite",  # Ignore the internal test suite
    "ijson.benchmark",  # Ignore the benchmark script added after ijson 3.2.3
    # Empty package with readme and download scripts
    "importlib_metadata",

    # All unused PyQt components
    "ctypes.test",
    # Unused PySide6 components
    "PyQt5.QtXmlPatterns",
    "PyQt5.QtNfc",
    "PyQt5.QtQml",
    "PyQt5.QtSql",
    "PySide6.glue",
    "PySide6.include",
    "PySide6.metatypes",
    "PyQt5.Qt3DAnimation",
    "PyQt5.Qt3DCore",
    "PySide6.plugins.assetimporters",
    "PyQt5.Qt3DExtras",
    "PyQt5.Qt3DInput",
    "PyQt5.Qt3DLogic",
    "PyQt5.Qt3DRender",
    "PySide6.plugins.canbus",
    "PySide6.plugins.designer",
    "PySide6.plugins.geometryloaders",
    "PyQt5.QtBluetooth",
    "PyQt5.QtChart",
    "PySide6.plugins.geoservices",
    "PyQt5.QtDataVisualisation",
    "PyQt5.QtLocation",
    "PyQt5.QtMultimedia",
    "PySide6.plugins.multimedia",
    "PyQt5.QtMultimediaWidgets",
    "PyQt5.QtNetwork",
    "PySide6.plugins.networkinformation",
    "PyQt5.QtNetworkAuth",
    "PyQt5.QtOpenGL",
    "PyQt5.QtPositioning",
    "PyQt5.QtPurchasing",
    "PyQt5.QtQuick",
    "PySide6.plugins.position",
    "PySide6.plugins.qmltooling",
    "PySide6.plugins.scxmldatamodel",
    "PyQt5.QtQuickWidgets",
    "PyQt5.QtRemoteObjects",
    "PyQt5.QtSensors",
    "PyQt5.QtSerialPort",
    "PyQt5.QtTest",
    "PyQt5.QtWebChannel",
    "PyQt5.QtWebEngine",
    "PyQt5.QtWebEngineCore",
    "PyQt5.QtWebEngineWidgets",
    "PyQt5.QtWebKit",
    "PySide6.plugins.sensors",
    "PySide6.plugins.sqldrivers",  # Use Python native sqlite3 module instead
    "PySide6.plugins.tls",
    "PySide6.qml",
    "PySide6.QtAsyncio",
    "PySide6.resources",
    "PySide6.scripts",
    "PySide6.support",
    "PySide6.translations.qtwebengine_locales",
    "PyQt5.QtWebKitWidgets",
    "PyQt5.QtWebSockets",
    "PySide6.typesystems",
    "PyQt5.uic.port_v2",
]

if sys.platform == "win32":
    excludes += [
        "platformdirs.android",
        "platformdirs.macos",
        "platformdirs.unix",

Changes to tests/conftest.py.

18
19
20
21
22
23
24
25

26
27
28
29
30
31
32
18
19
20
21
22
23
24

25
26
27
28
29
30
31
32







-
+







fixtures defined here are available in all test modules.
"""
import itertools
import sqlite3
import unittest.mock
from pathlib import Path

from PyQt5.QtGui import QColorConstants, QPixmap
from PySide6.QtGui import QColorConstants, QPixmap
import pytest

import mtg_proxy_printer.sqlite_helpers
import mtg_proxy_printer.settings
from mtg_proxy_printer.printing_filter_updater import PrintingFilterUpdater
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.document import Document
53
54
55
56
57
58
59
60

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
53
54
55
56
57
58
59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

76
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91
92
93
94
95
96
97








-
+















-









-













-
            ":memory:", "document-v6", CardDatabase.MIN_SUPPORTED_SQLITE_VERSION, check_same_thread=False)
    if request.param:
        db.execute("PRAGMA reverse_unordered_selects = TRUE")
    return db


@pytest.fixture
def image_db(tmp_path: Path):
def image_db(qtbot, tmp_path: Path):
    image_db = ImageDatabase(tmp_path)
    regular_width, regular_height = image_db.blank_image.width(), image_db.blank_image.height()
    for scryfall_id, is_front in itertools.product(
            ["0000579f-7b35-4ed3-b44c-db2a538066fe", "b3b87bfc-f97f-4734-94f6-e3e2f335fc4d"], [True, False]):
        # Regular card images
        key = ImageKey(scryfall_id, is_front, True)
        image_db.loaded_images[key] = image_db.blank_image.copy(0, 0, regular_width, regular_height)
        image_db.images_on_disk.add(key)
    for scryfall_id in ["650722b4-d72b-4745-a1a5-00a34836282b"]:
        # Oversized card images
        key = ImageKey(scryfall_id, True, True)
        image_db.loaded_images[key] = image_db.blank_image.scaled(regular_height, regular_width*2)
        image_db.images_on_disk.add(key)

    yield image_db
    image_db.__dict__.clear()


@pytest.fixture
def document(qtbot, card_db: CardDatabase, image_db: ImageDatabase) -> Document:
    fill_card_database_with_json_cards(qtbot, card_db, [
        "regular_english_card", "oversized_card", "english_double_faced_card"])
    document = Document(card_db, image_db)
    document.loader.db = card_db.db
    yield document
    document.__dict__.clear()


@pytest.fixture
def document_light(qtbot) -> Document:
    mock_card_db = unittest.mock.NonCallableMagicMock()
    mock_image_db = unittest.mock.NonCallableMagicMock(spec=ImageDatabase)
    mock_image_db.blank_image = QPixmap(CardSizes.REGULAR.as_qsize_px())
    mock_image_db.blank_image.fill(QColorConstants.Transparent)
    mock_card_db.db = mtg_proxy_printer.sqlite_helpers.create_in_memory_database(
        "carddb", CardDatabase.MIN_SUPPORTED_SQLITE_VERSION, check_same_thread=False)
    document = Document(mock_card_db, mock_image_db)
    document.loader.db = mock_card_db.db
    yield document
    document.__dict__.clear()

Changes to tests/document_controller/test_action_move_cards.py.

14
15
16
17
18
19
20
21

22
23
24
25
26
27
28
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28







-
+







# along with this program. If not, see <http://www.gnu.org/licenses/>.


from functools import partial

import pytest
from hamcrest import *
from PyQt5.QtCore import QModelIndex
from PySide6.QtCore import QModelIndex

from mtg_proxy_printer.model.document_page import PageType
from mtg_proxy_printer.document_controller import IllegalStateError
from mtg_proxy_printer.document_controller.page_actions import ActionNewPage
from mtg_proxy_printer.document_controller.move_cards import ActionMoveCards

from .helpers import card_container_with, append_new_card_in_page, card_container_with_name

Changes to tests/document_controller/test_action_remove_page.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from functools import partial

from hamcrest import *
from PyQt5.QtCore import QModelIndex
from PySide6.QtCore import QModelIndex

from mtg_proxy_printer.model.document_page import Page
from mtg_proxy_printer.document_controller import IllegalStateError
from mtg_proxy_printer.document_controller.page_actions import ActionRemovePage

from .helpers import append_new_pages, append_new_card_in_page, card_container_with, verify_page_index_cache_is_valid

Changes to tests/test___main__.py.

41
42
43
44
45
46
47
48

49
50
51
52
53
54
55
41
42
43
44
45
46
47

48
49
50
51
52
53
54
55







-
+








@pytest.fixture
def main_mocks():
    with patch("mtg_proxy_printer.__main__.mtg_proxy_printer.logger.configure_root_logger") as configure_root_logger, \
            patch.multiple(
            "mtg_proxy_printer.__main__",
            _app=DEFAULT, Application=DEFAULT, handle_ssl_certificates=DEFAULT,
            parse_args=DEFAULT, QTimer=DEFAULT, logger=DEFAULT, QApplication=DEFAULT) as mocks:
            parse_args=DEFAULT, QTimer=DEFAULT, logger=DEFAULT) as mocks:
        mocks["configure_root_logger"] = configure_root_logger
        yield mocks
    mocks.clear()


def test_main_calls_handle_ssl_certificates(main_mocks):
    mtg_proxy_printer.__main__.main()
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91
77
78
79
80
81
82
83

84
85
86
87
88
89
90
91







-
+







def test_main_configures_logger(main_mocks):
    mtg_proxy_printer.__main__.main()
    main_mocks["configure_root_logger"].assert_called_once()


def test_main_calls_exec_on_application_instance(main_mocks):
    mtg_proxy_printer.__main__.main()
    main_mocks["Application"]().exec_.assert_called_once()
    main_mocks["Application"]().exec.assert_called_once()


def test_enqueues_startup_tasks_on_regular_launch(main_mocks):
    main_mocks["parse_args"].return_value = Namespace(test_exit_on_launch=False)
    mtg_proxy_printer.__main__.main()
    app = main_mocks["Application"]()
    app.enqueue_startup_tasks.assert_called_once()

Changes to tests/test_card_list.py.

15
16
17
18
19
20
21
22

23
24
25
26
27
28
29
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29







-
+








from collections import Counter
import typing

from hamcrest import *
import pytest
from pytestqt.qtbot import QtBot
from PyQt5.QtCore import QItemSelectionModel
from PySide6.QtCore import QItemSelectionModel

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.model.card_list import CardListModel

from tests.helpers import fill_card_database_with_json_cards

OVERSIZED_ID = "650722b4-d72b-4745-a1a5-00a34836282b"

Changes to tests/test_carddb.py.

196
197
198
199
200
201
202
203

204
205
206
207
208
209
210
211
196
197
198
199
200
201
202

203

204
205
206
207
208
209
210







-
+
-







            "regular_english_card",
            "english_double_faced_card",
            "english_double_faced_art_series_card",
            "Flowerfoot_Swordmaster_card",
            "Flowerfoot_Swordmaster_token",
        ],
    )
    yield card_db
    return card_db
    card_db.__dict__.clear()


def generate_test_cases_for_test_translate_card_name():
    """Yields tuples with card data, target language and expected result."""
    # Same-language identity translation
    yield CardIdentificationData("en", "Forest"), "en", "Forest"
    yield CardIdentificationData("de", "Wald"), "de", "Wald"

Changes to tests/test_check_card_rendering.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import pytest
from hamcrest import *
from pytestqt.qtbot import QtBot
from PyQt5.QtGui import QPixmap, QColorConstants
from PySide6.QtGui import QPixmap, QColorConstants

from mtg_proxy_printer.model.carddb import Card, CheckCard, MTGSet
from mtg_proxy_printer.units_and_sizes import CardSizes


@pytest.fixture
def blank_image(qtbot) -> QPixmap:

Changes to tests/test_document.py.

16
17
18
19
20
21
22
23
24



25
26
27
28
29
30
31
16
17
18
19
20
21
22


23
24
25
26
27
28
29
30
31
32







-
-
+
+
+







import copy
import dataclasses
import pathlib
import typing
import unittest.mock
import textwrap

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap

from hamcrest import *

from hamcrest import contains_exactly
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.card_list import PageColumns
358
359
360
361
362
363
364
365

366
367
368
369
370
371
372
373
359
360
361
362
363
364
365

366

367
368
369
370
371
372
373







-
+
-







    custom_layout = PageLayoutSettings(
        page_height=300*mm, page_width=200*mm,
        margin_top=20*mm, margin_bottom=19*mm, margin_left=18*mm, margin_right=17*mm,
        row_spacing=3*mm, column_spacing=2*mm, card_bleed=1*mm,
        draw_cut_markers=True, draw_sharp_corners=False,
    )
    document.apply(ActionEditDocumentSettings(custom_layout))
    yield document
    return document
    document.__dict__.clear()


def test_document_reset_clears_modified_page_layout(qtbot: QtBot, document_custom_layout: Document):
    default_layout = PageLayoutSettings.create_from_settings()
    assert_that(
        document_custom_layout,
        has_property("page_layout", not_(equal_to(default_layout)))

Changes to tests/test_image_db.py.

11
12
13
14
15
16
17
18
19


20
21
22
23
24
25
26
11
12
13
14
15
16
17


18
19
20
21
22
23
24
25
26







-
-
+
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import io

from PyQt5.QtCore import QBuffer, QIODevice
from PyQt5.QtGui import QPixmap
from PySide6.QtCore import QBuffer, QIODevice
from PySide6.QtGui import QPixmap
from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.imagedb import ImageDatabase, ImageKey


def qpixmap_to_bytes_io(pixmap: QPixmap) -> io.BytesIO:
51
52
53
54
55
56
57
58
51
52
53
54
55
56
57








-
    image_db.delete_disk_cache_entries([keys[0]])
    assert_that((image_db.db_path / keys[0].format_relative_path()).is_file(), is_(False))
    assert_that((image_db.db_path / keys[1].format_relative_path()).is_file(), is_(True))
    assert_that((image_db.db_path / keys[0].format_relative_path()).parent.is_dir(), is_(True))
    image_db.delete_disk_cache_entries([keys[1]])
    assert_that((image_db.db_path / keys[1].format_relative_path()).is_file(), is_(False))
    assert_that((image_db.db_path / keys[0].format_relative_path()).parent.is_dir(), is_(False))

Changes to tests/test_known_card_image_model.py.

16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
16
17
18
19
20
21
22

23
24
25
26
27
28
29
30







-
+







"""
Tests the KnownCardImageModel used internally by the CacheCleanupWizard.
"""

import pathlib
import typing

from PyQt5.QtCore import Qt
from PySide6.QtCore import Qt
import pytest
from hamcrest import *

from mtg_proxy_printer.ui.cache_cleanup_wizard import KnownCardImageModel, KnownCardColumns
from mtg_proxy_printer.model.carddb import CardDatabase
from mtg_proxy_printer.model.imagedb import ImageDatabase

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
48
49
50
51
52
53
54

55
56
57
58
59
60
61







-







    front_image = image_db.db_path/"lowres_front"/"b3"/"b3b87bfc-f97f-4734-94f6-e3e2f335fc4d.png"
    back_image = image_db.db_path/"lowres_back"/"b3"/"b3b87bfc-f97f-4734-94f6-e3e2f335fc4d.png"
    front_image.parent.mkdir(parents=True)
    back_image.parent.mkdir(parents=True)
    image_db.blank_image.save(str(front_image), "PNG")
    image_db.blank_image.save(str(back_image), "PNG")
    yield Environment(card_db, image_db, front_image, back_image)
    image_db.__dict__.clear()


@pytest.mark.parametrize("is_hidden", [True, False])
@pytest.mark.parametrize("is_front", [True, False])
def test_add_row_identifies_low_resolution_images(environment: Environment, is_front: bool, is_hidden: bool):
    model = KnownCardImageModel(environment.card_db)
    card = environment.card_db.get_card_with_scryfall_id("b3b87bfc-f97f-4734-94f6-e3e2f335fc4d", is_front)

Changes to tests/test_page_layout_settings.py.

17
18
19
20
21
22
23
24
25


26
27
28
29
30
31
32
17
18
19
20
21
22
23


24
25
26
27
28
29
30
31
32







-
-
+
+








import mtg_proxy_printer.settings
import mtg_proxy_printer.model.document
import mtg_proxy_printer.model.document_loader
from mtg_proxy_printer.units_and_sizes import PageType, QuantityT, UnitT, unit_registry, StrDict
from mtg_proxy_printer.ui.page_scene import RenderMode

from PyQt5.QtGui import QPageLayout, QPageSize
from PyQt5.QtCore import QMarginsF
from PySide6.QtGui import QPageLayout, QPageSize
from PySide6.QtCore import QMarginsF
import pytest
from hamcrest import *
PageLayoutSettings = mtg_proxy_printer.model.document_loader.PageLayoutSettings

from tests.hasgetter import has_getters
from tests.helpers import quantity_close_to

Changes to tests/ui/settings/test_card_filter_widgets.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import typing
from unittest.mock import patch

from PyQt5.QtWidgets import QCheckBox
from PySide6.QtWidgets import QCheckBox
import pytest
from hamcrest import *

from mtg_proxy_printer.units_and_sizes import SectionProxy
import mtg_proxy_printer.settings
from mtg_proxy_printer.ui.printing_filter_widgets import AbstractPrintingFilter, GeneralPrintingFilter, \
    FormatPrintingFilter

Changes to tests/ui/settings/test_initial_page_selection.py.

9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23







-
+







# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from PyQt5.QtCore import QStringListModel
from PySide6.QtCore import QStringListModel

from hamcrest import *
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.ui.settings_window import SettingsWindow


Changes to tests/ui/test_add_card.py.

15
16
17
18
19
20
21
22
23


24
25
26
27
28
29
30
15
16
17
18
19
20
21


22
23
24
25
26
27
28
29
30







-
-
+
+








import typing

import pytest
from hamcrest import *
from pytestqt.qtbot import QtBot

from PyQt5.QtCore import Qt, QPoint, QRect, QItemSelectionModel
from PyQt5.QtWidgets import QDialogButtonBox
from PySide6.QtCore import Qt, QPoint, QRect, QItemSelectionModel
from PySide6.QtWidgets import QDialogButtonBox

from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData
from mtg_proxy_printer.ui.add_card import HorizontalAddCardWidget, VerticalAddCardWidget

from tests.helpers import fill_card_database_with_json_card

StringList = typing.List[str]
42
43
44
45
46
47
48



49


50
51
52
53
54
55

56
42
43
44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61







+
+
+
-
+
+






+

    """
    fill_card_database_with_json_card(qtbot, card_db, "english_double_faced_art_series_card")
    expected_card_identification_data = CardIdentificationData(
        "en", "Clearwater Pathway", "aznr", "25"
    )
    qtbot.add_widget(add_card_widget := widget_class())
    add_card_widget.set_card_database(card_db)
    add_card_widget.card_name_filter_updated("")  # Populate the card name list
    add_card_widget.ui.card_name_list.setSelection(QRect(1, 1, 1, 1), ClearAndSelect)
    qtbot.mouseClick(add_card_widget.ui.card_name_list, LeftButton, pos=QPoint(10, 10))
    add_card_widget.ui.copies_input.setValue(1)
    ok_button = add_card_widget.ui.button_box.button(StandardButton.Ok)
    qtbot.mouseClick(ok_button, LeftButton, pos=QPoint(10, 10))
    add_card_widget.ui.card_name_list.setSelection(QRect(1, 1, 1, 1), ClearAndSelect)
    qtbot.mouseClick(add_card_widget.ui.card_name_list, LeftButton, pos=QPoint(10, 10))
    qtbot.wait(10)
    qtbot.mouseClick(
        add_card_widget.ui.button_box.button(StandardButton.Ok), LeftButton
    )
    add_card_widget.ui.copies_input.setValue(1)
    assert_that(add_card_widget._read_card_data_from_ui(), is_(equal_to(expected_card_identification_data)))

Changes to tests/ui/test_card_item.py.

11
12
13
14
15
16
17
18
19
20



21
22
23
24
25
26
27
11
12
13
14
15
16
17



18
19
20
21
22
23
24
25
26
27







-
-
-
+
+
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from hamcrest import *
import pytest
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QColorConstants, QPixmap, QImage, QPainter, QColor
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsScene
from PySide6.QtCore import QSize
from PySide6.QtGui import QColorConstants, QPixmap, QImage, QPainter, QColor
from PySide6.QtWidgets import QGraphicsItem, QGraphicsScene

from mtg_proxy_printer.ui.page_scene import CardItem

from tests.document_controller.helpers import append_new_card_in_page
from tests.hasgetter import has_getter


Changes to tests/ui/test_central_widget.py.

14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28
29







-
+
+







# along with this program. If not, see <http://www.gnu.org/licenses/>.

from pathlib import PurePath
from unittest.mock import NonCallableMagicMock, patch

import pytest
from pytestqt.qtbot import QtBot
from PyQt5.QtCore import Qt
from PySide6.QtCore import Qt

from hamcrest import *

from mtg_proxy_printer.model.document_page import Page
from mtg_proxy_printer.model.carddb import Card, MTGSet, CheckCard
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard

# Import dynamically used by pytest. Without this, the main_window fixture won’t be found by pytest.

Changes to tests/ui/test_deck_import_wizard.py.

18
19
20
21
22
23
24
25
26
27



28
29
30
31
32
33
34
18
19
20
21
22
23
24



25
26
27
28
29
30
31
32
33
34







-
-
-
+
+
+







import unittest.mock
from unittest.mock import MagicMock

from hamcrest import *
from pytestqt.qtbot import QtBot
import pytest

from PyQt5.QtCore import QStringListModel, Qt, QPoint, QObject
from PyQt5.QtWidgets import QCheckBox, QWizard, QTableView, QComboBox, QLineEdit
from PyQt5.QtTest import QTest
from PySide6.QtCore import QStringListModel, Qt, QPoint, QObject
from PySide6.QtWidgets import QCheckBox, QWizard, QTableView, QComboBox, QLineEdit
from PySide6.QtTest import QTest

import mtg_proxy_printer.settings
from mtg_proxy_printer.model.carddb import CardDatabase, CardIdentificationData, CardList
from mtg_proxy_printer.ui.deck_import_wizard import DeckImportWizard
from mtg_proxy_printer.decklist_parser.re_parsers import MTGOnlineParser, MTGArenaParser, \
    GenericRegularExpressionDeckParser
from mtg_proxy_printer.model.card_list import PageColumns

Changes to tests/ui/test_item_delegate.py.

13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27







-
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from collections import Counter
import itertools
from unittest.mock import NonCallableMagicMock

from PyQt5.QtWidgets import QComboBox
from PySide6.QtWidgets import QComboBox
import pytest
from hamcrest import *

from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
from mtg_proxy_printer.model.carddb import CardDatabase, Card, MTGSet
from mtg_proxy_printer.model.card_list import CardListModel
from mtg_proxy_printer.model.document import Document

Changes to tests/ui/test_main_window.py.

13
14
15
16
17
18
19
20
21
22


23
24
25
26
27
28
29
13
14
15
16
17
18
19



20
21
22
23
24
25
26
27
28







-
-
-
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import dataclasses
import pathlib
import unittest.mock


from PyQt5.QtCore import QStringListModel, QThreadPool
from PyQt5.QtWidgets import QMessageBox
from PySide6.QtCore import QStringListModel, QThreadPool
from PySide6.QtWidgets import QMessageBox
from pytestqt.qtbot import QtBot
from hamcrest import *
import pytest

import mtg_proxy_printer.http_file
import mtg_proxy_printer.downloader_base
from mtg_proxy_printer.sqlite_helpers import open_database
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
57
58
59
60
61
62
63


64
65
66
67
68
69
70







-
-







        cid = CardInfoDownloader(card_db)
        main_window = MainWindow(card_db, cid, document.image_db, document, QStringListModel(["en"]))
        qtbot.add_widget(main_window)
        with qtbot.wait_exposed(main_window, timeout=1000):
            main_window.show()
        yield main_window
        main_window.hide()
        del cid
        main_window.__dict__.clear()


def test_main_window_hides_progress_bar_after_downloading_image_during_load(
        qtbot: QtBot, main_window: MainWindow):
    with unittest.mock.patch.object(  # Mock all HTTP-specific I/O calls
                mtg_proxy_printer.downloader_base.mtg_proxy_printer.http_file.MeteredSeekableHTTPFile,
                "_read_content_length") as cl_mock, \
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
137
138
139
140
141
142
143

144
145
146
147
148
149
150
151
152
153
154
155

156
157
158
159
160
161
162







-












-







        unittest.mock.patch(
            "mtg_proxy_printer.card_info_downloader.CardInfoDatabaseImportWorker.import_card_data_from_online_api") as import_from_api, \
        unittest.mock.patch.object(QThreadPool.globalInstance(), "start") as thread_pool_start, \
            qtbot.assertNotEmitted(main_window.loading_state_changed):
        main_window.show_card_data_update_available_message_box(10000)
    thread_pool_start.assert_not_called()
    import_from_api.assert_not_called()
    assert_that(ui.action_download_card_data.isEnabled(), is_(True))


def test_accepting_card_data_update_offer_results_in_performed_action(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(True)
    with unittest.mock.patch.object(
        mtg_proxy_printer.ui.main_window.QMessageBox,
            "question", return_value=StandardButton.Yes) as message_box, \
            unittest.mock.patch.object(QThreadPool.globalInstance(), "start") as thread_pool_start:
        main_window.show_card_data_update_available_message_box(10000)
    message_box.assert_called_once()
    thread_pool_start.assert_called_once()
    assert_that(ui.action_download_card_data.isEnabled(), is_(False))


def test_action_download_card_data_is_enabled_after_network_error(qtbot: QtBot, main_window: MainWindow):
    ui = main_window.ui
    ui.action_download_card_data.setEnabled(False)
    with unittest.mock.patch.object(
        mtg_proxy_printer.ui.main_window.QMessageBox, "warning", return_value=StandardButton.Ok

Changes to tests/ui/test_page_config_container.py.

9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23







-
+







# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from PyQt5.QtWidgets import QCheckBox, QDoubleSpinBox, QLineEdit
from PySide6.QtWidgets import QCheckBox, QDoubleSpinBox, QLineEdit

import pytest
from pytestqt.qtbot import QtBot
from hamcrest import *

from mtg_proxy_printer.model.document_loader import PageLayoutSettings
from mtg_proxy_printer.units_and_sizes import QuantityT, unit_registry

Changes to tests/ui/test_page_config_dialog.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import pytest

from PyQt5.QtWidgets import QDialogButtonBox
from PySide6.QtWidgets import QDialogButtonBox
from mtg_proxy_printer.ui.dialogs import DocumentSettingsDialog
StandardButton = QDialogButtonBox.StandardButton


def test__init__(qtbot, document_light):
    """Ensure that the dialog can be instantiated"""
    DocumentSettingsDialog(document_light)

Changes to tests/ui/test_page_config_widget.py.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26







-
+







#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from unittest.mock import patch

import pint
from PyQt5.QtWidgets import QDoubleSpinBox, QCheckBox, QLineEdit
from PySide6.QtWidgets import QDoubleSpinBox, QCheckBox, QLineEdit

from hamcrest import *
import pytest
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.model.document_loader import PageLayoutSettings
import mtg_proxy_printer.settings
118
119
120
121
122
123
124

125
126
127
128
129
130
131
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132







+







    assert_that(ui, has_property(attribute_name, instance_of(QLineEdit)))
    assert_that(widget.page_layout, has_property(attribute_name, instance_of(str)))
    line_edit: QLineEdit = getattr(ui, attribute_name)
    new_value = "Test"
    with qtbot.waitSignals([line_edit.textChanged, widget.page_layout_changed], timeout=100):
        line_edit.setText(new_value)
    assert_that(widget.page_layout, has_property(attribute_name, equal_to(new_value)))



ZeroMarginsSettings = {
    "paper-height": "297 mm",
    "paper-width": "210 mm",
    "margin-top": "0 mm",
    "margin-bottom": "0 mm",

Changes to tests/ui/test_page_renderer.py.

13
14
15
16
17
18
19
20
21


22
23
24
25
26
27
28
13
14
15
16
17
18
19


20
21
22
23
24
25
26
27
28







-
-
+
+







# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from unittest.mock import patch

import pytest
from hamcrest import *
from PyQt5.QtCore import QEvent
from PyQt5.QtWidgets import QAction
from PySide6.QtCore import QEvent
from PySide6.QtGui import QAction

from mtg_proxy_printer.ui.page_renderer import PageRenderer, ZoomDirection

PATH_PREFIX = "mtg_proxy_printer.ui.page_renderer."


@pytest.fixture
50
51
52
53
54
55
56
57

50
51
52
53
54
55
56

57







-
+


@pytest.mark.parametrize("zoom_action, direction", [
    ("zoom_in_action", ZoomDirection.IN), ("zoom_out_action", ZoomDirection.OUT)])
def test_renderer_zoom_action_triggers_zoom(renderer: PageRenderer, zoom_action: str, direction: ZoomDirection):
    action: QAction = getattr(renderer, zoom_action)
    action.trigger()
    renderer._perform_zoom_step.assert_called_once_with(direction, False)
    renderer._perform_zoom_step.assert_called_once_with(direction)

Changes to tests/ui/test_page_scene.py.

19
20
21
22
23
24
25
26
27
28



29
30
31
32
33
34
35
19
20
21
22
23
24
25



26
27
28
29
30
31
32
33
34
35







-
-
-
+
+
+







import typing
from unittest.mock import patch
from math import ceil

from hamcrest import *
import pytest

from PyQt5.QtWidgets import QGraphicsPixmapItem, QGraphicsLineItem
from PyQt5.QtGui import QPalette, QColorConstants, QPixmap, QImage, QColor, QPainter
from PyQt5.QtCore import QPoint
from PySide6.QtWidgets import QGraphicsPixmapItem, QGraphicsLineItem
from PySide6.QtGui import QPalette, QColorConstants, QPixmap, QImage, QColor, QPainter
from PySide6.QtCore import QPoint

from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize, UnitT, unit_registry, QuantityT
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard, ActionRemoveCards
from mtg_proxy_printer.document_controller.compact_document import ActionCompactDocument
from mtg_proxy_printer.model.document import Document

Changes to tests/ui/test_progress_bar.py.

11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25







-
+







# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from hamcrest import *
import pytest
from PyQt5.QtWidgets import QWidget
from PySide6.QtWidgets import QWidget
from pytestqt.qtbot import QtBot

from mtg_proxy_printer.ui.progress_bar import ProgressBar
from tests.hasgetter import has_getters


INNER_ELEMENTS = ["inner_progress_bar", "inner_progress_label"]