source: trunk/GSASIIpath.py

Last change on this file was 5638, checked in by toby, 2 months ago

fix for IPython >8.12 (I hope)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 59.5 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIpath - file location & update routines
3########### SVN repository information ###################
4# $Date: 2023-07-28 20:55:56 +0000 (Fri, 28 Jul 2023) $
5# $Author: vondreele $
6# $Revision: 5638 $
7# $URL: trunk/GSASIIpath.py $
8# $Id: GSASIIpath.py 5638 2023-07-28 20:55:56Z vondreele $
9########### SVN repository information ###################
10'''
11:mod:`GSASIIpath` Classes & routines follow
12'''
13
14from __future__ import division, print_function
15import os
16import sys
17import platform
18import glob
19import subprocess
20try:
21    import numpy as np
22except ImportError:
23    print("skipping numpy in GSASIIpath")
24g2home = 'https://subversion.xray.aps.anl.gov/pyGSAS'
25'Define the location of the GSAS-II subversion repository'
26   
27path2GSAS2 = os.path.dirname(os.path.abspath(os.path.expanduser(__file__))) # location of this file; save before any changes in pwd
28
29# convert version numbers as '1.2.3' to integers (1002) and back (to 1.2)
30fmtver = lambda v: str(v//1000)+'.'+str(v%1000)
31intver = lambda vs: sum([int(i) for i in vs.split('.')[0:2]]*np.array((1000,1)))
32
33def GetConfigValue(key,default=None):
34    '''Return the configuration file value for key or a default value if not present
35   
36    :param str key: a value to be found in the configuration (config.py) file
37    :param default: a value to be supplied if none is in the config file or
38      the config file is not found. Defaults to None
39    :returns: the value found or the default.
40    '''
41    try:
42        return configDict.get(key,default)
43    except NameError: # this happens when building docs
44        return None
45
46def SetConfigValue(parmdict):
47    '''Set configuration variables from a dictionary where elements are lists
48    First item in list is the default value and second is the value to use.
49    '''
50    global configDict
51    for var in parmdict:
52        if var in configDict:
53            del configDict[var]
54        if isinstance(parmdict[var],tuple):
55            configDict[var] = parmdict[var]
56        else:
57            if parmdict[var][1] is None: continue
58            if parmdict[var][1] == '': continue
59            if parmdict[var][0] == parmdict[var][1]: continue
60            configDict[var] = parmdict[var][1]
61
62def addPrevGPX(fil,configDict):
63    '''Add a GPX file to the list of previous files.
64    Move previous names to start of list. Keep most recent five files
65    '''
66    fil = os.path.abspath(os.path.expanduser(fil))
67    if 'previous_GPX_files' not in configDict: return
68    try:
69        pos = configDict['previous_GPX_files'][1].index(fil) 
70        if pos == 0: return
71        configDict['previous_GPX_files'][1].pop(pos) # if present, remove if not 1st
72    except ValueError:
73        pass
74    except AttributeError:
75        configDict['previous_GPX_files'][1] = []
76    files = list(configDict['previous_GPX_files'][1])
77    files.insert(0,fil)
78    configDict['previous_GPX_files'][1] = files[:5]
79
80# routines for looking a version numbers in files
81version = -1
82def SetVersionNumber(RevString):
83    '''Set the subversion version number
84
85    :param str RevString: something like "$Revision: 5638 $"
86      that is set by subversion when the file is retrieved from subversion.
87
88    Place ``GSASIIpath.SetVersionNumber("$Revision: 5638 $")`` in every python
89    file.
90    '''
91    try:
92        RevVersion = int(RevString.split(':')[1].split()[0])
93        global version
94        version = max(version,RevVersion)
95    except:
96        pass
97       
98def GetVersionNumber():
99    '''Return the maximum version number seen in :func:`SetVersionNumber`
100    '''
101    if version > 1000:
102        return version
103    else:
104        return "unknown"
105
106def LoadConfigFile(filename):
107    '''Read a GSAS-II configuration file.
108    Comments (starting with "%") are removed, as are empty lines
109   
110    :param str filename: base file name (such as 'file.dat'). Files with this name
111      are located from the path and the contents of each are concatenated.
112    :returns: a list containing each non-empty (after removal of comments) line
113      found in every matching config file.
114    '''
115    info = []
116    for path in sys.path:
117        fil = os.path.join(path,filename)
118        if not os.path.exists(fil): continue
119        try:
120            i = 0
121            fp = open(fil,'r')
122            for line in fp:
123                expr = line.split('#')[0].strip()
124                if expr:
125                    info.append(expr)
126                    i += 1
127            print(str(i)+' lines read from config file '+fil)
128        finally:
129            fp.close()
130    return info
131
132
133# routines to interface with subversion
134proxycmds = []
135'Used to hold proxy information for subversion, set if needed in whichsvn'
136svnLocCache = None
137'Cached location of svn to avoid multiple searches for it'
138
139def MakeByte2str(arg):
140    '''Convert output from subprocess pipes (bytes) to str (unicode) in Python 3.
141    In Python 2: Leaves output alone (already str).
142    Leaves stuff of other types alone (including unicode in Py2)
143    Works recursively for string-like stuff in nested loops and tuples.
144
145    typical use::
146
147        out = MakeByte2str(out)
148
149    or::
150
151        out,err = MakeByte2str(s.communicate())
152   
153    '''
154    if isinstance(arg,str): return arg
155    if isinstance(arg,bytes):
156        try:
157            return arg.decode()
158        except:
159            if GetConfigValue('debug'): print('Decode error')
160            return arg
161    if isinstance(arg,list):
162        return [MakeByte2str(i) for i in arg]
163    if isinstance(arg,tuple):
164        return tuple([MakeByte2str(i) for i in arg])
165    return arg
166               
167def getsvnProxy():
168    '''Loads a proxy for subversion from the proxyinfo.txt file created
169    by bootstrap.py or File => Edit Proxy...; If not found, then the
170    standard http_proxy and https_proxy environment variables are scanned
171    (see https://docs.python.org/3/library/urllib.request.html#urllib.request.getproxies)
172    with case ignored and that is used.
173    '''
174    global proxycmds
175    proxycmds = []
176    proxyinfo = os.path.join(os.path.expanduser('~/.G2local/'),"proxyinfo.txt")
177    if not os.path.exists(proxyinfo):
178        proxyinfo = os.path.join(path2GSAS2,"proxyinfo.txt")
179    if os.path.exists(proxyinfo):
180        fp = open(proxyinfo,'r')
181        host = fp.readline().strip()
182        # allow file to begin with comments
183        while host.startswith('#'):
184            host = fp.readline().strip()
185        port = fp.readline().strip()
186        etc = []
187        line = fp.readline()
188        while line:
189            etc.append(line.strip())
190            line = fp.readline()
191        fp.close()
192        setsvnProxy(host,port,etc)
193        return host,port,etc
194    import urllib.request
195    proxdict = urllib.request.getproxies()
196    varlist = ("https","http")
197    for var in proxdict:
198        if var.lower() in varlist:
199            proxy = proxdict[var]
200            pl = proxy.split(':')
201            if len(pl) < 2: continue
202            host = pl[1].strip('/')
203            port = ''
204            if len(pl) == 3:
205                port = pl[2].strip('/').strip()
206            return host,port,''
207    return '','',''
208
209def setsvnProxy(host,port,etc=[]):
210    '''Sets the svn commands needed to use a proxy
211    '''
212    global proxycmds
213    proxycmds = []
214    host = host.strip()
215    port = port.strip()
216    if host: 
217        proxycmds.append('--config-option')
218        proxycmds.append('servers:global:http-proxy-host='+host)
219        if port:
220            proxycmds.append('--config-option')
221            proxycmds.append('servers:global:http-proxy-port='+port)
222    for item in etc:
223        proxycmds.append(item)
224       
225def whichsvn():
226    '''Returns a path to the subversion exe file, if any is found.
227    Searches the current path after adding likely places where GSAS-II
228    might install svn.
229
230    :returns: None if svn is not found or an absolute path to the subversion
231      executable file.
232    '''
233    # use a previosuly cached svn location
234    global svnLocCache
235    if svnLocCache: return svnLocCache
236    # prepare to find svn
237    is_exe = lambda fpath: os.path.isfile(fpath) and os.access(fpath, os.X_OK)
238    svnprog = 'svn'
239    if sys.platform.startswith('win'): svnprog += '.exe'
240    host,port,etc = getsvnProxy()
241    if GetConfigValue('debug') and host:
242        print('DBG_Using proxy host {} port {}'.format(host,port))
243    if GetConfigValue('svn_exec'):
244        exe_file = GetConfigValue('svn_exec')
245        print('Using ',exe_file)
246        if is_exe(exe_file):
247            try:
248                p = subprocess.Popen([exe_file,'help'],stdout=subprocess.PIPE)
249                res = p.stdout.read()
250                if not res: return
251                p.communicate()
252                svnLocCache = os.path.abspath(exe_file)
253                return svnLocCache
254            except:
255                pass
256    # add likely places to find subversion when installed with GSAS-II
257    pathlist = os.environ["PATH"].split(os.pathsep)
258    pathlist.insert(0,os.path.split(sys.executable)[0])
259    pathlist.insert(1,path2GSAS2)
260    for rpt in ('..','bin'),('..','Library','bin'),('svn','bin'),('svn',),('.'):
261        pt = os.path.normpath(os.path.join(path2GSAS2,*rpt))
262        if os.path.exists(pt):
263            pathlist.insert(0,pt)   
264    # search path for svn or svn.exe
265    for path in pathlist:
266        exe_file = os.path.join(path, svnprog)
267        if is_exe(exe_file):
268            try:
269                p = subprocess.Popen([exe_file,'help'],stdout=subprocess.PIPE)
270                res = p.stdout.read()
271                if not res: return
272                p.communicate()
273                svnLocCache = os.path.abspath(exe_file)
274                return svnLocCache
275            except:
276                pass       
277    svnLocCache = None
278
279def svnVersion(svn=None):
280    '''Get the version number of the current subversion executable
281
282    :returns: a string with a version number such as "1.6.6" or None if
283      subversion is not found.
284
285    '''
286    if not svn: svn = whichsvn()
287    if not svn: return
288
289    cmd = [svn,'--version','--quiet']
290    s = subprocess.Popen(cmd,
291                         stdout=subprocess.PIPE,stderr=subprocess.PIPE)
292    out,err = MakeByte2str(s.communicate())
293    if err:
294        print ('subversion error!\nout=%s'%out)
295        print ('err=%s'%err)
296        s = '\nsvn command:  '
297        for i in cmd: s += i + ' '
298        print(s)
299        return None
300    return out.strip()
301
302def svnVersionNumber(svn=None):
303    '''Get the version number of the current subversion executable
304
305    :returns: a fractional version number such as 1.6 or None if
306      subversion is not found.
307
308    '''
309    ver = svnVersion(svn)
310    if not ver: return 
311    M,m = ver.split('.')[:2]
312    return int(M)+int(m)/10.
313
314def svnGetLog(fpath=os.path.split(__file__)[0],version=None):
315    '''Get the revision log information for a specific version of the specified package
316
317    :param str fpath: path to repository dictionary, defaults to directory where
318       the current file is located.
319    :param int version: the version number to be looked up or None (default)
320       for the latest version.
321
322    :returns: a dictionary with keys (one hopes) 'author', 'date', 'msg', and 'revision'
323
324    '''
325    import xml.etree.ElementTree as ET
326    svn = whichsvn()
327    if not svn: return
328    if version is not None:
329        vstr = '-r'+str(version)
330    else:
331        vstr = '-rHEAD'
332
333    cmd = [svn,'log',fpath,'--xml',vstr]
334    if proxycmds: cmd += proxycmds
335    s = subprocess.Popen(cmd,
336                         stdout=subprocess.PIPE,stderr=subprocess.PIPE)
337    out,err = MakeByte2str(s.communicate())
338    if err:
339        print ('out=%s'%out)
340        print ('err=%s'%err)
341        s = '\nsvn command:  '
342        for i in cmd: s += i + ' '
343        print(s)
344        return None
345    x = ET.fromstring(out)
346    d = {}
347    for i in x.iter('logentry'):
348        d = {'revision':i.attrib.get('revision','?')}
349        for j in i:
350            d[j.tag] = j.text
351        break # only need the first
352    return d
353
354svnLastError = ''
355def svnGetRev(fpath=os.path.split(__file__)[0],local=True):
356    '''Obtain the version number for the either the last update of the local version
357    or contacts the subversion server to get the latest update version (# of Head).
358
359    :param str fpath: path to repository dictionary, defaults to directory where
360       the current file is located
361    :param bool local: determines the type of version number, where
362       True (default): returns the latest installed update
363       False: returns the version number of Head on the server
364
365    :Returns: the version number as an str or
366       None if there is a subversion error (likely because the path is
367       not a repository or svn is not found). The error message is placed in
368       global variable svnLastError
369    '''
370
371    import xml.etree.ElementTree as ET
372    svn = whichsvn()
373    if not svn: return
374    if local:
375        cmd = [svn,'info',fpath,'--xml']
376    else:
377        cmd = [svn,'info',fpath,'--xml','-rHEAD']
378    if svnVersionNumber() >= 1.6:
379        cmd += ['--non-interactive', '--trust-server-cert']
380    if proxycmds: cmd += proxycmds
381    # if GetConfigValue('debug'):
382    #     s = 'subversion command:\n  '
383    #     for i in cmd: s += i + ' '
384    #     print(s)
385    s = subprocess.Popen(cmd, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
386    out,err = MakeByte2str(s.communicate())
387    if err:
388        print ('svn failed\n%s'%out)
389        print ('err=%s'%err)
390        s = '\nsvn command:  '
391        for i in cmd: s += i + ' '
392        print(s)
393        global svnLastError
394        svnLastError = err
395        return None
396    x = ET.fromstring(out)
397    for i in x.iter('entry'):
398        rev = i.attrib.get('revision')
399        if rev: return rev
400
401def svnFindLocalChanges(fpath=os.path.split(__file__)[0]):
402    '''Returns a list of files that were changed locally. If no files are changed,
403       the list has length 0
404
405    :param fpath: path to repository dictionary, defaults to directory where
406       the current file is located
407
408    :returns: None if there is a subversion error (likely because the path is
409       not a repository or svn is not found)
410
411    '''
412    import xml.etree.ElementTree as ET
413    svn = whichsvn()
414    if not svn: return
415    cmd = [svn,'status',fpath,'--xml']
416    if proxycmds: cmd += proxycmds
417    s = subprocess.Popen(cmd,
418                         stdout=subprocess.PIPE,stderr=subprocess.PIPE)
419    out,err = MakeByte2str(s.communicate())
420    if err: return None
421    x = ET.fromstring(out)
422    changed = []
423    for i in x.iter('entry'):
424        if i.find('wc-status').attrib.get('item') == 'modified': 
425            changed.append(i.attrib.get('path'))
426    return changed
427
428def svnCleanup(fpath=os.path.split(__file__)[0],verbose=True):
429    '''This runs svn cleanup on a selected local directory.
430
431    :param str fpath: path to repository dictionary, defaults to directory where
432       the current file is located
433    '''
434    svn = whichsvn()
435    if not svn: return
436    if verbose: print(u"Performing svn cleanup at "+fpath)
437    cmd = [svn,'cleanup',fpath]
438    if verbose: showsvncmd(cmd)       
439    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
440    out,err = MakeByte2str(s.communicate())
441    if err:
442        print(60*"=")
443        print("****** An error was noted, see below *********")
444        print(60*"=")
445        print(err)
446        s = '\nsvn command:  '
447        for i in cmd: s += i + ' '
448        print(s)
449        #raise Exception('svn cleanup failed')
450        return False
451    elif verbose:
452        print(out)
453    return True
454       
455def svnUpdateDir(fpath=os.path.split(__file__)[0],version=None,verbose=True):
456    '''This performs an update of the files in a local directory from a server.
457
458    :param str fpath: path to repository dictionary, defaults to directory where
459       the current file is located
460    :param version: the number of the version to be loaded. Used only
461       cast as a string, but should be an integer or something that corresponds to a
462       string representation of an integer value when cast. A value of None (default)
463       causes the latest version on the server to be used.
464    '''
465    svn = whichsvn()
466    if not svn: return
467    if version:
468        verstr = '-r' + str(version)
469    else:
470        verstr = '-rHEAD'
471    if verbose: print(u"Updating files at "+fpath)
472    cmd = [svn,'update',fpath,verstr,
473           '--non-interactive',
474           '--accept','theirs-conflict','--force']
475    if svnVersionNumber() >= 1.6:
476        cmd += ['--trust-server-cert']
477    if proxycmds: cmd += proxycmds
478    #if verbose or GetConfigValue('debug'):
479    if verbose:
480        s = 'subversion command:\n  '
481        for i in cmd: s += i + ' '
482        print(s)
483    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
484    out,err = MakeByte2str(s.communicate())
485    if err:
486        print(60*"=")
487        print("****** An error was noted, see below *********")
488        print(60*"=")
489        print(err)
490        s = '\nsvn command:  '
491        for i in cmd: s += i + ' '
492        print(s)
493        if svnCleanup(fpath):
494            s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
495            out,err = MakeByte2str(s.communicate())
496            if err:
497                print(60*"=")
498                print("****** Drat, failed again: *********")
499                print(60*"=")
500                print(err)
501            else:
502                return
503        if 'Checksum' in err:  # deal with Checksum problem
504            err = svnChecksumPatch(svn,fpath,verstr)
505            if err:
506                print('error from svnChecksumPatch\n\t',err)
507            else:
508                return
509        raise Exception('svn update failed')
510    elif verbose:
511        print(out)
512
513def showsvncmd(cmd):
514    s = '\nsvn command:  '
515    for i in cmd: s += i + ' '
516    print(s)
517
518def svnChecksumPatch(svn,fpath,verstr):
519    '''This performs a fix when svn cannot finish an update because of
520    a Checksum mismatch error. This seems to be happening on OS X for
521    unclear reasons.
522    '''
523    print('\nAttempting patch for svn Checksum mismatch error\n')
524    svnCleanup(fpath)
525    cmd = [svn,'update','--set-depth','empty',
526               os.path.join(fpath,'bindist')]
527    showsvncmd(cmd)       
528    if svnVersionNumber() >= 1.6:
529        cmd += ['--non-interactive', '--trust-server-cert']
530    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
531    out,err = MakeByte2str(s.communicate())
532    #if err: print('error=',err)
533    cmd = [svn,'switch',g2home+'/trunk/bindist',
534               os.path.join(fpath,'bindist'),
535               '--non-interactive', '--trust-server-cert', '--accept',
536               'theirs-conflict', '--force', '-rHEAD', '--ignore-ancestry']
537    showsvncmd(cmd)       
538    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
539    out,err = MakeByte2str(s.communicate())
540    DownloadG2Binaries(g2home,verbose=True)
541    cmd = [svn,'update','--set-depth','infinity',
542               os.path.join(fpath,'bindist')]
543    if svnVersionNumber() >= 1.6:
544        cmd += ['--non-interactive', '--trust-server-cert']
545    showsvncmd(cmd)       
546    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
547    out,err = MakeByte2str(s.communicate())
548    #if err: print('error=',err)
549    cmd = [svn,'update',fpath,verstr,
550                       '--non-interactive',
551                       '--accept','theirs-conflict','--force']
552    if svnVersionNumber() >= 1.6:
553        cmd += ['--trust-server-cert']
554    if proxycmds: cmd += proxycmds
555    showsvncmd(cmd)       
556    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
557    out,err = MakeByte2str(s.communicate())
558    #if err: print('error=',err)
559    return err
560       
561def svnUpgrade(fpath=os.path.split(__file__)[0]):
562    '''This reformats subversion files, which may be needed if an upgrade of subversion is
563    done.
564
565    :param str fpath: path to repository dictionary, defaults to directory where
566       the current file is located
567    '''
568    svn = whichsvn()
569    if not svn: return
570    cmd = [svn,'upgrade',fpath,'--non-interactive']
571    if proxycmds: cmd += proxycmds
572    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
573    out,err = MakeByte2str(s.communicate())
574    if err:
575        print("svn upgrade did not happen (this is probably OK). Messages:")
576        print (err)
577        s = '\nsvn command:  '
578        for i in cmd: s += i + ' '
579        print(s)
580
581def svnUpdateProcess(version=None,projectfile=None,branch=None):
582    '''perform an update of GSAS-II in a separate python process'''
583    if not projectfile:
584        projectfile = ''
585    else:
586        projectfile = os.path.realpath(projectfile)
587        print ('restart using %s'%projectfile)
588    if branch:
589        version = branch
590    elif not version:
591        version = ''
592    else:
593        version = str(version)
594    # start the upgrade in a separate interpreter (avoids loading .pyd files)
595    ex = sys.executable
596    if sys.platform == "darwin": # mac requires pythonw which is not always reported as sys.executable
597        if os.path.exists(ex+'w'): ex += 'w'
598    proc = subprocess.Popen([ex,__file__,projectfile,version])
599    if sys.platform != "win32":
600        proc.wait()
601    sys.exit()
602
603def svnSwitchDir(rpath,filename,baseURL,loadpath=None,verbose=True):
604    '''This performs a switch command to move files between subversion trees.
605    Note that if the files were previously downloaded,
606    the switch command will update the files to the newest version.
607   
608    :param str rpath: path to locate files, relative to the GSAS-II
609      installation path (defaults to path2GSAS2)
610    :param str URL: the repository URL
611    :param str loadpath: the prefix for the path, if specified. Defaults to path2GSAS2
612    :param bool verbose: if True (default) diagnostics are printed
613    '''
614    svn = whichsvn()
615    if not svn: return
616    URL = baseURL[:]
617    if baseURL[-1] != '/':
618        URL = baseURL + '/' + filename
619    else:
620        URL = baseURL + filename
621    if loadpath:
622        fpath = os.path.join(loadpath,rpath,filename)
623        svntmp = os.path.join(loadpath,'.svn','tmp')
624    else:
625        fpath = os.path.join(path2GSAS2,rpath,filename)
626        svntmp = os.path.join(path2GSAS2,'.svn','tmp')
627    # fix up problems with missing empty directories
628    if not os.path.exists(fpath):
629        print('Repairing missing directory',fpath)
630        cmd = [svn,'revert',fpath]
631        s = subprocess.Popen(cmd,stderr=subprocess.PIPE)
632        out,err = MakeByte2str(s.communicate())
633        if out: print(out)
634        if err: print(err)
635    if not os.path.exists(svntmp):
636        print('Repairing missing directory',svntmp)
637        cmd = ['mkdir',svntmp]
638        s = subprocess.Popen(cmd,stderr=subprocess.PIPE)
639        out,err = MakeByte2str(s.communicate())
640        if out: print(out)
641        if err: print(err)
642       
643    cmd = [svn,'switch',URL,fpath,
644           '--non-interactive','--trust-server-cert',
645           '--accept','theirs-conflict','--force','-rHEAD']
646    if svnVersionNumber(svn) > 1.6: cmd += ['--ignore-ancestry']
647    if proxycmds: cmd += proxycmds
648    if verbose:
649        print(u"Loading files to "+fpath+u"\n  from "+URL)
650    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
651    out,err = MakeByte2str(s.communicate())
652    if err:
653        print(60*"=")
654        print ("****** An error was noted, see below *********")
655        print(60*"=")
656        print ('out=%s'%out)
657        print ('err=%s'%err)
658        s = '\nsvn command:  '
659        for i in cmd: s += i + ' '
660        print(s)
661        if svnCleanup(fpath):
662            s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
663            out,err = MakeByte2str(s.communicate())
664            if err:
665                print(60*"=")
666                print("****** Drat, failed again: *********")
667                print(60*"=")
668                print(err)
669            else:
670                return True
671        return False
672    if verbose:
673        s = '\nsvn command:  '
674        for i in cmd: s += i + ' '
675        print(s)
676        print('\n=== Output from svn switch'+(43*'='))
677        print(out.strip())
678        print((70*'=')+'\n')
679    return True
680
681def svnInstallDir(URL,loadpath):
682    '''Load a subversion tree into a specified directory
683
684    :param str URL: the repository URL
685    :param str loadpath: path to locate files
686
687    '''
688    svn = whichsvn()
689    if not svn: return
690    cmd = [svn,'co',URL,loadpath,'--non-interactive']
691    if svnVersionNumber() >= 1.6: cmd += ['--trust-server-cert']
692    print("Loading files from "+URL)
693    if proxycmds: cmd += proxycmds
694    s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
695    out,err = MakeByte2str(s.communicate())   #this fails too easily
696    if err:
697        print(60*"=")
698        print ("****** An error was noted, see below *********")
699        print(60*"=")
700        print (err)
701        s = '\nsvn command:  '
702        for i in cmd: s += i + ' '
703        print(s)
704        if svnCleanup(loadpath):
705            s = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
706            out,err = MakeByte2str(s.communicate())
707            if err:
708                print(60*"=")
709                print("****** Drat, failed again: *********")
710                print(60*"=")
711                print(err)
712                return False
713        else:
714            return False
715    print ("Files installed at: "+loadpath)
716    return True
717
718def svnGetFileStatus(fpath=os.path.split(__file__)[0],version=None):
719    '''Compare file status to repository (svn status -u)
720
721    :returns: updatecount,modcount,locked where
722       updatecount is the number of files waiting to be updated from
723       repository
724       modcount is the number of files that have been modified locally
725       locked  is the number of files tagged as locked
726    '''
727    import xml.etree.ElementTree as ET
728    svn = whichsvn()
729    if version is not None:
730        vstr = '-r'+str(version)
731    else:
732        vstr = '-rHEAD'
733    cmd = [svn,'st',fpath,'--xml','-u',vstr]
734    if svnVersionNumber() >= 1.6:
735        cmd += ['--non-interactive', '--trust-server-cert']
736    if proxycmds: cmd += proxycmds
737    s = subprocess.Popen(cmd,
738                         stdout=subprocess.PIPE,stderr=subprocess.PIPE)
739    out,err = MakeByte2str(s.communicate())
740    if err:
741        print ('out=%s'%out)
742        print ('err=%s'%err)
743        s = '\nsvn command:  '
744        for i in cmd: s += i + ' '
745        print(s)
746        return None
747
748    locked = 0
749    updatecount = 0
750    modcount = 0
751    x = ET.fromstring(out)
752    for i0 in x.iter('entry'):
753        filename = i0.attrib.get('path','?')
754        wc_rev = ''
755        status = ''
756        switched = ''
757        for i1 in i0.iter('wc-status'):
758            wc_rev = i1.attrib.get('revision','')
759            status = i1.attrib.get('item','')
760            switched = i1.attrib.get('switched','')
761            if i1.attrib.get('wc-locked',''): locked += 1
762        if status == "unversioned": continue
763        if switched == "true": continue
764        if status == "modified":
765            modcount += 1
766        elif status == "normal":
767            updatecount += 1
768        file_rev = ''
769        for i2 in i1.iter('commit'):
770            file_rev = i2.attrib.get('revision','')
771        local_status = ''
772        for i1 in i0.iter('repos-status'):
773            local_status = i1.attrib.get('item','')
774        #print(filename,wc_rev,file_rev,status,local_status,switched)
775    return updatecount,modcount,locked
776
777def GetBinaryPrefix(pyver=None):
778    '''Creates the first part of the binary directory name
779    such as linux_64_p3.9 (where the full name will be
780    linux_64_p3.9_n1.21).
781
782    Note that any change made here is also needed in GetBinaryDir in
783    fsource/SConstruct
784    '''
785    if sys.platform == "win32":
786        prefix = 'win'
787    elif sys.platform == "darwin":
788        prefix = 'mac'
789    elif sys.platform.startswith("linux"):
790        prefix = 'linux'
791    else:
792        print(u'Unknown platform: '+sys.platform)
793        raise Exception('Unknown platform')
794    if 'arm' in platform.machine() and sys.platform == "darwin":
795        bits = 'arm'
796    elif 'aarch' in platform.machine() and '64' in platform.architecture()[0]:
797        bits = 'arm64'
798    elif 'arm' in platform.machine():
799        bits = 'arm32'
800    elif '64' in platform.architecture()[0]:
801        bits = '64'
802    else:
803        bits = '32'
804
805    # format current python version
806    if pyver:
807        pyver = 'p'+pyver
808    else:
809        pyver = 'p{}.{}'.format(*sys.version_info[0:2])
810
811    return '_'.join([prefix,bits,pyver])
812
813def svnList(URL,verbose=True):
814    '''Get a list of subdirectories from and svn repository
815    '''   
816    svn = whichsvn()
817    if not svn:
818        print('**** unable to load files: svn not found ****')
819        return ''
820    # get binaries matching the required type -- other than for the numpy version
821    cmd = [svn, 'list', URL,'--non-interactive', '--trust-server-cert']
822    if proxycmds: cmd += proxycmds
823    if verbose:
824        s = 'Running svn command:\n  '
825        for i in cmd: s += i + ' '
826        print(s)
827    p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
828    res,err = MakeByte2str(p.communicate())
829    return res
830
831def DownloadG2Binaries(g2home,verbose=True):
832    '''Download GSAS-II binaries from appropriate section of the
833    GSAS-II svn repository based on the platform, numpy and Python
834    version
835    '''   
836    bindir = GetBinaryPrefix()
837    #npver = 'n{}.{}'.format(*np.__version__.split('.')[0:2])
838    inpver = intver(np.__version__)
839    svn = whichsvn()
840    if not svn:
841        print('**** unable to load files: svn not found ****')
842        return ''
843    # get binaries matching the required type -- other than for the numpy version
844    cmd = [svn, 'list', g2home + '/Binaries/','--non-interactive', '--trust-server-cert']
845    if proxycmds: cmd += proxycmds
846    if verbose:
847        s = 'Running svn command:\n  '
848        for i in cmd: s += i + ' '
849        print(s)
850    p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
851    res,err = MakeByte2str(p.communicate())
852    versions = {}
853    for d in res.split():
854        if d.startswith(bindir):
855            v = intver(d.rstrip('/').split('_')[3].lstrip('n'))
856            versions[v] = d
857    intVersionsList = sorted(versions.keys())
858    if not intVersionsList:
859        print('No binaries located matching',bindir)
860        return
861    elif inpver < min(intVersionsList):
862        vsel = min(intVersionsList)
863        print('Warning: The current numpy version, {}, is older than\n\tthe oldest dist version, {}'
864              .format(np.__version__,fmtver(vsel)))
865    elif inpver >= max(intVersionsList):
866        vsel = max(intVersionsList)
867        if verbose: print(
868                'FYI: The current numpy version, {}, is newer than the newest dist version {}'
869                .format(np.__version__,fmtver(vsel)))
870    else:
871        vsel = min(intVersionsList)
872        for v in intVersionsList:
873            if v <= inpver:
874                vsel = v
875            else:
876                if verbose: print(
877                        'FYI: Selecting dist version {} as the current numpy version, {},\n\tis older than the next dist version {}'
878                        .format(fmtver(vsel),np.__version__,fmtver(v)))
879                break
880    distdir = g2home + '/Binaries/' + versions[vsel]
881    # switch reset command: distdir = g2home + '/trunk/bindist'
882    svnSwitchDir('bindist','',distdir,verbose=verbose)
883    return os.path.join(path2GSAS2,'bindist')
884
885# def svnTestBranch(loc=None):
886#     '''Returns the name of the branch directory if the installation has been switched.
887#     Returns none, if not a branch
888#     the test 2frame branch. False otherwise
889#     '''
890#     if loc is None: loc = path2GSAS2
891#     svn = whichsvn()
892#     if not svn:
893#         print('**** unable to load files: svn not found ****')
894#         return ''
895#     cmd = [svn, 'info', loc]
896#     if proxycmds: cmd += proxycmds
897#     p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
898#     res,err = MakeByte2str(p.communicate())
899#     for l in res.split('\n'):
900#         if "Relative URL:" in l: break
901#     if "/branch/" in l:
902#         return l[l.find("/branch/")+8:].strip()
903#     else:
904#         return None
905   
906def svnSwitch2branch(branch=None,loc=None,svnHome=None):
907    '''Switch to a subversion branch if specified. Switches to trunk otherwise.
908    '''
909    if svnHome is None: svnHome = g2home
910    svnURL = svnHome + '/trunk'
911    if branch:
912        if svnHome.endswith('/'):
913            svnURL = svnHome[:-1]
914        else:
915            svnURL = svnHome
916        if branch.startswith('/'):
917            svnURL += branch
918        else:
919            svnURL += '/' + branch
920    svnSwitchDir('','',svnURL,loadpath=loc)
921   
922
923def IPyBreak_base(userMsg=None):
924    '''A routine that invokes an IPython session at the calling location
925    This routine is only used when debug=True is set in config.py
926    '''
927    savehook = sys.excepthook # save the exception hook
928    try: 
929        from IPython.terminal.embed import InteractiveShellEmbed
930    except ImportError:
931        try:
932            # try the IPython 0.12 approach
933            from IPython.frontend.terminal.embed import InteractiveShellEmbed
934        except ImportError:
935            print ('IPython InteractiveShellEmbed not found')
936            return
937    import inspect
938    #from IPython import __version__
939    #if __version__.startswith('8.12.'): # see https://github.com/ipython/ipython/issues/13966
940    from IPython.core import getipython
941    if getipython.get_ipython() is None:
942        ipshell = InteractiveShellEmbed.instance()
943    else:
944        ipshell = InteractiveShellEmbed()
945
946    frame = inspect.currentframe().f_back
947    msg   = 'Entering IPython console inside {0.f_code.co_filename} at line {0.f_lineno}\n'.format(frame)
948    if userMsg: msg += userMsg
949    # globals().update(locals()) # This might help with vars inside list comprehensions, etc.
950    ipshell(msg,stack_depth=2) # Go up one level, to see the calling routine
951    sys.excepthook = savehook # reset IPython's change to the exception hook
952
953try:
954    from IPython.core import ultratb
955except:
956    pass
957
958def exceptHook(*args):
959    '''A routine to be called when an exception occurs. It prints the traceback
960    with fancy formatting and then calls an IPython shell with the environment
961    of the exception location.
962   
963    This routine is only used when debug=True is set in config.py   
964    '''
965    import IPython.core
966    if sys.platform.startswith('win'):
967        IPython.core.ultratb.FormattedTB(call_pdb=False,color_scheme='NoColor')(*args)
968    else:
969        IPython.core.ultratb.FormattedTB(call_pdb=False,color_scheme='LightBG')(*args)
970
971    try: 
972        from IPython.terminal.embed import InteractiveShellEmbed
973    except ImportError:
974        try:
975            # try the IPython 0.12 approach
976            from IPython.frontend.terminal.embed import InteractiveShellEmbed
977        except ImportError:
978            print ('IPython InteractiveShellEmbed not found')
979            return
980    import inspect
981    frame = inspect.getinnerframes(args[2])[-1][0]
982    msg   = 'Entering IPython console at {0.f_code.co_filename} at line {0.f_lineno}\n'.format(frame)
983    savehook = sys.excepthook # save the exception hook
984    try: # try IPython 5 call 1st
985        class c(object): pass
986        pseudomod = c() # create something that acts like a module
987        pseudomod.__dict__ = frame.f_locals
988        InteractiveShellEmbed(banner1=msg)(module=pseudomod,global_ns=frame.f_globals)
989    except:
990        InteractiveShellEmbed(banner1=msg)(local_ns=frame.f_locals,global_ns=frame.f_globals)
991    sys.excepthook = savehook # reset IPython's change to the exception hook
992
993def DoNothing():
994    '''A routine that does nothing. This is called in place of IPyBreak and pdbBreak
995    except when the debug option is set True in config.py
996    '''
997    pass 
998
999IPyBreak = DoNothing
1000pdbBreak = DoNothing
1001def InvokeDebugOpts():
1002    'Called in GSASII.py to set up debug options'
1003    if any('SPYDER' in name for name in os.environ):
1004        print('Running from Spyder, keeping breakpoint() active & skipping exception trapping')
1005    elif GetConfigValue('debug'):
1006        try:
1007            import pdb
1008            global pdbBreak
1009            pdbBreak = pdb.set_trace
1010            import IPython
1011            global IPyBreak
1012            IPyBreak = IPyBreak_base
1013            sys.excepthook = exceptHook
1014            os.environ['PYTHONBREAKPOINT'] = 'GSASIIpath.IPyBreak_base'
1015            print ('Debug on: IPython: Exceptions and G2path.IPyBreak(); pdb: G2path.pdbBreak()')
1016        except:
1017            print ('Debug on failed. IPython not installed?')
1018    else: # not in spyder or debug enabled, hide breakpoints
1019        os.environ['PYTHONBREAKPOINT'] = '0'
1020
1021def TestSPG(fpth):
1022    '''Test if pyspg.[so,.pyd] can be run from a location in the path
1023    '''
1024    if not os.path.exists(fpth): return False
1025    if not glob.glob(os.path.join(fpth,'pyspg.*')): return False
1026    savpath = sys.path[:]
1027    sys.path = [fpth]
1028    # test to see if a shared library can be used
1029    try:
1030        import pyspg
1031        pyspg.sgforpy('P -1')
1032    except Exception as err:
1033        print(70*'=')
1034        print('Failed to run pyspg in {}\nerror: {}'.format(fpth,err))
1035        print(70*'=')
1036        sys.path = savpath
1037        return False
1038    sys.path = savpath
1039    return True
1040   
1041# see if a directory for local modifications is defined. If so, stick that in the path
1042if os.path.exists(os.path.expanduser('~/.G2local/')):
1043    sys.path.insert(0,os.path.expanduser('~/.G2local/'))
1044    fl = glob.glob(os.path.expanduser('~/.G2local/GSASII*.py*'))
1045    files = ""
1046    prev = None
1047    for f in sorted(fl): # make a list of files, dropping .pyc files where a .py exists
1048        f = os.path.split(f)[1]
1049        if os.path.splitext(f)[0] == prev: continue
1050        prev = os.path.splitext(f)[0]
1051        if files: files += ", "
1052        files += f
1053    if files:
1054        print("*"*75)
1055        print("Warning: the following source files are locally overridden in "+os.path.expanduser('~/.G2local/'))
1056        print("  "+files)
1057        print("*"*75)
1058
1059BinaryPathLoaded = False
1060binaryPath = ''
1061def SetBinaryPath(printInfo=False, loadBinary=True):
1062    '''
1063    Add location of GSAS-II shared libraries (binaries: .so or .pyd files) to path
1064   
1065    This routine must be executed after GSASIIpath is imported and before any other
1066    GSAS-II imports are done.
1067    '''
1068    # do this only once no matter how many times it is called
1069    global BinaryPathLoaded,binaryPath
1070    if BinaryPathLoaded: return
1071    try:
1072        inpver = intver(np.__version__)
1073    except (AttributeError,TypeError): # happens on building docs
1074        return
1075    if path2GSAS2 not in sys.path:
1076        sys.path.insert(0,path2GSAS2)  # make sure current path is used
1077    binpath = None
1078    binprfx = GetBinaryPrefix()
1079    for loc in (os.path.abspath(sys.path[0]),os.path.abspath(os.path.split(__file__)[0]),
1080               os.path.expanduser('~/.GSASII')):
1081        # Look at bin directory (created by a local compile) before looking for standard dist files
1082        searchpathlist = [os.path.join(loc,'bin')]
1083        # also look for matching binary dist in loc/AllBinaries
1084        versions = {}
1085        for d in glob.glob(os.path.join(loc,'AllBinaries',binprfx+'*')):
1086            d = os.path.realpath(d)
1087            v = intver(d.rstrip('/').split('_')[-1].lstrip('n'))
1088            versions[v] = d
1089        searchpathlist = [os.path.join(loc,'bin')]
1090        vmin = None
1091        vmax = None
1092        for v in sorted(versions.keys()):
1093            if v <= inpver:
1094                vmin = v
1095            elif v > inpver:
1096                vmax = v
1097                break
1098        if vmin in versions:
1099            searchpathlist.append(versions[vmin])
1100        if vmax in versions:
1101            searchpathlist.append(versions[vmax])
1102        searchpathlist.append(os.path.join(loc,'bindist'))
1103        for fpth in searchpathlist:
1104            if TestSPG(fpth):
1105                binpath = fpth
1106                break       
1107        if binpath: break
1108    if binpath:                                            # were GSAS-II binaries found
1109        sys.path.insert(0,binpath)
1110        binaryPath = binpath
1111        if printInfo:
1112            print('GSAS-II binary directory: {}'.format(binpath))
1113        BinaryPathLoaded = True
1114    elif not loadBinary:
1115        raise Exception
1116    else:                                                  # try loading them
1117        if printInfo:
1118            print('Attempting to download GSAS-II binary files...')
1119        try:
1120            binpath = DownloadG2Binaries(g2home)
1121        except AttributeError:   # this happens when building in Read The Docs
1122            if printInfo:
1123                print('Problem with download')
1124        if binpath and TestSPG(binpath):
1125            if printInfo:
1126                print('GSAS-II binary directory: {}'.format(binpath))
1127            sys.path.insert(0,binpath)
1128            binaryPath = binpath
1129            BinaryPathLoaded = True
1130        # this must be imported before anything that imports any .pyd/.so file for GSASII
1131        else:
1132            #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1133            #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1134            # patch: use old location based on the host OS and the python version, 
1135            # path is relative to location of the script that is called as well as this file
1136            BinaryPathLoaded = True
1137            bindir = None
1138            if sys.platform == "win32":
1139                if platform.architecture()[0] == '64bit':
1140                    bindir = 'binwin64-%d.%d' % sys.version_info[0:2]
1141                else:
1142                    bindir = 'binwin%d.%d' % sys.version_info[0:2]
1143            elif sys.platform == "darwin":
1144                if platform.architecture()[0] == '64bit':
1145                    bindir = 'binmac64-%d.%d' % sys.version_info[0:2]
1146                else:
1147                    bindir = 'binmac%d.%d' % sys.version_info[0:2]
1148                #if platform.mac_ver()[0].startswith('10.5.'):
1149                #    bindir += '_10.5'
1150            elif sys.platform.startswith("linux"):
1151                if platform.architecture()[0] == '64bit':
1152                    bindir = 'binlinux64-%d.%d' % sys.version_info[0:2]
1153                else:
1154                    bindir = 'binlinux%d.%d' % sys.version_info[0:2]
1155            for loc in os.path.abspath(sys.path[0]),os.path.abspath(os.path.split(__file__)[0]):
1156            # Look at bin directory (created by a local compile) before standard dist
1157            # that at the top of the path
1158                fpth = os.path.join(loc,bindir)
1159                binpath = fpth
1160                if TestSPG(fpth):
1161                    sys.path.insert(0,binpath)
1162                    binaryPath = binpath
1163                    if printInfo:
1164                        print('\n'+75*'*')
1165                        print('  Warning. Using an old-style GSAS-II binary library. This is unexpected')
1166                        print('  and will break in future GSAS-II versions. Please contact toby@anl.gov')
1167                        print('  so we can learn what is not working on your installation.')
1168                        print('GSAS-II binary directory: {}'.format(binpath))
1169                        print(75*'*')
1170                    break
1171            else:
1172            # end patch
1173            #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1174            #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1175                if printInfo:
1176                    print(75*'*')
1177                    print('Use of GSAS-II binary directory {} failed!'.format(binpath))
1178                    print(75*'*')
1179                raise Exception("**** ERROR GSAS-II binary libraries not found, GSAS-II cannot run ****")
1180
1181    # add the data import and export directory to the search path
1182    newpath = os.path.join(path2GSAS2,'imports')
1183    if newpath not in sys.path: sys.path.append(newpath)
1184    newpath = os.path.join(path2GSAS2,'exports')
1185    if newpath not in sys.path: sys.path.append(newpath)
1186    LoadConfig(printInfo)
1187
1188def LoadConfig(printInfo=True):
1189    # setup read of config.py, if present
1190    global configDict
1191    try:
1192        import config
1193        configDict = config.__dict__
1194        import inspect
1195        vals = [True for i in inspect.getmembers(config) if '__' not in i[0]]
1196        if printInfo:
1197            print (str(len(vals))+' values read from config file '+os.path.abspath(config.__file__))
1198    except ImportError:
1199        configDict = {'Clip_on':True}
1200    except Exception as err:
1201        print(60*'*',"\nError reading config.py file")
1202        if printInfo:
1203            import traceback
1204            print(traceback.format_exc())
1205        print(60*'*')
1206        configDict = {'Clip_on':True}
1207
1208def MacStartGSASII(g2script,project=''):
1209    '''Start a new instance of GSAS-II by opening a new terminal window and starting
1210    a new GSAS-II process. Used on Mac OS X only.
1211
1212    :param str g2script: file name for the GSASII.py script
1213    :param str project: GSAS-II project (.gpx) file to be opened, default is blank
1214      which opens a new project
1215    '''
1216    if project and os.path.splitext(project)[1] != '.gpx':
1217        print('file {} cannot be used. Not GSAS-II project (.gpx) file'.format(project))
1218        return
1219    if project and not os.path.exists(project):
1220        print('file {} cannot be found.'.format(project))
1221        return 
1222    elif project:
1223        project = os.path.abspath(project)
1224    g2script = os.path.abspath(g2script)
1225    pythonapp = sys.executable
1226    if os.path.exists(pythonapp+'w'): pythonapp += 'w'
1227    script = '''
1228set python to "{}"
1229set appwithpath to "{}"
1230set filename to "{}"
1231
1232tell application "Terminal"
1233     activate
1234     do script python & " " & appwithpath & " " & filename & "; exit"
1235end tell
1236'''.format(pythonapp,g2script,project)
1237    subprocess.Popen(["osascript","-e",script])
1238
1239def MacRunScript(script):
1240    '''Start a bash script in a new terminal window.
1241    Used on Mac OS X only.
1242
1243    :param str script: file name for a bash script
1244    '''
1245    script = os.path.abspath(script)
1246    osascript = '''
1247set bash to "/bin/bash"
1248set filename to "{}"
1249
1250tell application "Terminal"
1251     activate
1252     do script bash & " " & filename & "; exit"
1253end tell
1254'''.format(script)
1255    subprocess.Popen(["osascript","-e",osascript])
1256
1257#======================================================================
1258# conda/pip routines
1259def findConda():
1260    '''Determines if GSAS-II has been installed as g2conda or gsas2full
1261    with conda located relative to this file.
1262    We could also look for conda relative to the python (sys.executable)
1263    image, but I don't want to muck around with python that someone else
1264    installed.
1265    '''
1266    parent = os.path.split(path2GSAS2)[0]
1267    if sys.platform != "win32":
1268        activate = os.path.join(parent,'bin','activate')
1269        conda = os.path.join(parent,'bin','conda')
1270    else:
1271        activate = os.path.join(parent,'Scripts','activate.bat')
1272        conda = os.path.join(parent,'condabin','conda.bat')
1273    if os.path.exists(activate) and os.path.exists(conda):
1274        return conda,activate
1275    else:
1276        return None
1277
1278def runScript(cmds=[], wait=False, G2frame=None):
1279    '''run a shell script of commands in an external process
1280   
1281    :param list cmds: a list of str's, each ietm containing a shell (cmd.exe
1282      or bash) command
1283    :param bool wait: if True indicates the commands should be run and then
1284      the script should return. If False, then the currently running Python
1285      will exit. Default is False
1286    :param wx.Frame G2frame: provides the location of the current .gpx file
1287      to be used to restart GSAS-II after running the commands, if wait
1288      is False. Default is None which prevents restarting GSAS-II regardless of
1289      the value of wait.
1290    '''
1291    import tempfile
1292    if not cmds:  #debug
1293        print('nothing to do in runScript')
1294        return
1295    if sys.platform != "win32":
1296        suffix = '.sh'
1297    else:
1298        suffix = '.bat'
1299       
1300    fp = tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False)
1301    shellname = fp.name
1302    for line in cmds:
1303        fp.write(line)
1304        fp.write('\n')
1305
1306    if not wait:
1307        if G2frame:
1308            projectfile = ''
1309            if G2frame.GSASprojectfile:
1310                projectfile = os.path.realpath(G2frame.GSASprojectfile)
1311            main = os.path.join(path2GSAS2,'GSASII.py')
1312            ex = sys.executable
1313            if sys.platform == "darwin": # mac requires pythonw which is not always reported as sys.executable
1314                if os.path.exists(ex+'w'): ex += 'w'
1315            print ('restart using ',' '.join([ex,main,projectfile]))
1316            fp.write(' '.join([ex,main,projectfile]))
1317            fp.write('\n')
1318    fp.close()
1319
1320    # start the upgrade in a separate interpreter (avoids loading .pyd files)
1321    if sys.platform != "win32":
1322        proc = subprocess.Popen(['bash',shellname])
1323    else:
1324        proc = subprocess.Popen([shellname],shell=True)
1325    if wait:
1326        proc.wait()
1327    else:
1328        if sys.platform != "win32": proc.wait()
1329        sys.exit()
1330       
1331def condaTest(requireAPI=False):
1332    '''Returns True if it appears that Python is being run under Anaconda
1333    Python with conda present. Tests for conda environment vars and that
1334    the conda package is installed in the current environment.
1335
1336    :returns: True, if running under Conda
1337    '''   
1338    if not all([(i in os.environ) for i in ('CONDA_DEFAULT_ENV','CONDA_EXE', 'CONDA_PREFIX', 'CONDA_PYTHON_EXE')]): return False
1339    if requireAPI:
1340        # is the conda package available?
1341        try:
1342            import conda.cli.python_api
1343        except:
1344            print('You do not have the conda package installed in this environment',
1345                  '\nConsider using the "conda install conda" command')
1346            return False
1347
1348    # There is no foolproof way to check if someone activates conda
1349    # but then calls a different Python using its path...
1350    # ...If we are in the base environment then the conda Python
1351    # should be the same path as the one currently being run:
1352    if os.environ['CONDA_DEFAULT_ENV'] == 'base':
1353        try:
1354            if os.path.samefile(os.environ['CONDA_PYTHON_EXE'],
1355                                sys.executable): return True
1356        except:
1357            return False
1358
1359    # ...If not in the base environment, what we can do is check if the
1360    # python we are running in shares the beginning part of its path with
1361    # the one in the base installation:
1362    dir1 = os.path.dirname(os.environ['CONDA_PYTHON_EXE'])
1363    dir2 = os.path.dirname(sys.executable)
1364    if sys.platform != "win32": # python in .../bin/..
1365        dir1 = os.path.dirname(dir1)
1366        dir2 = os.path.dirname(dir2)   
1367    return commonPath(dir1,dir2)
1368
1369def condaInstall(packageList):
1370    '''Installs one or more packages using the anaconda conda package
1371    manager. Can be used to install multiple packages and optionally
1372    use channels.
1373
1374    :param list packageList: a list of strings with name(s) of packages
1375      and optionally conda options.
1376      Examples::
1377
1378       packageList=['gsl']
1379       packageList=['-c','conda-forge','wxpython']
1380       packageList=['numpy','scipy','matplotlib']
1381
1382    :returns: None if the the command ran normally, or an error message
1383      if it did not.
1384    '''
1385    try:
1386        import conda.cli.python_api
1387    except:
1388        print('You do not have the conda package installed in this environment',
1389                  '\nConsider using the "conda install conda" command')
1390        return None
1391    try:
1392        (out, err, rc) = conda.cli.python_api.run_command(
1393            conda.cli.python_api.Commands.INSTALL,packageList
1394#    use_exception_handler=True#, stdout=sys.stdout, stderr=sys.stderr)
1395            )
1396        #print('rc=',rc)
1397        print('Ran conda. output follows...')
1398        print(70*'='+'\n'+out+'\n'+70*'=')
1399        #print('err=',err)
1400        if rc != 0: return str(out)
1401    except Exception as msg:
1402        print("Error occurred, see below\n",msg) 
1403        return "error occurred"
1404    return None
1405   
1406def fullsplit(fil,prev=None):
1407    '''recursive routine to split all levels of directory names
1408    '''
1409    if prev is None: # first call: normalize and drop file name
1410        fil = os.path.normcase(os.path.abspath(os.path.dirname(fil)))
1411        prev = []
1412    i,j = os.path.split(fil)
1413    if j:
1414        prev.insert(0,j)
1415        out = fullsplit(i,prev)
1416    else:
1417        return [i]+prev
1418    return out
1419
1420def commonPath(dir1,dir2):
1421    '''Check if two directories share a path. Note that paths
1422    are considered the same if either directory is a subdirectory
1423    of the other, but not if they are in different subdirectories
1424    /a/b/c shares a path with /a/b/c/d but /a/b/c/d and /a/b/c/e do not.
1425
1426    :returns: True if the paths are common
1427    '''
1428
1429    for i,j in zip(fullsplit(dir1),fullsplit(dir2)):
1430        if i != j: return False
1431    return True
1432
1433def pipInstall(packageList):
1434    '''Installs one or more packages using the pip package installer.
1435    Use of this should be avoided if conda can be used (see :func:`condaTest`
1436    to test for conda). Can be used to install multiple packages together.
1437    One can use pip options, but this is probably not needed.
1438
1439    :param list packageList: a list of strings with name(s) of packages
1440      Examples::
1441
1442       packageList=['gsl']
1443       packageList=['wxpython','matplotlib','scipy']
1444       packageList=[r'\\Mac\\Home\\Scratch\\wheels\\pygsl-2.3.3-py3-none-any.whl']
1445       packageList=['z:/Scratch/wheels/pygsl-2.3.3-py3-none-any.whl']
1446
1447    :returns: None if the the command ran normally, or an error message
1448      if it did not.
1449    '''
1450    try:
1451        subprocess.check_call([sys.executable, '-m', 'pip', 'install']+packageList)
1452    except Exception as msg:
1453        return msg
1454    return None
1455   
1456def condaEnvCreate(envname, packageList, force=False):
1457    '''Create a Python interpreter in a new conda environment. Use this
1458    when there is a potential conflict between packages and it would
1459    be better to keep the packages separate (which is one of the reasons
1460    conda supports environments). Note that conda should be run from the
1461    case environment; this attempts to deal with issues if it is not.
1462
1463    :param str envname: the name of the environment to be created.
1464      If the environment exists, it will be overwritten only if force is True.
1465    :param list packageList: a list of conda install create command
1466      options, such as::
1467
1468            ['python=3.7', 'conda', 'gsl', 'diffpy.pdffit2',
1469                '-c', 'conda-forge', '-c', 'diffpy']
1470
1471    :param bool force: if False (default) an error will be generated
1472      if an environment exists
1473
1474    :returns: (status,msg) where status is True if an error occurs and
1475      msg is a string with error information if status is True or the
1476      location of the newly-created Python interpreter.
1477    '''
1478    if not all([(i in os.environ) for i in ('CONDA_DEFAULT_ENV',
1479                            'CONDA_EXE', 'CONDA_PREFIX', 'CONDA_PYTHON_EXE')]):
1480        return True,'not running under conda?'
1481    try:
1482        import conda.cli.python_api
1483    except:
1484        return True,'conda package not available (in environment)'
1485    # workaround for bug that avoids nesting packages if running from an
1486    # environment (see https://github.com/conda/conda/issues/11493)
1487    p = os.path.dirname(os.path.dirname(os.environ['CONDA_EXE']))
1488    if not os.path.exists(os.path.join(p,'envs')):
1489        msg = ('Error derived installation path not found: '+
1490                  os.path.join(p,'envs'))
1491        print(msg)
1492        return True,msg
1493    newenv = os.path.join(p,'envs',envname)
1494    if os.path.exists(newenv) and not force:
1495        msg = 'path '+newenv+' already exists and force is not set, aborting'
1496        print(msg)
1497        return True,msg
1498    pathList = ['-p',newenv]
1499    try:
1500        (out, err, rc) = conda.cli.python_api.run_command(
1501            conda.cli.python_api.Commands.CREATE,
1502            packageList + pathList,
1503    use_exception_handler=True, stdout=sys.stdout, stderr=sys.stderr
1504            )
1505        #print('rc=',rc)
1506        #print('out=',out)
1507        #print('err=',err)
1508        if rc != 0: return True,str(out)
1509        if sys.platform == "win32":
1510            newpython = os.path.join(newenv,'python.exe')
1511        else:
1512            newpython = os.path.join(newenv,'bin','python')
1513        if os.path.exists(newpython):
1514            return False,newpython
1515        return True,'Unexpected, '+newpython+' not found'
1516    except Exception as msg:
1517        print("Error occurred, see below\n",msg)
1518        return True,'Error: '+str(msg)
1519   
1520def addCondaPkg():
1521    '''Install the conda API into the current conda environment using the
1522    command line, so that the API can be used in the current Python interpreter
1523
1524    Attempts to do this without a shell failed on the Mac because it seems that
1525    the environment was inherited; seems to work w/o shell on Windows.
1526    '''
1527    if not all([(i in os.environ) for i in ('CONDA_DEFAULT_ENV','CONDA_EXE',
1528                        'CONDA_PREFIX', 'CONDA_PYTHON_EXE')]):
1529        return None
1530    condaexe = os.environ['CONDA_EXE']
1531    currenv = os.environ['CONDA_DEFAULT_ENV']
1532    if sys.platform == "win32":
1533        cmd = [os.environ['CONDA_EXE'],'install','conda','-n',currenv,'-y']
1534        p = subprocess.Popen(cmd,
1535                         #stdout=subprocess.PIPE,
1536                         stderr=subprocess.PIPE)
1537    else:
1538        script = 'source ' + os.path.join(
1539            os.path.dirname(os.environ['CONDA_PYTHON_EXE']),
1540            'activate') + ' base; '
1541        script += 'conda install conda -n '+currenv+' -y'
1542        p = subprocess.Popen(script,shell=True,env={},
1543                         #stdout=subprocess.PIPE,
1544                         stderr=subprocess.PIPE)
1545    out,err = MakeByte2str(p.communicate())
1546    #print('Output from adding conda:\n',out)
1547    if err:
1548        print('Note error/warning:')
1549        print(err)
1550    if currenv == "base":
1551        print('\nUnexpected action: adding conda to base environment???')
1552
1553def makeScriptShortcut():
1554    '''Creates a shortcut to GSAS-II in the current Python installation
1555    so that "import G2script" (or "import G2script as GSASIIscripting")
1556    can be used without having to add GSASII to the path.
1557
1558    The new shortcut is then tested.
1559
1560    :returns: returns the name of the created file if successful. None
1561      indicates an error.
1562    '''
1563    import datetime as dt
1564    for p in sys.path:
1565        if 'site-packages' in p: break
1566    else:
1567        print('No site-packages directory found in Python path')
1568        return
1569    newfil = os.path.join(p,'G2script.py')
1570    fp = open(newfil,'w')
1571    fp.write(f'#Created in makeScriptShortcut from {__file__}')
1572    fp.write(dt.datetime.strftime(dt.datetime.now(),
1573                                      " at %Y-%m-%dT%H:%M\n"))
1574
1575    fp.write(f"""import sys,os
1576Path2GSASII='{path2GSAS2}'
1577if os.path.exists(os.path.join(Path2GSASII,'GSASIIscriptable.py')):
1578    print('setting up GSASIIscriptable from',Path2GSASII)
1579    if Path2GSASII not in sys.path:
1580        sys.path.insert(0,Path2GSASII)
1581    from GSASIIscriptable import *
1582else:
1583    print('GSASIIscriptable not found in ',Path2GSASII)
1584    print('Rerun "Install GSASIIscriptable shortcut" from inside GSAS-II')
1585    sys.exit()
1586""")
1587    fp.close()
1588    print('Created file',newfil)
1589    try:
1590        import G2script
1591    except ImportError:
1592        print('Unexpected error: import of G2script failed!')
1593        return
1594    return newfil
1595
1596if __name__ == '__main__':
1597    '''What follows is called to update (or downdate) GSAS-II in a separate process.
1598    '''
1599    LoadConfig()
1600    import time
1601    time.sleep(1) # delay to give the main process a chance to exit
1602    # perform an update and restart GSAS-II
1603    try:
1604        project,version = sys.argv[1:3]
1605    except ValueError:
1606        project = None
1607        version = 'trunk'
1608    loc = os.path.dirname(__file__)
1609    if version == 'trunk':
1610        svnSwitch2branch('')
1611    elif '/' in version:
1612        svnSwitch2branch(version)
1613    elif version:
1614        print("Regress to version "+str(version))
1615        svnUpdateDir(loc,version=version)
1616    else:
1617        print("Update to current version")
1618        svnUpdateDir(loc)
1619    ex = sys.executable
1620    if sys.platform == "darwin": # mac requires pythonw which is not always reported as sys.executable
1621        if os.path.exists(ex+'w'): ex += 'w'
1622    if project:
1623        print("Restart GSAS-II with project file "+str(project))
1624        subprocess.Popen([ex,os.path.join(loc,'GSASII.py'),project])
1625    else:
1626        print("Restart GSAS-II without a project file ")
1627        subprocess.Popen([ex,os.path.join(loc,'GSASII.py')])
1628    print ('exiting update process')
1629    sys.exit()
Note: See TracBrowser for help on using the repository browser.