Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On Mac, how to create a drag & drop app for a python script where the script name rather than Python shows in the MenuBar (etc.)?

I have a Python script that I would like to start either by clicking on something or by dropping a file I want to open on that thing. I also want to avoid the annoying mac Python issue that the name of the interpreter (Python) rather than the name of the script. I don't want to use py2app, since I don't want to bundle Python and want the python files to remain as text files. I'd like a pure Python (or at best shell script solution.) This shows how to do everything, but the drag and drop part; this shows how to do the same by creating a bundle, which allows one to control the icon.

like image 726
bht Avatar asked Jul 05 '13 01:07

bht


People also ask

How do I turn on drag and drop on a Mac?

Select Apple > System Preferences > Accessibility > Pointer Control > Trackpad Options > Enable dragging and then select three finger drag. It will let you drag and drop on your Mac by tapping three fingers at once on the trackpad. Some people find this easier than the single-finger default.


2 Answers

I have an answer to my question, but I am hoping that someone can come up with something less complex. The solution I came up with was a Python script to create an AppleScript (ugh!), compile it into a bundle and then modify the bundle to change the allowed file extension(s), the icon and so on.

I did not find a way to get drag-and-drop working when starting a wxpython script, without an AppleScript (or using py2app) and I could not get the AppleScript to work in a bundle I created, I had to use osacompile to create the bundle for me.

I like better the approach here or here, where one creates the plist file "by hand" rather than than modify an externally created one as I do here, but the script below does do the job.

#!/usr/bin/env python
'''This script creates an AppleScript app to launch a python script.
The app is created in the current working directory. A softlink is
made to an app version of the current python (with name
.../Resources/Python.app/Contents/MacOS/Python and found in the
directory tree of calling python interpreter), but with the name of
the app to be created, this means that the app name shows up in the
Mac menus.

Run this script with one, two or three arguments:
    <python script>
    <project name>
    <icon file>
The python script path may be specified relative to the current path
or given an absolute path, but will be accessed via an absolute path
in the AppleScript.

If the project name is not specified, it will be taken from the root
name of the script.

If the icon file is not specified, it will be assumed to be the same
as the python script, but with the extension changed from .py to
.icns.  (Note that image files can be saved in ICNS format in
Preview.). No error occurs if the icon file does not exist.
'''
import sys, os, os.path, stat, shutil, subprocess, plistlib
allowedfiletypes = ['txt',]
allowedtypedesc = 'Text Files'
scripttype = 'Editor' # use 'Viewer' if the script does not change the file

def Usage():
    print("\n\tUsage: python "+sys.argv[0]+" <python script> [<project name>] [<icon file>]\n")
    sys.exit()

if not 2 <= len(sys.argv) <= 4:
    Usage()

script = os.path.abspath(sys.argv[1])
if not os.path.exists(script):
    print("\nFile "+script+" not found")
    Usage()
if os.path.splitext(script)[1].lower() != '.py':
    print("\nScript "+script+" does not have extension .py")
    Usage()
# make sure we found it
if not os.path.exists(script):
    print("\nFile "+script+" not found")
    Usage()

if len(sys.argv) >= 3:
    project = sys.argv[2]
else:
    project = os.path.splitext(os.path.split(script)[1])[0]

if len(sys.argv) == 4:
    iconfile = sys.argv[3]
else:
    iconfile = os.path.splitext(script)[0]+'.icns'

# app will be created in current working directory 
apppath = os.path.abspath(os.path.join('.',project+".app")) # full path to app bundle

# find the python application; which must be an OS X app
pythonpath,top = os.path.split(os.path.realpath(sys.executable))
while top:
    if 'Resources' in pythonpath:
        pass
    elif os.path.exists(os.path.join(pythonpath,'Resources')):
        break
    pythonpath,top = os.path.split(pythonpath)
else:
    print("\nSorry, failed to find a Resources directory associated with "+str(sys.executable))
    sys.exit()
