Pyinstaller app is accessing txt files, but not writing to them (works before app compilation)
Asked Answered
L

2

1

settings.txt is stored and accessed within the compiled single file app, but it's not being written to. This works prior to Pyinstaller compilation when the file is in the same directory as the script.

The app is compiled from the terminal:

pyinstaller script.spec script.py --windowed --onefile

a.datas is set in the spec file as:

a.datas += [(‘settings.txt’,’/path/to/settings.txt’, "DATA”)]

and the file is read properly within the app:

with open(resource_path('settings.txt'), 'r') as f2

However, the file isn’t updated when attempting to overwrite the file:

def OnExit(self, event):
    with open(resource_path('settings.txt'), 'w') as f2:
        f2.write('update')

    self.Destroy()

resource_path is defined as:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.environ.get("_MEIPASS2", os.path.abspath("."))

    return os.path.join(base_path, relative_path)
Lundt answered 10/12, 2016 at 7:14 Comment(7)
f2.write('update') you lose data if not closing file. Need f2.close() before exiting app.Helbonnas
I believe that the close() method is automatically invoked when using withLundt
No if your setting file is empty !Helbonnas
Another point how you know "all has been done" on exit ? Need validate everythings before exiting.Helbonnas
its writing them ... but it does not rebundle your application so the next time you run it it will expand your bundle with the original app ... try saving your settings file in someplace like os.path.expanduser("~/.usersettings")Chronometer
Sorry if obtuse here. It appears what you said implies that settings can't be saved within the app itself (I understand if that's the case, that a settings directory would need to be made on the user's machine). If I'm mistaken, I'll post my updated spec and script code.Lundt
Yes...recalled the reason for the dot before the folder nameLundt
T
3

If you are on Windows, _MEIPASS returns the "short" name for the path in case that any component of it is more than 8 characters long. So, to test that this is the issue, try to make it a one-folder frozen app and then move it in a simple and short path: e.g., C:/test.

If this is the issue, you can workaround the problem by retrieving the long path using something like:

if hasattr(sys, '_MEIPASS'):
    import win32api
    sys_meipass = win32api.GetLongPathName(sys._MEIPASS)
Tinct answered 11/12, 2016 at 15:55 Comment(0)
T
0

I wanted to share my solution, which simultaneously addresses many issues with relative paths in general (see function __doc__string).

I have a module named top_level_locator.py, with a modified function module_path as seen in other answers which takes a relative_path.

usage in other .py files:

from top_level_locator import module_path
resource_location = module_path(relative_path = 'resource.ext')

import sys
from pathlib import Path
from inspect import getsourcefile

def module_path(relative_path):
    """
    Combine top level path location, in this project app.py folder because it serves as main/entry_point, with user relative path.

    NOTE: top_level_locator.py should be in same folder as entry_point.py(/main.py) script 
    - TEST this with executable 
    - TEST this without executable 

    NOTE: care with use of __file__ as it comes with unwarranted side effects when:
    - running from IDLE (Python shell), no __file__ attribute
    - freezers, e.g. py2exe & pyinstaller do not have __file__ attribute! 

    NOTE: care with use of sys.argv[0]
    - unexpected result when you want current module path and get path where script/executable was run from! 

    NOTE: care with use of sys.executable
    - if non-frozen application/module/script: python/path/python.exe 
    - else                                   : standalone_application_executable_name.exe
    """
    # 0 if this module next to your_entry_point.py (main.py) else += 1 for every directory deeper
    n_deep = 1

    print('sys.executable:', sys.executable)
    print('   sys.argv[0]:', Path(sys.argv[0]).parents[n_deep].absolute() / sys.argv[0])
    print('      __file__:', __file__)
    print(' getsourcefile:', Path(getsourcefile(lambda:0)).parents[n_deep].absolute())
    
    if hasattr(sys, "frozen"):
        # retreive possible longpath if needed from _MEIPASS: import win32api; 
        # sys_meipass = win32api.GetLongPathName(sys._MEIPASS)
        base_path = getattr(sys, '_MEIPASS', Path(sys.executable).parent)
        print('      _MEIPASS:', base_path)
        return Path(base_path).joinpath(relative_path)
    return Path(getsourcefile(lambda:0)).parents[n_deep].absolute().joinpath(relative_path)

if __name__ == '__main__':
    module_path()

In non-frozen applications the output will (should) be as such:

  • sys.executable: C:\Users\<usr_name>\AppData\Local\Programs\Python\Python37\python.exe
  • sys.argv[0]: c:\Users\<usr_name>\Desktop\<project_name>\<project_code_folder>\app.py
  • __file__: c:\Users\<usr_name>\Desktop\<project_name>\<project_code_folder>\utils\top_level_locator.py
  • getsourcefile: c:\Users\<usr_name>\Desktop\<project_name>\<project_code_folder>

In frozen applications:

  • sys.executable: C:\Users\<usr_name>\Desktop\<project_name>\dist\app.exe
  • sys.argv[0]: C:\Users\<usr_name>\Desktop\<project_name>\dist\app.exe
  • __file__: C:\Users\<usr_name>\AppData\Local\Temp\_MEI155562\utils\top_level_locator.pyc
  • getsourcefile: C:\Users\<usr_name>\Desktop\<project_name>
  • _MEIPASS: C:\Users\<usr_name>\AppData\Local\Temp\_MEI155562
Toscana answered 21/12, 2020 at 15:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.