Skip to content

Commit

Permalink
Merge pull request #135 from SAMOXA/pathlib_and_cross_os_paths
Browse files Browse the repository at this point in the history
Pathlib and cross os paths
  • Loading branch information
Himura2la authored Feb 28, 2019
2 parents bbd2b70 + f8085da commit 53ea314
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 71 deletions.
52 changes: 30 additions & 22 deletions src/main.pyw
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import wx.grid
from background_music_player import BackgroundMusicPlayer
from constants import Config, Colors, Columns, FileTypes, Strings
from projector import ProjectorWindow
from settings import SettingsDialog, path_make_abs
from settings import SettingsDialog
from logger import Logger
from file_replacer import FileReplacer
from text_window import TextWindow
from os_tools import path

locale_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'locale')
if os.path.isfile(os.path.join(locale_dir, 'ru', 'LC_MESSAGES', 'main.mo')):
Expand Down Expand Up @@ -66,23 +67,22 @@ class MainWindow(wx.Frame):
Config.COUNTDOWN_TIME_FMT: u"Ждём Вас в %s ^_^"}

self.config_ok = False
self.session_file_path = ''
self.fest_file_path = ''
if os.path.isfile(Config.LAST_SESSION_PATH):
try:
self.session_file_path = open(Config.LAST_SESSION_PATH, 'r', encoding='utf-8-sig').read()
except UnicodeDecodeError as e:
self.fest_file_path = open(Config.LAST_SESSION_PATH, 'r', encoding='utf-8-sig').read()
except UnicodeDecodeError:
try:
self.session_file_path = open(Config.LAST_SESSION_PATH, 'r', encoding='latin-1').read()
except UnicodeDecodeError as e:
self.fest_file_path = open(Config.LAST_SESSION_PATH, 'r', encoding='latin-1').read()
except UnicodeDecodeError:
print("Fail to read last session file.")
self.session_file_path = ''
self.fest_file_path = ''

if not os.path.isabs(self.session_file_path):
self.session_file_path = os.path.normpath(os.path.join(os.path.abspath(__file__), self.session_file_path))
self.fest_file_path = path.make_abs(self.fest_file_path)

if os.path.isfile(self.session_file_path):
if os.path.isfile(self.fest_file_path):
try:
loaded_config = json.load(open(self.session_file_path, 'r', encoding='utf-8-sig'))
loaded_config = json.load(open(self.fest_file_path, 'r', encoding='utf-8-sig'))
config_keys_diff = set(base_config.keys()) - set(loaded_config.keys())
if config_keys_diff:
self.logger.log("[WARNING] Config file is missing the following keys: " + str(config_keys_diff))
Expand All @@ -91,8 +91,13 @@ class MainWindow(wx.Frame):
except json.decoder.JSONDecodeError as e:
msg = _("Unfortunately, you broke the JSON format...\n"
"Please fix the configuration file%s ASAP.\n\nDetails: %s") % \
("\n(%s)" % self.session_file_path, str(e))
("\n(%s)" % self.fest_file_path, str(e))
wx.MessageBox(msg, "JSON Error", wx.OK | wx.ICON_ERROR, self)
else:
self.logger.log("Session path %s is not file" % self.fest_file_path)
self.fest_file_path = ''

path.fest_file = self.fest_file_path # TODO: Remove self.fest_file_path

if not self.config_ok:
self.config = base_config
Expand All @@ -117,7 +122,7 @@ class MainWindow(wx.Frame):
self.Bind(wx.EVT_TIMER, self.on_background_timer, self.bg_player_timer)

self.bg_tracks_dir = None
self.files_dirs = [path_make_abs(d, self.session_file_path) for d in self.config[Config.FILES_DIRS]]
self.files_dirs = [path.make_abs(d, path.fest_file) for d in self.config[Config.FILES_DIRS]]

# ------------------ Menu ------------------
menu_bar = wx.MenuBar()
Expand All @@ -130,8 +135,8 @@ class MainWindow(wx.Frame):

