1# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
3import contextlib
4import imp
5import os
6import sys
7import glob
8
9from importlib import machinery as import_machinery
10
11
12# Note: The following Warning subclasses are simply copies of the Warnings in
13# Astropy of the same names.
14class AstropyWarning(Warning):
15    """
16    The base warning class from which all Astropy warnings should inherit.
17
18    Any warning inheriting from this class is handled by the Astropy logger.
19    """
20
21
22class AstropyDeprecationWarning(AstropyWarning):
23    """
24    A warning class to indicate a deprecated feature.
25    """
26
27
28class AstropyPendingDeprecationWarning(PendingDeprecationWarning,
29                                       AstropyWarning):
30    """
31    A warning class to indicate a soon-to-be deprecated feature.
32    """
33
34
35def _get_platlib_dir(cmd):
36    """
37    Given a build command, return the name of the appropriate platform-specific
38    build subdirectory directory (e.g. build/lib.linux-x86_64-2.7)
39    """
40
41    plat_specifier = '.{0}-{1}'.format(cmd.plat_name, sys.version[0:3])
42    return os.path.join(cmd.build_base, 'lib' + plat_specifier)
43
44
45def get_numpy_include_path():
46    """
47    Gets the path to the numpy headers.
48    """
49    # We need to go through this nonsense in case setuptools
50    # downloaded and installed Numpy for us as part of the build or
51    # install, since Numpy may still think it's in "setup mode", when
52    # in fact we're ready to use it to build astropy now.
53
54    import builtins
55    if hasattr(builtins, '__NUMPY_SETUP__'):
56        del builtins.__NUMPY_SETUP__
57    import imp
58    import numpy
59    imp.reload(numpy)
60
61    try:
62        numpy_include = numpy.get_include()
63    except AttributeError:
64        numpy_include = numpy.get_numpy_include()
65    return numpy_include
66
67
68class _DummyFile(object):
69    """A noop writeable object."""
70
71    errors = ''
72
73    def write(self, s):
74        pass
75
76    def flush(self):
77        pass
78
79
80@contextlib.contextmanager
81def silence():
82    """A context manager that silences sys.stdout and sys.stderr."""
83
84    old_stdout = sys.stdout
85    old_stderr = sys.stderr
86    sys.stdout = _DummyFile()
87    sys.stderr = _DummyFile()
88    exception_occurred = False
89    try:
90        yield
91    except:
92        exception_occurred = True
93        # Go ahead and clean up so that exception handling can work normally
94        sys.stdout = old_stdout
95        sys.stderr = old_stderr
96        raise
97
98    if not exception_occurred:
99        sys.stdout = old_stdout
100        sys.stderr = old_stderr
101
102
103if sys.platform == 'win32':
104    import ctypes
105
106    def _has_hidden_attribute(filepath):
107        """
108        Returns True if the given filepath has the hidden attribute on
109        MS-Windows.  Based on a post here:
110        http://stackoverflow.com/questions/284115/cross-platform-hidden-file-detection
111        """
112        if isinstance(filepath, bytes):
113            filepath = filepath.decode(sys.getfilesystemencoding())
114        try:
115            attrs = ctypes.windll.kernel32.GetFileAttributesW(filepath)
116            assert attrs != -1
117            result = bool(attrs & 2)
118        except (AttributeError, AssertionError):
119            result = False
120        return result
121else:
122    def _has_hidden_attribute(filepath):
123        return False
124
125
126def is_path_hidden(filepath):
127    """
128    Determines if a given file or directory is hidden.
129
130    Parameters
131    ----------
132    filepath : str
133        The path to a file or directory
134
135    Returns
136    -------
137    hidden : bool
138        Returns `True` if the file is hidden
139    """
140
141    name = os.path.basename(os.path.abspath(filepath))
142    if isinstance(name, bytes):
143        is_dotted = name.startswith(b'.')
144    else:
145        is_dotted = name.startswith('.')
146    return is_dotted or _has_hidden_attribute(filepath)
147
148
149def walk_skip_hidden(top, onerror=None, followlinks=False):
150    """
151    A wrapper for `os.walk` that skips hidden files and directories.
152
153    This function does not have the parameter `topdown` from
154    `os.walk`: the directories must always be recursed top-down when
155    using this function.
156
157    See also
158    --------
159    os.walk : For a description of the parameters
160    """
161
162    for root, dirs, files in os.walk(
163            top, topdown=True, onerror=onerror,
164            followlinks=followlinks):
165        # These lists must be updated in-place so os.walk will skip
166        # hidden directories
167        dirs[:] = [d for d in dirs if not is_path_hidden(d)]
168        files[:] = [f for f in files if not is_path_hidden(f)]
169        yield root, dirs, files
170
171
172def write_if_different(filename, data):
173    """Write `data` to `filename`, if the content of the file is different.
174
175    Parameters
176    ----------
177    filename : str
178        The file name to be written to.
179    data : bytes
180        The data to be written to `filename`.
181    """
182
183    assert isinstance(data, bytes)
184
185    if os.path.exists(filename):
186        with open(filename, 'rb') as fd:
187            original_data = fd.read()
188    else:
189        original_data = None
190
191    if original_data != data:
192        with open(filename, 'wb') as fd:
193            fd.write(data)
194
195
196def import_file(filename, name=None):
197    """
198    Imports a module from a single file as if it doesn't belong to a
199    particular package.
200
201    The returned module will have the optional ``name`` if given, or else
202    a name generated from the filename.
203    """
204    # Specifying a traditional dot-separated fully qualified name here
205    # results in a number of "Parent module 'astropy' not found while
206    # handling absolute import" warnings.  Using the same name, the
207    # namespaces of the modules get merged together.  So, this
208    # generates an underscore-separated name which is more likely to
209    # be unique, and it doesn't really matter because the name isn't
210    # used directly here anyway.
211    mode = 'r'
212
213    if name is None:
214        basename = os.path.splitext(filename)[0]
215        name = '_'.join(os.path.relpath(basename).split(os.sep)[1:])
216
217    if not os.path.exists(filename):
218        raise ImportError('Could not import file {0}'.format(filename))
219
220    if import_machinery:
221        loader = import_machinery.SourceFileLoader(name, filename)
222        mod = loader.load_module()
223    else:
224        with open(filename, mode) as fd:
225            mod = imp.load_module(name, fd, filename, ('.py', mode, 1))
226
227    return mod
228
229
230def resolve_name(name):
231    """Resolve a name like ``module.object`` to an object and return it.
232
233    Raise `ImportError` if the module or name is not found.
234    """
235
236    parts = name.split('.')
237    cursor = len(parts) - 1
238    module_name = parts[:cursor]
239    attr_name = parts[-1]
240
241    while cursor > 0:
242        try:
243            ret = __import__('.'.join(module_name), fromlist=[attr_name])
244            break
245        except ImportError:
246            if cursor == 0:
247                raise
248            cursor -= 1
249            module_name = parts[:cursor]
250            attr_name = parts[cursor]
251            ret = ''
252
253    for part in parts[cursor:]:
254        try:
255            ret = getattr(ret, part)
256        except AttributeError:
257            raise ImportError(name)
258
259    return ret
260
261
262def extends_doc(extended_func):
263    """
264    A function decorator for use when wrapping an existing function but adding
265    additional functionality.  This copies the docstring from the original
266    function, and appends to it (along with a newline) the docstring of the
267    wrapper function.
268
269    Examples
270    --------
271
272        >>> def foo():
273        ...     '''Hello.'''
274        ...
275        >>> @extends_doc(foo)
276        ... def bar():
277        ...     '''Goodbye.'''
278        ...
279        >>> print(bar.__doc__)
280        Hello.
281
282        Goodbye.
283
284    """
285
286    def decorator(func):
287        if not (extended_func.__doc__ is None or func.__doc__ is None):
288            func.__doc__ = '\n\n'.join([extended_func.__doc__.rstrip('\n'),
289                                        func.__doc__.lstrip('\n')])
290        return func
291
292    return decorator
293
294
295def find_data_files(package, pattern):
296    """
297    Include files matching ``pattern`` inside ``package``.
298
299    Parameters
300    ----------
301    package : str
302        The package inside which to look for data files
303    pattern : str
304        Pattern (glob-style) to match for the data files (e.g. ``*.dat``).
305        This supports the``**``recursive syntax. For example, ``**/*.fits``
306        matches all files ending with ``.fits`` recursively. Only one
307        instance of ``**`` can be included in the pattern.
308    """
309
310    return glob.glob(os.path.join(package, pattern), recursive=True)
311