MIL-009: Executable with PyInstaller
Review record RC-048 (Go, autonomous mode, not independent). Task: MIL-009#1 Task: MIL-009#2 Task: MIL-009#3 Task: MIL-009#4 Task: MIL-009#5 Task: MIL-009#6 Task: MIL-009#7 Task: MIL-009#8
This commit is contained in:
@@ -9,6 +9,8 @@ from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from flash_card.runtime import executable_dir, is_frozen, resource_root
|
||||
|
||||
## @brief Window background colour.
|
||||
BACKGROUND_COLOR: Final[str] = "#B1DDC6"
|
||||
## @brief Window title.
|
||||
@@ -77,12 +79,24 @@ VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv"
|
||||
## @brief File name of the words still to learn.
|
||||
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
|
||||
|
||||
## @brief Root folder of the project.
|
||||
## @brief Root folder of the project, for a source run.
|
||||
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
|
||||
## @brief Default folder of the data files.
|
||||
DEFAULT_DATA_DIR: Final[Path] = PROJECT_ROOT / "data"
|
||||
## @brief Folder of the image assets.
|
||||
ASSETS_DIR: Final[Path] = Path(__file__).resolve().parent / "presentation" / "assets"
|
||||
## @brief Folder of the image assets, inside the bundle when frozen.
|
||||
ASSETS_DIR: Final[Path] = (
|
||||
resource_root() / "flash_card" / "presentation" / "assets"
|
||||
if is_frozen()
|
||||
else Path(__file__).resolve().parent / "presentation" / "assets"
|
||||
)
|
||||
## @brief Folder of the word list delivered with the program.
|
||||
BUNDLED_DATA_DIR: Final[Path] = resource_root() / "data"
|
||||
## @brief The word list delivered with the program.
|
||||
BUNDLED_VOCABULARY_PATH: Final[Path] = BUNDLED_DATA_DIR / VOCABULARY_FILE_NAME
|
||||
## @brief Default folder of the data files, next to the executable when frozen.
|
||||
DEFAULT_DATA_DIR: Final[Path] = executable_dir() / "data"
|
||||
## @brief File name of the program icon.
|
||||
ICON_FILE_NAME: Final[str] = "flash_card.ico"
|
||||
## @brief Name of the executable without extension.
|
||||
EXECUTABLE_NAME: Final[str] = "flash-card"
|
||||
## @brief File name of the card front image.
|
||||
CARD_FRONT_IMAGE_NAME: Final[str] = "card_front.png"
|
||||
## @brief File name of the card back image.
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import NoReturn
|
||||
from flash_card.application.ports.speech_port import SpeechPort
|
||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||
from flash_card.constants import (
|
||||
BUNDLED_VOCABULARY_PATH,
|
||||
DEFAULT_DATA_DIR,
|
||||
LEVEL_ARGUMENT_VALUES,
|
||||
PROGRESS_FILE_NAME,
|
||||
@@ -151,6 +152,20 @@ class Application:
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def resolve_data_files(data_dir: Path | None) -> tuple[Path, Path]:
|
||||
"""Find the word list and the progress file.
|
||||
|
||||
@param data_dir Folder given by the user, or None for the default folder.
|
||||
@return The vocabulary path and the progress path. Without a given folder,
|
||||
the delivered word list is used when the default folder has none.
|
||||
"""
|
||||
folder = DEFAULT_DATA_DIR if data_dir is None else data_dir
|
||||
vocabulary = folder / VOCABULARY_FILE_NAME
|
||||
if data_dir is None and not vocabulary.exists():
|
||||
vocabulary = BUNDLED_VOCABULARY_PATH
|
||||
return vocabulary, folder / PROGRESS_FILE_NAME
|
||||
|
||||
|
||||
def create_application(
|
||||
data_dir: Path | None = None,
|
||||
speech_enabled: bool = True,
|
||||
@@ -158,19 +173,20 @@ def create_application(
|
||||
) -> Application:
|
||||
"""Create the window and show the level screen, or start at the given level.
|
||||
|
||||
@param data_dir Folder of the data files, or None for the default folder.
|
||||
@param data_dir Folder of the data files, or None for the default folder
|
||||
with the delivered word list as fallback.
|
||||
@param speech_enabled False to run without spoken words.
|
||||
@param level The level to start at, or None to show the level screen.
|
||||
@return The application, its window in the root attribute.
|
||||
@throws VocabularyError When a level is given and the word list or saved
|
||||
progress is unusable.
|
||||
"""
|
||||
folder = DEFAULT_DATA_DIR if data_dir is None else data_dir
|
||||
vocabulary_path, progress_path = resolve_data_files(data_dir)
|
||||
root = tk.Tk()
|
||||
application = Application(
|
||||
root,
|
||||
CsvVocabularyRepository(folder / VOCABULARY_FILE_NAME),
|
||||
CsvProgressRepository(folder / PROGRESS_FILE_NAME),
|
||||
CsvVocabularyRepository(vocabulary_path),
|
||||
CsvProgressRepository(progress_path),
|
||||
build_speech(speech_enabled),
|
||||
)
|
||||
if level is None:
|
||||
@@ -209,8 +225,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser.add_argument(
|
||||
"--data-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_DATA_DIR,
|
||||
help="folder of the data files",
|
||||
default=None,
|
||||
help="folder of the data files (default: data next to the program)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-speech",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -21,6 +21,7 @@ from flash_card.presentation.features.level.level_view_model import (
|
||||
LevelChoice,
|
||||
LevelViewModel,
|
||||
)
|
||||
from flash_card.presentation.shared.window import configure_window
|
||||
|
||||
|
||||
class LevelView:
|
||||
@@ -40,6 +41,7 @@ class LevelView:
|
||||
"""
|
||||
## @brief Called with the level the user picked.
|
||||
self._on_choose = on_choose
|
||||
configure_window(root)
|
||||
## @brief Frame that holds the whole screen.
|
||||
self.frame = tk.Frame(
|
||||
root, bg=BACKGROUND_COLOR, width=CARD_WIDTH, height=LEVEL_SCREEN_HEIGHT
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Locate resources and the executable folder, frozen or from source.
|
||||
|
||||
@file runtime.py
|
||||
@brief Packaging support: tells where bundled files and the executable are.
|
||||
Imports only the standard library; it is not one of the layers.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
## @brief Number of folders from this file up to the project root.
|
||||
_ROOT_LEVELS = 2
|
||||
|
||||
|
||||
def is_frozen() -> bool:
|
||||
"""Tell whether the program runs as a PyInstaller executable.
|
||||
|
||||
@return True when frozen.
|
||||
"""
|
||||
return bool(getattr(sys, "frozen", False))
|
||||
|
||||
|
||||
def source_root() -> Path:
|
||||
"""Return the project root of a source run.
|
||||
|
||||
@return The folder that holds src.
|
||||
"""
|
||||
return Path(__file__).resolve().parents[_ROOT_LEVELS]
|
||||
|
||||
|
||||
def pick_resource_root(frozen: bool, bundle_dir: Path | None, root: Path) -> Path:
|
||||
"""Choose the folder that holds the bundled resources.
|
||||
|
||||
@param frozen True when running as an executable.
|
||||
@param bundle_dir The unpack folder of the executable, when frozen.
|
||||
@param root The project root of a source run.
|
||||
@return The bundle folder when frozen and known, else the project root.
|
||||
"""
|
||||
if frozen and bundle_dir is not None:
|
||||
return bundle_dir
|
||||
return root
|
||||
|
||||
|
||||
def pick_executable_dir(frozen: bool, executable: Path, root: Path) -> Path:
|
||||
"""Choose the folder next to the executable.
|
||||
|
||||
@param frozen True when running as an executable.
|
||||
@param executable Path of the running executable.
|
||||
@param root The project root of a source run.
|
||||
@return The folder of the executable when frozen, else the project root.
|
||||
"""
|
||||
if frozen:
|
||||
return executable.parent
|
||||
return root
|
||||
|
||||
|
||||
def resource_root() -> Path:
|
||||
"""Return the folder that holds the bundled resources.
|
||||
|
||||
@return The PyInstaller unpack folder when frozen, else the project root.
|
||||
"""
|
||||
bundle = getattr(sys, "_MEIPASS", None)
|
||||
bundle_dir = Path(bundle) if bundle else None
|
||||
return pick_resource_root(is_frozen(), bundle_dir, source_root())
|
||||
|
||||
|
||||
def executable_dir() -> Path:
|
||||
"""Return the folder of the running executable.
|
||||
|
||||
@return The folder of the executable when frozen, else the project root.
|
||||
"""
|
||||
return pick_executable_dir(is_frozen(), Path(sys.executable), source_root())
|
||||
Reference in New Issue
Block a user