menu_file.AppendSeparator()

if self.session_file_path:
session_folder, session_file = os.path.split(self.session_file_path)
if self.fest_file_path:
session_folder, session_file = os.path.split(self.fest_file_path)
self.Bind(wx.EVT_MENU, lambda e: webbrowser.open(os.path.abspath(session_folder)),
menu_file.Append(wx.ID_ANY, _("Open &Folder with '%s'") % session_file))

Expand Down Expand Up @@ -515,15 +520,16 @@ class MainWindow(wx.Frame):

def on_settings(self, e=None):
prev_config = copy.copy(self.config)
with SettingsDialog(self.session_file_path, self.config, self) as settings_dialog:
with SettingsDialog(self.fest_file_path, self.config, self) as settings_dialog:
action = settings_dialog.ShowModal()

self.session_file_path = settings_dialog.session_file_path # To be sure.
self.fest_file_path = path.make_abs(settings_dialog.fest_file_path) # To be sure.
path.fest_file = self.fest_file_path
self.config = settings_dialog.config # Maybe redundant
self.config_ok = action in {wx.ID_SAVE, wx.ID_OPEN}

if prev_config != self.config: # Safety is everything!
bkp_name = "%s-%s.bkp.fest" % (os.path.splitext(self.session_file_path)[0],
bkp_name = "%s-%s.bkp.fest" % (os.path.splitext(self.fest_file_path)[0],
time.strftime("%d%m%y%H%M%S", time.localtime()))
json.dump(prev_config, open(bkp_name, 'w', encoding='utf-8'),
ensure_ascii=False, indent=4)
Expand Down Expand Up @@ -627,7 +633,8 @@ class MainWindow(wx.Frame):
return
self.proj_win.switch_to_images()
if self.config[Config.BG_ZAD_PATH] and not no_show:
self.proj_win.load_zad(path_make_abs(self.config[Config.BG_ZAD_PATH], self.session_file_path), True)
self.proj_win.load_zad(path.make_abs(self.config[Config.BG_ZAD_PATH],
path.fest_file), True)
self.image_status("Background")
else:
self.proj_win.no_show()
Expand Down Expand Up @@ -689,6 +696,7 @@ class MainWindow(wx.Frame):
all_files = [item for sublist in all_files for item in sublist] # Flatten

for file_path in all_files:
# FIXME: check that file_path is file. Otherwise there will be a crash.
name, ext = os.path.basename(file_path).rsplit('.', 1)
ext = ext.lower() # Never forget doing this!
match = re.search(self.filename_re, name)
Expand Down Expand Up @@ -738,7 +746,7 @@ class MainWindow(wx.Frame):

self.add_countdown_row(False, 0, self.config[Config.COUNTDOWN_OPENING_TEXT])

self.SetLabel("%s: %s" % (Strings.APP_NAME, self.session_file_path))
self.SetLabel("%s: %s" % (Strings.APP_NAME, self.fest_file_path))

self.load_data_item.Enable(False) # Safety is everything!

Expand Down Expand Up @@ -1131,7 +1139,7 @@ class MainWindow(wx.Frame):
# -------------------------------------------- Background Music Player --------------------------------------------

def on_bg_load_files(self, e=None):
self.bg_tracks_dir = path_make_abs(self.config[Config.BG_TRACKS_DIR], self.session_file_path)
self.bg_tracks_dir = path.make_abs(self.config[Config.BG_TRACKS_DIR], path.fest_file)
if not self.config[Config.BG_TRACKS_DIR] or not os.path.isdir(self.bg_tracks_dir):
msg = _("Background MP3 path is invalid. Please specify a\n"
"valid path with your background tracks in settings.\n\n"
Expand Down Expand Up @@ -1249,7 +1257,7 @@ class MainWindow(wx.Frame):
if Config.C2_DATABASE_PATH not in self.config or not self.config[Config.C2_DATABASE_PATH]:
self.status(_("No Cosplay2 database in config"))
return
db_path = path_make_abs(self.config[Config.C2_DATABASE_PATH], self.session_file_path)
db_path = path.make_abs(self.config[Config.C2_DATABASE_PATH], path.fest_file)
if not os.path.isfile(db_path):
self.status(_("Cosplay2 database not found"))
return
Expand Down
62 changes: 62 additions & 0 deletions src/os_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# This file contains OS-level stuff (paths, strings, locale, etc)
# By default, the app uses absolute paths with POSIX notation.
# All path translations are performed on start (change settings, read config, etc)


import os
import sys
from pathlib import Path, PureWindowsPath


class PathTools(object):
def __init__(self):
if getattr(sys, 'frozen', False):
self._work_dir = str(Path(sys._MEIPASS))
else:
self._work_dir = str(Path(__file__).resolve().parent)
self._fest_file = None

@property
def work_dir(self):
return self._work_dir

@property
def fest_file(self):
return self._fest_file

@fest_file.setter
def fest_file(self, fest_file):
self._fest_file = str(Path(fest_file).resolve())

def make_abs(self, path, anchor=None):
path, anchor = self._prepare_paths(path, anchor)
return str(Path(os.path.join(str(anchor), str(path))).resolve())

def make_rel(self, path, anchor=None):
path, anchor = self._prepare_paths(path, anchor)

if self._can_make_rel(path, anchor):
# Path().relative_to() have differ semantic with os.path.relpath()
return str(os.path.relpath(str(path.resolve()), str(anchor)))
else:
return str(path.resolve())

def _prepare_paths(self, path, anchor):
path = Path(PureWindowsPath(path).as_posix()) if os.name == 'posix' and '\\' in str(path) else Path(path)
if anchor is None:
anchor = Path(self._work_dir)
else:
anchor = Path(anchor).resolve()
if anchor.is_file():
anchor = anchor.parent
return path, anchor

@staticmethod
def _can_make_rel(path, anchor):
if not path.exists() or not anchor.exists():
return False
if os.lstat(path.resolve().as_posix()).st_dev != os.lstat(anchor.resolve().as_posix()).st_dev:
return False
return True

path = PathTools()
80 changes: 32 additions & 48 deletions src/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,19 @@
import json
import wx
from constants import Config, FileTypes


def path_make_abs(path, session_file_path):
if not path or os.path.isabs(path):
return path
else: # this is relative, so we calculate a path relative to a directory where the .fest file resides
session_file_dir = os.path.dirname(session_file_path)
return os.path.normpath(os.path.join(session_file_dir, path))


def path_session_try_to_relative(path):
session_file_stat = os.lstat(path)
if getattr(sys, 'frozen', False):
work_dir = sys._MEIPASS
else:
work_dir = os.path.dirname(os.path.abspath(__file__))
fest_engine_run_location = os.lstat(work_dir)
if session_file_stat.st_dev != fest_engine_run_location.st_dev:
return os.path.abspath(path)
else:
return os.path.relpath(path, os.path.abspath(__file__))
from os_tools import path


class SettingsDialog(wx.Dialog):
def __init__(self, session_file_path, config, parent):
def __init__(self, fest_file_path, config, parent):
wx.Dialog.__init__(self,
parent,
title=_("Settings"),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
self.session_file_path = session_file_path
self.fest_file_path = fest_file_path
self.config = config

if not session_file_path:
if not self.fest_file_path:
wx.MessageBox(_("Hi ^_^ Please select a new or existing *.fest file in the 'Current Fest' field.\n"
"It will load automatically on each start unless you change it. The configuration\n"
"may seem confusing, if you find it so, open the 'File | About' menu item.\n"
Expand All @@ -53,7 +33,7 @@ def __init__(self, session_file_path, config, parent):
style=wx.FLP_SAVE | wx.FLP_USE_TEXTCTRL,
wildcard="Fest Engine sessions (*.fest)|*.fest;*.fest_bkp")
self.Bind(wx.EVT_FILEPICKER_CHANGED, self.on_fest_selected, self.session_picker)
self.session_picker.SetPath(self.session_file_path)
self.session_picker.SetPath(self.fest_file_path)
session_sizer.Add(self.session_picker, 1, wx.EXPAND | wx.ALL, 5)
session_sizer.Add(wx.StaticLine(self.panel, style=wx.LI_VERTICAL), 0,
wx.EXPAND | wx.TOP | wx.BOTTOM | wx.RIGHT, 5)
Expand Down Expand Up @@ -88,20 +68,20 @@ def __init__(self, session_file_path, config, parent):

# Database File
self.db_path = wx.FilePickerCtrl(self.panel, wildcard="SQLite Databases|*.sqlite;*.db;*.data")
self.db_path.SetInitialDirectory(self.session_file_path)
self.db_path.SetPath(path_make_abs(self.config[Config.C2_DATABASE_PATH], self.session_file_path))
self.db_path.SetInitialDirectory(self.fest_file_path)
self.db_path.SetPath(path.make_abs(self.config[Config.C2_DATABASE_PATH], self.fest_file_path))
self.configs_grid.Add(wx.StaticText(self.panel, label=_("Cosplay2 Database Path")), 0, wx.ALIGN_CENTER_VERTICAL)
self.configs_grid.Add(self.db_path, 1, wx.EXPAND)

# Background Tracks
self.bg_tracks = wx.DirPickerCtrl(self.panel)
self.bg_tracks.SetPath(path_make_abs(self.config[Config.BG_TRACKS_DIR], self.session_file_path))
self.bg_tracks.SetPath(path.make_abs(self.config[Config.BG_TRACKS_DIR], path.fest_file))
self.configs_grid.Add(wx.StaticText(self.panel, label=_("Background Tracks Dir")), 0, wx.ALIGN_CENTER_VERTICAL)
self.configs_grid.Add(self.bg_tracks, 1, wx.EXPAND)

img_wc = "Images ({0})|{0}".format(";".join(["*.%s" % x for x in FileTypes.img_extensions]))
self.bg_zad = wx.FilePickerCtrl(self.panel, wildcard=img_wc)
self.bg_zad.SetPath(path_make_abs(self.config[Config.BG_ZAD_PATH], self.session_file_path))
self.bg_zad.SetPath(path.make_abs(self.config[Config.BG_ZAD_PATH], path.fest_file))
self.configs_grid.Add(wx.StaticText(self.panel, label=_("Background ZAD Path")), 0, wx.ALIGN_CENTER_VERTICAL)
self.configs_grid.Add(self.bg_zad, 1, wx.EXPAND)

Expand Down Expand Up @@ -150,7 +130,7 @@ def __init__(self, session_file_path, config, parent):
buttons_sizer.Add(button_cancel, 1)

self.top_sizer.Add(buttons_sizer, 0, wx.EXPAND | wx.ALL, 5)
self.on_fest_selected(first_run=not self.session_file_path)
self.on_fest_selected(first_run=not self.fest_file_path)

def add_dir(self, path=None):
dir_picker = wx.DirPickerCtrl(self.panel)
Expand Down Expand Up @@ -188,7 +168,8 @@ def process_children(sizer):
def on_fest_selected(self, e=None, first_run=False):
fest_file_exists = os.path.isfile(e.Path) if e else False
if fest_file_exists:
self.session_file_path = os.path.normpath(e.Path)
self.fest_file_path = path.make_abs(e.Path)
path.fest_file = self.fest_file_path
try:
self.config = json.load(open(e.Path, 'r', encoding='utf-8-sig'))
except json.decoder.JSONDecodeError as e:
Expand All @@ -206,16 +187,16 @@ def on_fest_selected(self, e=None, first_run=False):
self.enable_settings(not fest_file_exists)
self.button_save.Enable(not fest_file_exists)
self.button_load.Enable(fest_file_exists)
self.session_file_edit_btn.Enable(os.path.exists(self.session_file_path))
self.session_file_edit_btn.Enable(os.path.exists(self.fest_file_path))

def config_to_ui(self):
self.screens_combobox.SetSelection(self.config[Config.PROJECTOR_SCREEN])
self.db_path.SetPath(path_make_abs(self.config[Config.C2_DATABASE_PATH], self.session_file_path))
self.db_path.SetPath(path.make_abs(self.config[Config.C2_DATABASE_PATH], path.fest_file))
self.filename_re.SetValue(self.config[Config.FILENAME_RE])
self.bg_tracks.SetPath(path_make_abs(self.config[Config.BG_TRACKS_DIR], self.session_file_path))
self.bg_zad.SetPath(path_make_abs(self.config[Config.BG_ZAD_PATH], self.session_file_path))
self.bg_tracks.SetPath(path.make_abs(self.config[Config.BG_TRACKS_DIR], path.fest_file))
self.bg_zad.SetPath(path.make_abs(self.config[Config.BG_ZAD_PATH], path.fest_file))
[self.rm_dir() for i in range(len(self.dir_pickers))]
[self.add_dir(path_make_abs(path, self.session_file_path)) for path in self.config[Config.FILES_DIRS]]
[self.add_dir(path.make_abs(d, path.fest_file)) for d in self.config[Config.FILES_DIRS]]
self.panel.SetSizerAndFit(self.top_sizer)
self.top_sizer.Fit(self)
self.SetClientSize((self.GetClientSize()[0] + 300, self.GetClientSize()[1]))
Expand All @@ -229,14 +210,12 @@ def path_validate(self, widget, msg):
widget.SetPath("")
return ""

def path_try_relative(self, path):
session_file_dir = os.path.dirname(self.session_file_path) + os.sep
if os.path.normpath(path).startswith(session_file_dir):
return './' + os.path.relpath(path, session_file_dir).replace(os.sep, '/')
return path
def path_try_relative(self, p):
return path.make_rel(p, path.fest_file)

def ui_to_config(self):
""" Saves selected values from UI to JSON config """
# FIXME: prevent .fest file from saving if path validation failed

self.config[Config.PROJECTOR_SCREEN] = self.screens_combobox.GetSelection()
self.config[Config.FILENAME_RE] = self.filename_re.GetValue()
Expand Down Expand Up @@ -269,22 +248,27 @@ def ui_to_config(self):
)

def on_ok(self, e):
path = self.session_picker.GetPath()
fest_file_path = self.session_picker.GetPath()
ext = '.bkp.fest'
if path.find(ext) == len(path) - len(ext):
self.session_file_path = path[:-4]
shutil.copy(path, self.session_file_path)
if fest_file_path.find(ext) == len(fest_file_path) - len(ext):
self.fest_file_path = fest_file_path[:-4]
shutil.copy(fest_file_path, self.fest_file_path)
else:
ext = '.fest'
self.session_file_path = path if path.endswith(ext) else path + ext
self.fest_file_path = fest_file_path if fest_file_path.endswith(ext) else fest_file_path + ext

if e.Id == wx.ID_SAVE:
# For normal path translation .fest file must exist
f = open(self.fest_file_path, 'w', encoding='utf-8')
f.close()

self.ui_to_config()
json.dump(self.config, open(self.session_file_path, 'w', encoding='utf-8'),
json.dump(self.config, open(self.fest_file_path, 'w', encoding='utf-8'),
ensure_ascii=False, indent=4)

path.fest_file = self.fest_file_path
with open(Config.LAST_SESSION_PATH, 'w', encoding='utf-8-sig') as f:
f.write(path_session_try_to_relative(self.session_file_path))
f.write(path.make_rel(self.fest_file_path))

self.EndModal(e.Id)

Expand Down
2 changes: 1 addition & 1 deletion test/data
Submodule data updated from 6ee69b to d86832
Loading

0 comments on commit 53ea314

Please sign in to comment.