1"""
2This module contains various utilities for introspecting the distutils
3module and the setup process.
4
5Some of these utilities require the
6`astropy_helpers.setup_helpers.register_commands` function to be called first,
7as it will affect introspection of setuptools command-line arguments.  Other
8utilities in this module do not have that restriction.
9"""
10
11import os
12import sys
13
14from distutils import ccompiler, log
15from distutils.dist import Distribution
16from distutils.errors import DistutilsError
17
18from .utils import silence
19
20
21# This function, and any functions that call it, require the setup in
22# `astropy_helpers.setup_helpers.register_commands` to be run first.
23def get_dummy_distribution():
24    """
25    Returns a distutils Distribution object used to instrument the setup
26    environment before calling the actual setup() function.
27    """
28
29    from .setup_helpers import _module_state
30
31    if _module_state['registered_commands'] is None:
32        raise RuntimeError(
33            'astropy_helpers.setup_helpers.register_commands() must be '
34            'called before using '
35            'astropy_helpers.setup_helpers.get_dummy_distribution()')
36
37    # Pre-parse the Distutils command-line options and config files to if
38    # the option is set.
39    dist = Distribution({'script_name': os.path.basename(sys.argv[0]),
40                         'script_args': sys.argv[1:]})
41    dist.cmdclass.update(_module_state['registered_commands'])
42
43    with silence():
44        try:
45            dist.parse_config_files()
46            dist.parse_command_line()
47        except (DistutilsError, AttributeError, SystemExit):
48            # Let distutils handle DistutilsErrors itself AttributeErrors can
49            # get raise for ./setup.py --help SystemExit can be raised if a
50            # display option was used, for example
51            pass
52
53    return dist
54
55
56def get_main_package_directory(distribution):
57    """
58    Given a Distribution object, return the main package directory.
59    """
60    return min(distribution.packages, key=len).replace('.', os.sep)
61
62def get_distutils_option(option, commands):
63    """ Returns the value of the given distutils option.
64
65    Parameters
66    ----------
67    option : str
68        The name of the option
69
70    commands : list of str
71        The list of commands on which this option is available
72
73    Returns
74    -------
75    val : str or None
76        the value of the given distutils option. If the option is not set,
77        returns None.
78    """
79
80    dist = get_dummy_distribution()
81
82    for cmd in commands:
83        cmd_opts = dist.command_options.get(cmd)
84        if cmd_opts is not None and option in cmd_opts:
85            return cmd_opts[option][1]
86    else:
87        return None
88
89
90def get_distutils_build_option(option):
91    """ Returns the value of the given distutils build option.
92
93    Parameters
94    ----------
95    option : str
96        The name of the option
97
98    Returns
99    -------
100    val : str or None
101        The value of the given distutils build option. If the option
102        is not set, returns None.
103    """
104    return get_distutils_option(option, ['build', 'build_ext', 'build_clib'])
105
106
107def get_distutils_install_option(option):
108    """ Returns the value of the given distutils install option.
109
110    Parameters
111    ----------
112    option : str
113        The name of the option
114
115    Returns
116    -------
117    val : str or None
118        The value of the given distutils build option. If the option
119        is not set, returns None.
120    """
121    return get_distutils_option(option, ['install'])
122
123
124def get_distutils_build_or_install_option(option):
125    """ Returns the value of the given distutils build or install option.
126
127    Parameters
128    ----------
129    option : str
130        The name of the option
131
132    Returns
133    -------
134    val : str or None
135        The value of the given distutils build or install option. If the
136        option is not set, returns None.
137    """
138    return get_distutils_option(option, ['build', 'build_ext', 'build_clib',
139                                         'install'])
140
141
142def get_compiler_option():
143    """ Determines the compiler that will be used to build extension modules.
144
145    Returns
146    -------
147    compiler : str
148        The compiler option specified for the build, build_ext, or build_clib
149        command; or the default compiler for the platform if none was
150        specified.
151
152    """
153
154    compiler = get_distutils_build_option('compiler')
155    if compiler is None:
156        return ccompiler.get_default_compiler()
157
158    return compiler
159
160
161def add_command_option(command, name, doc, is_bool=False):
162    """
163    Add a custom option to a setup command.
164
165    Issues a warning if the option already exists on that command.
166
167    Parameters
168    ----------
169    command : str
170        The name of the command as given on the command line
171
172    name : str
173        The name of the build option
174
175    doc : str
176        A short description of the option, for the `--help` message
177
178    is_bool : bool, optional
179        When `True`, the option is a boolean option and doesn't
180        require an associated value.
181    """
182
183    dist = get_dummy_distribution()
184    cmdcls = dist.get_command_class(command)
185
186    if (hasattr(cmdcls, '_astropy_helpers_options') and
187            name in cmdcls._astropy_helpers_options):
188        return
189
190    attr = name.replace('-', '_')
191
192    if hasattr(cmdcls, attr):
193        raise RuntimeError(
194            '{0!r} already has a {1!r} class attribute, barring {2!r} from '
195            'being usable as a custom option name.'.format(cmdcls, attr, name))
196
197    for idx, cmd in enumerate(cmdcls.user_options):
198        if cmd[0] == name:
199            log.warn('Overriding existing {0!r} option '
200                     '{1!r}'.format(command, name))
201            del cmdcls.user_options[idx]
202            if name in cmdcls.boolean_options:
203                cmdcls.boolean_options.remove(name)
204            break
205
206    cmdcls.user_options.append((name, None, doc))
207
208    if is_bool:
209        cmdcls.boolean_options.append(name)
210
211    # Distutils' command parsing requires that a command object have an
212    # attribute with the same name as the option (with '-' replaced with '_')
213    # in order for that option to be recognized as valid
214    setattr(cmdcls, attr, None)
215
216    # This caches the options added through add_command_option so that if it is
217    # run multiple times in the same interpreter repeated adds are ignored
218    # (this way we can still raise a RuntimeError if a custom option overrides
219    # a built-in option)
220    if not hasattr(cmdcls, '_astropy_helpers_options'):
221        cmdcls._astropy_helpers_options = set([name])
222    else:
223        cmdcls._astropy_helpers_options.add(name)
224
225
226def get_distutils_display_options():
227    """ Returns a set of all the distutils display options in their long and
228    short forms.  These are the setup.py arguments such as --name or --version
229    which print the project's metadata and then exit.
230
231    Returns
232    -------
233    opts : set
234        The long and short form display option arguments, including the - or --
235    """
236
237    short_display_opts = set('-' + o[1] for o in Distribution.display_options
238                             if o[1])
239    long_display_opts = set('--' + o[0] for o in Distribution.display_options)
240
241    # Include -h and --help which are not explicitly listed in
242    # Distribution.display_options (as they are handled by optparse)
243    short_display_opts.add('-h')
244    long_display_opts.add('--help')
245
246    # This isn't the greatest approach to hardcode these commands.
247    # However, there doesn't seem to be a good way to determine
248    # whether build *will be* run as part of the command at this
249    # phase.
250    display_commands = set([
251        'clean', 'register', 'setopt', 'saveopts', 'egg_info',
252        'alias'])
253
254    return short_display_opts.union(long_display_opts.union(display_commands))
255
256
257def is_distutils_display_option():
258    """ Returns True if sys.argv contains any of the distutils display options
259    such as --version or --name.
260    """
261
262    display_options = get_distutils_display_options()
263    return bool(set(sys.argv[1:]).intersection(display_options))
264