pythonapp = os.path.join(pythonpath,'Resources','Python.app','Contents','MacOS','Python')
if not os.path.exists(pythonapp): 
    print("\nSorry, failed to find a Python app in "+str(pythonapp))
    sys.exit()

# new name to call python
newpython =  os.path.join(apppath,"Contents","MacOS",project)

if os.path.exists(apppath): # cleanup
    print("\nRemoving old "+project+" app ("+str(apppath)+")")
    shutil.rmtree(apppath)

# create an AppleScript that launches python with the requested app
shell = os.path.join("/tmp/","appscrpt.script")
f = open(shell, "w")
f.write('''(*   drag and drop AppleScript 
     It can launch a python script by double clicking or by dropping a data file
     over the app. It runs the script in a terminal window.
*)

(* test if a file is present and exit with an error message if it is not  *)
on TestFilePresent(appwithpath)
        tell application "System Events"
                if (file appwithpath exists) then
                else
                        display dialog "Error: file " & appwithpath & " not found. If you have moved this file, recreate the AppleScript." with icon caution buttons {{"Quit"}}
                        return
                end if
        end tell
end TestFilePresent

(* 
------------------------------------------------------------------------
this section responds to a double-click. No file is supplied 
------------------------------------------------------------------------ 
*)
on run
        set python to "{:s}"
        set appwithpath to "{:s}"
        TestFilePresent(appwithpath)
        TestFilePresent(python)
        tell application "Terminal"
                activate
                do script python & " " & appwithpath & "; exit"
        end tell
end run

(*
-----------------------------------------------------------------------------------------------
this section handles starting with files dragged into the AppleScript
 o it goes through the list of file(s) dragged in
 o then it converts the colon-delimited macintosh file location to a POSIX filename
 o for every non-directory file dragged into the icon, it starts GSAS-II, passing the file name
------------------------------------------------------------------------------------------------
*)

on open names
        set python to "{:s}"
        set appwithpath to "{:s}"
        TestFilePresent(appwithpath)
        repeat with filename in names
                set filestr to (filename as string)
                if filestr ends with ":" then
                        (* should not happen, skip directories *)
                else
                        (* if this is an input file, open it *)
                        set filename to the quoted form of the POSIX path of filename
                        tell application "Terminal"
                                activate
                                do script python & " " & appwithpath & " " & filename & "; exit"
                        end tell
                end if
        end repeat
end open
'''.format(newpython,script,newpython,script))
f.close()

try: 
    subprocess.check_output(["osacompile","-o",apppath,shell],stderr=subprocess.STDOUT)
except subprocess.CalledProcessError, msg:
    print('Error compiling AppleScript:')
    print msg.output
    sys.exit()

# create a link to the python app, but named to match the project
os.symlink(pythonapp,newpython)

# change the icon
oldicon = os.path.join(apppath,"Contents","Resources","droplet.icns")
if os.path.exists(iconfile) and os.path.exists(oldicon):
    shutil.copyfile(iconfile,oldicon)

# Edit the app plist file to restrict the type of files that can be dropped
d = plistlib.readPlist(os.path.join(apppath,"Contents",'Info.plist'))
d['CFBundleDocumentTypes'] = [{
    'CFBundleTypeExtensions': allowedfiletypes,
    'CFBundleTypeName': allowedtypedesc,
    'CFBundleTypeRole': scripttype}]
plistlib.writePlist(d,os.path.join(apppath,"Contents",'Info.plist'))

print("\nCreated "+project+" app ("+str(apppath)+
      ").\nViewing app in Finder so you can drag it to the dock if, you wish.")
subprocess.call(["open","-R",apppath])

Does anyone have a better way to do this without use of py2app?

like image 149
bht Avatar answered Sep 29 '22 14:09

bht


DnD of files onto the application's icon can be handled by overriding the MacOpenFiles method of your wx.App object.

There is a script distributed in the Python library in lib/python2.7/plat-mac called bundlebuilder.py that can be used to help you build an application bundle. It is from the early days of OSX and is probably out of date in some ways, but it worked the last time I tried it.

like image 38
RobinDunn Avatar answered Sep 29 '22 15:09

RobinDunn