I'm trying to run my python script against specific folder when I specify the folder name in the terminal e.g
python script.py -'folder'
python script.py 'folder2'
'folder' being the I folder I would like to run the script in. Is there a command line switch that I must use?
The cd command in the shell switches your current directory.
Perhaps see also What exactly is current working directory?
If you would like your Python script to accept a directory argument, you'll have to implement the command-line processing yourself. In its simplest form, it might look something like
import sys
if len(sys.argv) == 1:
mydir = '.'
else:
mydir = sys.argv[1]
do_things_with(mydir)
Usually, you would probably wrap this in if __name__ == '__main__': etc and maybe accept more than one directory and loop over the arguments?
import sys
from os import scandir
def how_many_files(dirs):
"""
Show the number of files in each directory in dirs
"""
for adir in dirs:
try:
files = list(scandir(adir))
except (PermissionError, FileNotFoundError) as exc:
print('%s: %s' % (adir, exc), file=sys.stderr)
continue
print('%s: %i directory entries' % (adir, len(files)))
if __name__ == '__main__':
how_many_files(sys.argv[1:] or ['.'])
Related
I found the answer
So it looks like PyInstaller actually runs in a temp directory, not your own, which explains my issue. This is an explanation for that. I guess I will keep this up incase people in the future have problems.
Original question
I am trying to use PyInstaller to create an executable of a simple python script called test.py that just creates a text file and adds numbers to it, as a test of PyInstaller. This file works correctly when run normally.
from os.path import dirname, abspath
def main():
txtfile = dirname(abspath(__file__)) + '/text.txt'
with open(txtfile, 'w') as f:
for num in range(101):
f.write(str(num) + '\n')
if __name__ == '__main__':
main()
print('script executed')
When I use:
pyinstaller test.py --onefile in the same directory as test.py it successfully creates the dist file with the binary file test inside of it.
when I cd dist and do ./test to execute the file inside dist it successfully prints out main called and script executed but it doesn't actually create the file. So, main is being called and the script is being executed, but the file isn't created at all, and I am quite confused about what I'm doing wrong..I must be getting file paths messed up? But I have specified the exact full path with os.path, so it doesn't make sense to me.
The system exit code is 0, and there are no errors raised when I call ./test
I found this that shows that PyInstaller will save to a temp file. I created this script below to check if the script is being executed directly or via PyInstaller.
import os
import sys
def create_file(path):
with open(path + '/test.txt', 'w') as f:
for num in range(101):
f.write(str(num) + '\n')
def check_using_pyinstaller():
if getattr(sys, 'frozen', False):
application_path = os.path.dirname(sys.executable)
return application_path
return os.path.dirname(os.path.abspath(__file__))
def main():
path = check_using_pyinstaller()
os.chdir(path)
create_file(path)
if __name__ == '__main__':
main()
I have two files, one bell.mp3 one main.py file, that plays back bell.mp3 via subprocess.
If I do:
pyinstaller main.py
the Dist file ends up correctly, and everything works fine, The program runs in a directory.
This is the code for my file which i call pyinst_tester.py
it creates a text file, and plays a bell.mp3 file
#
from con import * # this is just a configuration file that has g='play' in it.
import subprocess
f=open(r'/home/godzilla/Desktop/Pyinstaller testing/testfile1','w')
f.write('This has worked')
f.close()
file='/home/godzilla/Desktop/Pyinstaller testing/data/bell.mp3'
if 'play' == g:
subprocess.call(['/usr/bin/cvlc',file])
a single file is created, but if I delete the bell.mp3 file it doesn't work. In a single file isn't the bell.mp3 zipped inside the main.exe ? therefore, redundant as a separate file ?
What Is the point having a single file exe, if you need an adjacent file with all the mp3s inside?
Pyinstaller has many features and if you want to include non python files (for example mp3 files) you have to do so explicitly with the --add-binary switch.
In one file mode the executable will be unpacked into a temporary directory prior to execution of the python code.
So how to write your code to access these data files.
You might want to look at the pyinstaller documention at following sections:
https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#run-time-information
https://pyinstaller.readthedocs.io/en/stable/runtime-information.html#using-sys-executable-and-sys-argv-0
I personally place all my files in a separate directory. e.g. data.
If you place the file bell.mp3 in the directory data, then you had to call pyinstaller with the option --add-binary data:data
in the one file mode the executable is extracted into a temporary directory
whose path you get get from the variable sys._MEIPASS
Your data directory will bi in the sub directory data of sys._MEIPASS
In my example I create a function, that will be able to locate the data files in normal python mode and in pyinstaller one file or one directory mode.
Just try it out it should be self explaining
simple example:
minitst.py
import os, sys
import time
is_frozen = getattr(sys, "frozen", False)
MYDIR = os.path.realpath(os.path.dirname(__file__))
def data_fname(fname):
if is_frozen:
return os.path.join(sys._MEIPASS, "data", fname)
else:
return os.path.join(MYDIR, "data", fname)
def main():
print("This application is %s frozen" %
("" if is_frozen else "not"))
print("executable =", sys.executable,
"File =", __file__,
"mydir =", MYDIR)
if is_frozen:
print("MEIPASS", sys._MEIPASS)
fname = data_fname("tst.txt")
print("will open", fname)
with open(fname) as fin:
print(fin.read())
time.sleep(5) # this shall allow to view the console e.g. on windows if clicking on the executable.
if __name__ == "__main__":
main()
now create a directory data and place a file "tst.txt"
data/tst.txt
Hello world
Now call
pyinstaller -F minitst.py --add-binary data:data -c
and call dist/minitst from a console.
The output should look like:
This application is frozen
executable = /home/gelonida/so/pyinst/dist/minitst File = minitst.py mydir = /home/gelonida/so/pyinst
MEIPASS /tmp/_MEIKGqah9
will open /tmp/_MEIKGqah9/data/tst.txt
Hello
Now concerning your code.
I compacted the code to determine the datadir a little, but it is the same logic as in the upper example
import os, sys
from con import * # this is just a configuration file that has g='play' in it.
import subprocess
basedir = getattr(sys, "_MEIPASS", os.path.realpath(os.path.dirname(__file__)))
f=open('testfile1','w')
f.write('This has worked')
f.close()
file=os.path.join(basedir, 'data/bell.mp3')
if 'play' == g:
subprocess.call(['/usr/bin/cvlc',file])
I'm writing a python script that will be aliased and run from various directories like this:
# working
python myScript.py file.txt
or:
# not working
python script/myScript.py other_file.txt
I'm referencing the file input like this:
file = sys.argv[1]
How can I have the script look for the file based on the command line users location instead of relative to the script's location?
Try this:
import os
print(os.getcwd())
This will give you the current working directory(cwd). And using other functions like os.path.join, you can achieve what you want.
Full example:
import os
import sys
def main():
if len(sys.argv) < 2:
print('Not enough arguments.')
sys.exit(1)
print('Current working directory: %s' % os.getcwd())
print('What you want: %s' % os.path.join(os.getcwd(), sys.argv[1]))
if __name__ == '__main__':
main()
Try using it.
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)
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)
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
As an newcomer to python I figured I'd write a little python3 script to help me switch directories on the command line (ubuntu trusty). Unfortunately os.chdir() does not seems to work.
I've tried tinkering with it in various ways such as placing quotes around the path, removing the leading slash (which obviously doesn't work) and even just hardcoding it, but I can't get it to work - can anybody tell me what I'm missing here?
The call to chdir() happens towards the end - you can see the code in github too
#!/usr/bin/env python3
# #python3
# #author sabot <sabot#inuits.eu>
"""Switch directories without wearing out your slash key"""
import sys
import os
import json
import click
__VERSION__ = '0.0.1'
# 3 params are needed for click callback
def show_version(ctx, param, value):
"""Print version information and exit."""
if not value:
return
click.echo('Goto %s' % __VERSION__)
ctx.exit() # quit the program
def add_entry(dictionary, filepath, path, alias):
"""Add a new path alias."""
print("Adding alias {} for path {} ".format(alias,path))
dictionary[alias] = path
try:
jsondata = json.dumps(dictionary, sort_keys=True)
fd = open(filepath, 'w')
fd.write(jsondata)
fd.close()
except Exception as e:
print('Error writing to dictionary file: ', str(e))
pass
def get_entries(filename):
"""Get the alias entries in json."""
returndata = {}
if os.path.exists(filename) and os.path.getsize(filename) > 0:
try:
fd = open(filename, 'r')
entries = fd.read()
fd.close()
returndata = json.loads(entries)
except Exception as e:
print('Error reading dictionary file: ', str(e))
pass
else:
print('Dictionary file not found or empty- spawning new one in', filename)
newfile = open(filename,'w')
newfile.write('')
newfile.close()
return returndata
#click.command()
#click.option('--version', '-v', is_flag=True, is_eager=True,
help='Print version information and exit.', expose_value=False,
callback=show_version)
#click.option('--add', '-a', help="Add a new path alias")
#click.option('--target', '-t', help="Alias target path instead of the current directory")
#click.argument('alias', default='currentdir')
#click.pass_context
def goto(ctx, add, alias, target):
'''Go to any directory in your filesystem'''
# load dictionary
filepath = os.path.join(os.getenv('HOME'), '.g2dict')
dictionary = get_entries(filepath)
# add a path alias to the dictionary
if add:
if target: # don't use current dir as target
if not os.path.exists(target):
print('Target path not found!')
ctx.exit()
else:
add_entry(dictionary, filepath, target, add)
else: # use current dir as target
current_dir = os.getcwd()
add_entry(dictionary, filepath, current_dir, add)
elif alias != 'currentdir':
if alias in dictionary:
entry = dictionary[alias]
print('jumping to',entry)
os.chdir(entry)
elif alias == 'hell':
print("Could not locate C:\Documents and settings")
else:
print("Alias not found in dictionary - did you forget to add it?")
if __name__ == '__main__':
goto()
The problem is not with Python, the problem is that what you're trying to do is impossible.
When you start a Python interpreter (script or interactive REPL), you do so from your "shell" (Bash etc.). The shell has some working directory, and it launches Python in the same one. When Python changes its own working directory, it does not affect the parent shell, nor do changes in the working directory of the shell affect Python after it has started.
If you want to write a program which changes the directory in your shell, you should define a function in your shell itself. That function could invoke Python to determine the directory to change to, e.g. the shell function could be simply cd $(~/myscript.py) if myscript.py prints the directory it wants to switch to.
Here's Python 3 version of #ephemient's C solution:
#!/usr/bin/env python3
"""Change parent working directory."""
#XXX DIRTY HACK, DO NOT DO IT
import os
import sys
from subprocess import Popen, PIPE, DEVNULL, STDOUT
gdb_cmd = 'call chdir("{dir}")\ndetach\nquit\n'.format(dir=sys.argv[1])
with Popen(["gdb", "-p", str(os.getppid()), '-q'],
stdin=PIPE, stdout=DEVNULL, stderr=STDOUT) as p:
p.communicate(os.fsencode(gdb_cmd))
sys.exit(p.wait())
Example:
# python3 cd.py /usr/lib && python3 -c 'import os; print(os.getcwd())'