19
19
contains an option called ``auto_use`` with a value of ``True``, it will
20
20
automatically call the main function of this module called
21
21
`use_astropy_helpers` (see that function's docstring for full details).
22
- Otherwise no further action is taken (however,
23
- ``ah_bootstrap.use_astropy_helpers`` may be called manually from within the
24
- setup.py script).
22
+ Otherwise no further action is taken and by default the system-installed version
23
+ of astropy-helpers will be used (however, ``ah_bootstrap.use_astropy_helpers``
24
+ may be called manually from within the setup.py script).
25
+
26
+ This behavior can also be controlled using the ``--auto-use`` and
27
+ ``--no-auto-use`` command-line flags. For clarity, an alias for
28
+ ``--no-auto-use`` is ``--use-system-astropy-helpers``, and we recommend using
29
+ the latter if needed.
25
30
26
31
Additional options in the ``[ah_boostrap]`` section of setup.cfg have the same
27
32
names as the arguments to `use_astropy_helpers`, and can be used to configure
33
38
34
39
import contextlib
35
40
import errno
36
- import imp
37
41
import io
38
42
import locale
39
43
import os
40
44
import re
41
45
import subprocess as sp
42
46
import sys
43
47
48
+ __minimum_python_version__ = (3 , 5 )
49
+
50
+ if sys .version_info < __minimum_python_version__ :
51
+ print ("ERROR: Python {} or later is required by astropy-helpers" .format (
52
+ __minimum_python_version__ ))
53
+ sys .exit (1 )
54
+
44
55
try :
45
56
from ConfigParser import ConfigParser , RawConfigParser
46
57
except ImportError :
47
58
from configparser import ConfigParser , RawConfigParser
48
59
49
60
50
- if sys .version_info [0 ] < 3 :
51
- _str_types = (str , unicode )
52
- _text_type = unicode
53
- PY3 = False
54
- else :
55
- _str_types = (str , bytes )
56
- _text_type = str
57
- PY3 = True
61
+ _str_types = (str , bytes )
58
62
59
63
60
64
# What follows are several import statements meant to deal with install-time
61
65
# issues with either missing or misbehaving pacakges (including making sure
62
66
# setuptools itself is installed):
63
67
68
+ # Check that setuptools 1.0 or later is present
69
+ from distutils .version import LooseVersion
64
70
65
- # Some pre-setuptools checks to ensure that either distribute or setuptools >=
66
- # 0.7 is used (over pre-distribute setuptools) if it is available on the path;
67
- # otherwise the latest setuptools will be downloaded and bootstrapped with
68
- # ``ez_setup.py``. This used to be included in a separate file called
69
- # setuptools_bootstrap.py; but it was combined into ah_bootstrap.py
70
71
try :
71
- import pkg_resources
72
- _setuptools_req = pkg_resources .Requirement .parse ('setuptools>=0.7' )
73
- # This may raise a DistributionNotFound in which case no version of
74
- # setuptools or distribute is properly installed
75
- _setuptools = pkg_resources .get_distribution ('setuptools' )
76
- if _setuptools not in _setuptools_req :
77
- # Older version of setuptools; check if we have distribute; again if
78
- # this results in DistributionNotFound we want to give up
79
- _distribute = pkg_resources .get_distribution ('distribute' )
80
- if _setuptools != _distribute :
81
- # It's possible on some pathological systems to have an old version
82
- # of setuptools and distribute on sys.path simultaneously; make
83
- # sure distribute is the one that's used
84
- sys .path .insert (1 , _distribute .location )
85
- _distribute .activate ()
86
- imp .reload (pkg_resources )
87
- except :
88
- # There are several types of exceptions that can occur here; if all else
89
- # fails bootstrap and use the bootstrapped version
90
- from ez_setup import use_setuptools
91
- use_setuptools ()
72
+ import setuptools
73
+ assert LooseVersion (setuptools .__version__ ) >= LooseVersion ('1.0' )
74
+ except (ImportError , AssertionError ):
75
+ print ("ERROR: setuptools 1.0 or later is required by astropy-helpers" )
76
+ sys .exit (1 )
77
+
78
+ # typing as a dependency for 1.6.1+ Sphinx causes issues when imported after
79
+ # initializing submodule with ah_boostrap.py
80
+ # See discussion and references in
81
+ # https://github.com/astropy/astropy-helpers/issues/302
82
+
83
+ try :
84
+ import typing # noqa
85
+ except ImportError :
86
+ pass
92
87
93
88
94
89
# Note: The following import is required as a workaround to
97
92
# later cause the TemporaryDirectory class defined in it to stop working when
98
93
# used later on by setuptools
99
94
try :
100
- import setuptools .py31compat
95
+ import setuptools .py31compat # noqa
101
96
except ImportError :
102
97
pass
103
98
126
121
127
122
from setuptools import Distribution
128
123
from setuptools .package_index import PackageIndex
129
- from setuptools .sandbox import run_setup
130
124
131
125
from distutils import log
132
126
from distutils .debug import DEBUG
135
129
# TODO: Maybe enable checking for a specific version of astropy_helpers?
136
130
DIST_NAME = 'astropy-helpers'
137
131
PACKAGE_NAME = 'astropy_helpers'
132
+ UPPER_VERSION_EXCLUSIVE = None
138
133
139
134
# Defaults for other options
140
135
DOWNLOAD_IF_NEEDED = True
@@ -166,7 +161,7 @@ def __init__(self, path=None, index_url=None, use_git=None, offline=None,
166
161
if not (isinstance (path , _str_types ) or path is False ):
167
162
raise TypeError ('path must be a string or False' )
168
163
169
- if PY3 and not isinstance (path , _text_type ):
164
+ if not isinstance (path , str ):
170
165
fs_encoding = sys .getfilesystemencoding ()
171
166
path = path .decode (fs_encoding ) # path to unicode
172
167
@@ -276,6 +271,18 @@ def parse_command_line(cls, argv=None):
276
271
config ['offline' ] = True
277
272
argv .remove ('--offline' )
278
273
274
+ if '--auto-use' in argv :
275
+ config ['auto_use' ] = True
276
+ argv .remove ('--auto-use' )
277
+
278
+ if '--no-auto-use' in argv :
279
+ config ['auto_use' ] = False
280
+ argv .remove ('--no-auto-use' )
281
+
282
+ if '--use-system-astropy-helpers' in argv :
283
+ config ['auto_use' ] = False
284
+ argv .remove ('--use-system-astropy-helpers' )
285
+
279
286
return config
280
287
281
288
def run (self ):
@@ -453,9 +460,10 @@ def _directory_import(self):
453
460
# setup.py exists we can generate it
454
461
setup_py = os .path .join (path , 'setup.py' )
455
462
if os .path .isfile (setup_py ):
456
- with _silence ():
457
- run_setup (os .path .join (path , 'setup.py' ),
458
- ['egg_info' ])
463
+ # We use subprocess instead of run_setup from setuptools to
464
+ # avoid segmentation faults - see the following for more details:
465
+ # https://github.com/cython/cython/issues/2104
466
+ sp .check_output ([sys .executable , 'setup.py' , 'egg_info' ], cwd = path )
459
467
460
468
for dist in pkg_resources .find_distributions (path , True ):
461
469
# There should be only one...
@@ -490,16 +498,32 @@ def get_option_dict(self, command_name):
490
498
if version :
491
499
req = '{0}=={1}' .format (DIST_NAME , version )
492
500
else :
493
- req = DIST_NAME
501
+ if UPPER_VERSION_EXCLUSIVE is None :
502
+ req = DIST_NAME
503
+ else :
504
+ req = '{0}<{1}' .format (DIST_NAME , UPPER_VERSION_EXCLUSIVE )
494
505
495
506
attrs = {'setup_requires' : [req ]}
496
507
508
+ # NOTE: we need to parse the config file (e.g. setup.cfg) to make sure
509
+ # it honours the options set in the [easy_install] section, and we need
510
+ # to explicitly fetch the requirement eggs as setup_requires does not
511
+ # get honored in recent versions of setuptools:
512
+ # https://github.com/pypa/setuptools/issues/1273
513
+
497
514
try :
498
- if DEBUG :
499
- _Distribution (attrs = attrs )
500
- else :
501
- with _silence ():
502
- _Distribution (attrs = attrs )
515
+
516
+ context = _verbose if DEBUG else _silence
517
+ with context ():
518
+ dist = _Distribution (attrs = attrs )
519
+ try :
520
+ dist .parse_config_files (ignore_option_errors = True )
521
+ dist .fetch_build_eggs (req )
522
+ except TypeError :
523
+ # On older versions of setuptools, ignore_option_errors
524
+ # doesn't exist, and the above two lines are not needed
525
+ # so we can just continue
526
+ pass
503
527
504
528
# If the setup_requires succeeded it will have added the new dist to
505
529
# the main working_set
@@ -702,7 +726,7 @@ def _update_submodule(self, submodule, status):
702
726
if self .offline :
703
727
cmd .append ('--no-fetch' )
704
728
elif status == 'U' :
705
- raise _AHBoostrapSystemExit (
729
+ raise _AHBootstrapSystemExit (
706
730
'Error: Submodule {0} contains unresolved merge conflicts. '
707
731
'Please complete or abandon any changes in the submodule so that '
708
732
'it is in a usable state, then try again.' .format (submodule ))
@@ -763,7 +787,7 @@ def run_cmd(cmd):
763
787
msg = 'Command not found: `{0}`' .format (' ' .join (cmd ))
764
788
raise _CommandNotFound (msg , cmd )
765
789
else :
766
- raise _AHBoostrapSystemExit (
790
+ raise _AHBootstrapSystemExit (
767
791
'An unexpected error occurred when running the '
768
792
'`{0}` command:\n {1}' .format (' ' .join (cmd ), str (e )))
769
793
@@ -780,9 +804,9 @@ def run_cmd(cmd):
780
804
stdio_encoding = 'latin1'
781
805
782
806
# Unlikely to fail at this point but even then let's be flexible
783
- if not isinstance (stdout , _text_type ):
807
+ if not isinstance (stdout , str ):
784
808
stdout = stdout .decode (stdio_encoding , 'replace' )
785
- if not isinstance (stderr , _text_type ):
809
+ if not isinstance (stderr , str ):
786
810
stderr = stderr .decode (stdio_encoding , 'replace' )
787
811
788
812
return (p .returncode , stdout , stderr )
@@ -835,6 +859,10 @@ def flush(self):
835
859
pass
836
860
837
861
862
+ @contextlib .contextmanager
863
+ def _verbose ():
864
+ yield
865
+
838
866
@contextlib .contextmanager
839
867
def _silence ():
840
868
"""A context manager that silences sys.stdout and sys.stderr."""
@@ -878,46 +906,6 @@ def __init__(self, *args):
878
906
super (_AHBootstrapSystemExit , self ).__init__ (msg , * args [1 :])
879
907
880
908
881
- if sys .version_info [:2 ] < (2 , 7 ):
882
- # In Python 2.6 the distutils log does not log warnings, errors, etc. to
883
- # stderr so we have to wrap it to ensure consistency at least in this
884
- # module
885
- import distutils
886
-
887
- class log (object ):
888
- def __getattr__ (self , attr ):
889
- return getattr (distutils .log , attr )
890
-
891
- def warn (self , msg , * args ):
892
- self ._log_to_stderr (distutils .log .WARN , msg , * args )
893
-
894
- def error (self , msg ):
895
- self ._log_to_stderr (distutils .log .ERROR , msg , * args )
896
-
897
- def fatal (self , msg ):
898
- self ._log_to_stderr (distutils .log .FATAL , msg , * args )
899
-
900
- def log (self , level , msg , * args ):
901
- if level in (distutils .log .WARN , distutils .log .ERROR ,
902
- distutils .log .FATAL ):
903
- self ._log_to_stderr (level , msg , * args )
904
- else :
905
- distutils .log .log (level , msg , * args )
906
-
907
- def _log_to_stderr (self , level , msg , * args ):
908
- # This is the only truly 'public' way to get the current threshold
909
- # of the log
910
- current_threshold = distutils .log .set_threshold (distutils .log .WARN )
911
- distutils .log .set_threshold (current_threshold )
912
- if level >= current_threshold :
913
- if args :
914
- msg = msg % args
915
- sys .stderr .write ('%s\n ' % msg )
916
- sys .stderr .flush ()
917
-
918
- log = log ()
919
-
920
-
921
909
BOOTSTRAPPER = _Bootstrapper .main ()
922
910
923
911
0 commit comments