1# -*- coding: utf-8 -*-
2# Licensed under a 3-clause BSD style license - see LICENSE.rst
3
4import io
5import os
6import sys
7import subprocess
8
9import pytest
10
11from astropy.config import (configuration, set_temp_config, paths,
12                            create_config_file)
13from astropy.utils.data import get_pkg_data_filename
14from astropy.utils.exceptions import AstropyDeprecationWarning
15
16
17OLD_CONFIG = {}
18
19
20def setup_module():
21    OLD_CONFIG.clear()
22    OLD_CONFIG.update(configuration._cfgobjs)
23
24
25def teardown_module():
26    configuration._cfgobjs.clear()
27    configuration._cfgobjs.update(OLD_CONFIG)
28
29
30def test_paths():
31    assert 'astropy' in paths.get_config_dir()
32    assert 'astropy' in paths.get_cache_dir()
33
34    assert 'testpkg' in paths.get_config_dir(rootname='testpkg')
35    assert 'testpkg' in paths.get_cache_dir(rootname='testpkg')
36
37
38def test_set_temp_config(tmpdir, monkeypatch):
39    # Check that we start in an understood state.
40    assert configuration._cfgobjs == OLD_CONFIG
41    # Temporarily remove any temporary overrides of the configuration dir.
42    monkeypatch.setattr(paths.set_temp_config, '_temp_path', None)
43
44    orig_config_dir = paths.get_config_dir(rootname='astropy')
45    temp_config_dir = str(tmpdir.mkdir('config'))
46    temp_astropy_config = os.path.join(temp_config_dir, 'astropy')
47
48    # Test decorator mode
49    @paths.set_temp_config(temp_config_dir)
50    def test_func():
51        assert paths.get_config_dir(rootname='astropy') == temp_astropy_config
52
53        # Test temporary restoration of original default
54        with paths.set_temp_config() as d:
55            assert d == orig_config_dir == paths.get_config_dir(rootname='astropy')
56
57    test_func()
58
59    # Test context manager mode (with cleanup)
60    with paths.set_temp_config(temp_config_dir, delete=True):
61        assert paths.get_config_dir(rootname='astropy') == temp_astropy_config
62
63    assert not os.path.exists(temp_config_dir)
64    # Check that we have returned to our old configuration.
65    assert configuration._cfgobjs == OLD_CONFIG
66
67
68def test_set_temp_cache(tmpdir, monkeypatch):
69    monkeypatch.setattr(paths.set_temp_cache, '_temp_path', None)
70
71    orig_cache_dir = paths.get_cache_dir(rootname='astropy')
72    temp_cache_dir = str(tmpdir.mkdir('cache'))
73    temp_astropy_cache = os.path.join(temp_cache_dir, 'astropy')
74
75    # Test decorator mode
76    @paths.set_temp_cache(temp_cache_dir)
77    def test_func():
78        assert paths.get_cache_dir(rootname='astropy') == temp_astropy_cache
79
80        # Test temporary restoration of original default
81        with paths.set_temp_cache() as d:
82            assert d == orig_cache_dir == paths.get_cache_dir(rootname='astropy')
83
84    test_func()
85
86    # Test context manager mode (with cleanup)
87    with paths.set_temp_cache(temp_cache_dir, delete=True):
88        assert paths.get_cache_dir(rootname='astropy') == temp_astropy_cache
89
90    assert not os.path.exists(temp_cache_dir)
91
92
93def test_set_temp_cache_resets_on_exception(tmpdir):
94    """Test for regression of  bug #9704"""
95    t = paths.get_cache_dir()
96    a = tmpdir / 'a'
97    with open(a, 'wt') as f:
98        f.write("not a good cache\n")
99    with pytest.raises(OSError):
100        with paths.set_temp_cache(a):
101            pass
102    assert t == paths.get_cache_dir()
103
104
105def test_config_file():
106    from astropy.config.configuration import get_config, reload_config
107
108    apycfg = get_config('astropy')
109    assert apycfg.filename.endswith('astropy.cfg')
110
111    cfgsec = get_config('astropy.config')
112    assert cfgsec.depth == 1
113    assert cfgsec.name == 'config'
114    assert cfgsec.parent.filename.endswith('astropy.cfg')
115
116    # try with a different package name, still inside astropy config dir:
117    testcfg = get_config('testpkg', rootname='astropy')
118    parts = os.path.normpath(testcfg.filename).split(os.sep)
119    assert '.astropy' in parts or 'astropy' in parts
120    assert parts[-1] == 'testpkg.cfg'
121    configuration._cfgobjs['testpkg'] = None  # HACK
122
123    # try with a different package name, no specified root name (should
124    #   default to astropy):
125    testcfg = get_config('testpkg')
126    parts = os.path.normpath(testcfg.filename).split(os.sep)
127    assert '.astropy' in parts or 'astropy' in parts
128    assert parts[-1] == 'testpkg.cfg'
129    configuration._cfgobjs['testpkg'] = None  # HACK
130
131    # try with a different package name, specified root name:
132    testcfg = get_config('testpkg', rootname='testpkg')
133    parts = os.path.normpath(testcfg.filename).split(os.sep)
134    assert '.testpkg' in parts or 'testpkg' in parts
135    assert parts[-1] == 'testpkg.cfg'
136    configuration._cfgobjs['testpkg'] = None  # HACK
137
138    # try with a subpackage with specified root name:
139    testcfg_sec = get_config('testpkg.somemodule', rootname='testpkg')
140    parts = os.path.normpath(testcfg_sec.parent.filename).split(os.sep)
141    assert '.testpkg' in parts or 'testpkg' in parts
142    assert parts[-1] == 'testpkg.cfg'
143    configuration._cfgobjs['testpkg'] = None  # HACK
144
145    reload_config('astropy')
146
147
148def check_config(conf):
149    # test that the output contains some lines that we expect
150    assert '# unicode_output = False' in conf
151    assert '[io.fits]' in conf
152    assert '[table]' in conf
153    assert '# replace_warnings = ,' in conf
154    assert '[table.jsviewer]' in conf
155    assert '# css_urls = https://cdn.datatables.net/1.10.12/css/jquery.dataTables.css,' in conf
156    assert '[visualization.wcsaxes]' in conf
157    assert '## Whether to log exceptions before raising them.' in conf
158    assert '# log_exceptions = False' in conf
159
160
161def test_generate_config(tmp_path):
162    from astropy.config.configuration import generate_config
163    out = io.StringIO()
164    generate_config('astropy', out)
165    conf = out.getvalue()
166
167    outfile = tmp_path / 'astropy.cfg'
168    generate_config('astropy', outfile)
169    with open(outfile) as fp:
170        conf2 = fp.read()
171
172    for c in (conf, conf2):
173        check_config(c)
174
175
176def test_generate_config2(tmp_path):
177    """Test that generate_config works with the default filename."""
178
179    with set_temp_config(tmp_path):
180        from astropy.config.configuration import generate_config
181        generate_config('astropy')
182
183    assert os.path.exists(tmp_path / 'astropy' / 'astropy.cfg')
184
185    with open(tmp_path / 'astropy' / 'astropy.cfg') as fp:
186        conf = fp.read()
187
188    check_config(conf)
189
190
191def test_create_config_file(tmp_path, caplog):
192    with set_temp_config(tmp_path):
193        create_config_file('astropy')
194
195    # check that the config file has been created
196    assert ('The configuration file has been successfully written'
197            in caplog.records[0].message)
198    assert os.path.exists(tmp_path / 'astropy' / 'astropy.cfg')
199
200    with open(tmp_path / 'astropy' / 'astropy.cfg') as fp:
201        conf = fp.read()
202    check_config(conf)
203
204    caplog.clear()
205
206    # now modify the config file
207    conf = conf.replace('# unicode_output = False', 'unicode_output = True')
208    with open(tmp_path / 'astropy' / 'astropy.cfg', mode='w') as fp:
209        fp.write(conf)
210
211    with set_temp_config(tmp_path):
212        create_config_file('astropy')
213
214    # check that the config file has not been overwritten since it was modified
215    assert ('The configuration file already exists and seems to have been '
216            'customized' in caplog.records[0].message)
217
218    caplog.clear()
219
220    with set_temp_config(tmp_path):
221        create_config_file('astropy', overwrite=True)
222
223    # check that the config file has been overwritten
224    assert ('The configuration file has been successfully written'
225            in caplog.records[0].message)
226
227
228def test_configitem():
229
230    from astropy.config.configuration import ConfigNamespace, ConfigItem, get_config
231
232    ci = ConfigItem(34, 'this is a Description')
233
234    class Conf(ConfigNamespace):
235        tstnm = ci
236
237    conf = Conf()
238
239    assert ci.module == 'astropy.config.tests.test_configs'
240    assert ci() == 34
241    assert ci.description == 'this is a Description'
242
243    assert conf.tstnm == 34
244
245    sec = get_config(ci.module)
246    assert sec['tstnm'] == 34
247
248    ci.description = 'updated Descr'
249    ci.set(32)
250    assert ci() == 32
251
252    # It's useful to go back to the default to allow other test functions to
253    # call this one and still be in the default configuration.
254    ci.description = 'this is a Description'
255    ci.set(34)
256    assert ci() == 34
257
258    # Test iterator for one-item namespace
259    result = [x for x in conf]
260    assert result == ['tstnm']
261    result = [x for x in conf.keys()]
262    assert result == ['tstnm']
263    result = [x for x in conf.values()]
264    assert result == [ci]
265    result = [x for x in conf.items()]
266    assert result == [('tstnm', ci)]
267
268
269def test_configitem_types():
270
271    from astropy.config.configuration import ConfigNamespace, ConfigItem
272
273    ci1 = ConfigItem(34)
274    ci2 = ConfigItem(34.3)
275    ci3 = ConfigItem(True)
276    ci4 = ConfigItem('astring')
277
278    class Conf(ConfigNamespace):
279        tstnm1 = ci1
280        tstnm2 = ci2
281        tstnm3 = ci3
282        tstnm4 = ci4
283
284    conf = Conf()
285
286    assert isinstance(conf.tstnm1, int)
287    assert isinstance(conf.tstnm2, float)
288    assert isinstance(conf.tstnm3, bool)
289    assert isinstance(conf.tstnm4, str)
290
291    with pytest.raises(TypeError):
292        conf.tstnm1 = 34.3
293    conf.tstnm2 = 12  # this would should succeed as up-casting
294    with pytest.raises(TypeError):
295        conf.tstnm3 = 'fasd'
296    with pytest.raises(TypeError):
297        conf.tstnm4 = 546.245
298
299    # Test iterator for multi-item namespace. Assume ordered by insertion order.
300    item_names = [x for x in conf]
301    assert item_names == ['tstnm1', 'tstnm2', 'tstnm3', 'tstnm4']
302    result = [x for x in conf.keys()]
303    assert result == item_names
304    result = [x for x in conf.values()]
305    assert result == [ci1, ci2, ci3, ci4]
306    result = [x for x in conf.items()]
307    assert result == [('tstnm1', ci1), ('tstnm2', ci2), ('tstnm3', ci3), ('tstnm4', ci4)]
308
309
310def test_configitem_options(tmpdir):
311
312    from astropy.config.configuration import ConfigNamespace, ConfigItem, get_config
313
314    cio = ConfigItem(['op1', 'op2', 'op3'])
315
316    class Conf(ConfigNamespace):
317        tstnmo = cio
318
319    conf = Conf()  # noqa
320
321    sec = get_config(cio.module)
322
323    assert isinstance(cio(), str)
324    assert cio() == 'op1'
325    assert sec['tstnmo'] == 'op1'
326
327    cio.set('op2')
328    with pytest.raises(TypeError):
329        cio.set('op5')
330    assert sec['tstnmo'] == 'op2'
331
332    # now try saving
333    apycfg = sec
334    while apycfg.parent is not apycfg:
335        apycfg = apycfg.parent
336    f = tmpdir.join('astropy.cfg')
337    with open(f.strpath, 'wb') as fd:
338        apycfg.write(fd)
339    with open(f.strpath, 'r', encoding='utf-8') as fd:
340        lns = [x.strip() for x in f.readlines()]
341
342    assert 'tstnmo = op2' in lns
343
344
345def test_config_noastropy_fallback(monkeypatch):
346    """
347    Tests to make sure configuration items fall back to their defaults when
348    there's a problem accessing the astropy directory
349    """
350
351    # make sure the config directory is not searched
352    monkeypatch.setenv('XDG_CONFIG_HOME', 'foo')
353    monkeypatch.delenv('XDG_CONFIG_HOME')
354    monkeypatch.setattr(paths.set_temp_config, '_temp_path', None)
355
356    # make sure the _find_or_create_root_dir function fails as though the
357    # astropy dir could not be accessed
358    def osraiser(dirnm, linkto, pkgname=None):
359        raise OSError
360    monkeypatch.setattr(paths, '_find_or_create_root_dir', osraiser)
361
362    # also have to make sure the stored configuration objects are cleared
363    monkeypatch.setattr(configuration, '_cfgobjs', {})
364
365    with pytest.raises(OSError):
366        # make sure the config dir search fails
367        paths.get_config_dir(rootname='astropy')
368
369    # now run the basic tests, and make sure the warning about no astropy
370    # is present
371    test_configitem()
372
373
374def test_configitem_setters():
375
376    from astropy.config.configuration import ConfigNamespace, ConfigItem
377
378    class Conf(ConfigNamespace):
379        tstnm12 = ConfigItem(42, 'this is another Description')
380
381    conf = Conf()
382
383    assert conf.tstnm12 == 42
384    with conf.set_temp('tstnm12', 45):
385        assert conf.tstnm12 == 45
386    assert conf.tstnm12 == 42
387
388    conf.tstnm12 = 43
389    assert conf.tstnm12 == 43
390
391    with conf.set_temp('tstnm12', 46):
392        assert conf.tstnm12 == 46
393
394    # Make sure it is reset even with Exception
395    try:
396        with conf.set_temp('tstnm12', 47):
397            raise Exception
398    except Exception:
399        pass
400
401    assert conf.tstnm12 == 43
402
403
404def test_empty_config_file():
405    from astropy.config.configuration import is_unedited_config_file
406
407    def get_content(fn):
408        with open(get_pkg_data_filename(fn), 'rt', encoding='latin-1') as fd:
409            return fd.read()
410
411    content = get_content('data/empty.cfg')
412    assert is_unedited_config_file(content)
413
414    content = get_content('data/not_empty.cfg')
415    assert not is_unedited_config_file(content)
416
417
418class TestAliasRead:
419
420    def setup_class(self):
421        configuration._override_config_file = get_pkg_data_filename('data/alias.cfg')
422
423    def test_alias_read(self):
424        from astropy.utils.data import conf
425
426        with pytest.warns(
427                AstropyDeprecationWarning,
428                match=r"Config parameter 'name_resolve_timeout' in section "
429                      r"\[coordinates.name_resolve\].*") as w:
430            conf.reload()
431            assert conf.remote_timeout == 42
432
433        assert len(w) == 1
434
435    def teardown_class(self):
436        from astropy.utils.data import conf
437
438        configuration._override_config_file = None
439        conf.reload()
440
441
442def test_configitem_unicode(tmpdir):
443
444    from astropy.config.configuration import ConfigNamespace, ConfigItem, get_config
445
446    cio = ConfigItem('ასტრონომიის')
447
448    class Conf(ConfigNamespace):
449        tstunicode = cio
450
451    conf = Conf()  # noqa
452
453    sec = get_config(cio.module)
454
455    assert isinstance(cio(), str)
456    assert cio() == 'ასტრონომიის'
457    assert sec['tstunicode'] == 'ასტრონომიის'
458
459
460def test_warning_move_to_top_level():
461    # Check that the warning about deprecation config items in the
462    # file works.  See #2514
463    from astropy import conf
464
465    configuration._override_config_file = get_pkg_data_filename('data/deprecated.cfg')
466
467    try:
468        with pytest.warns(AstropyDeprecationWarning) as w:
469            conf.reload()
470            conf.max_lines
471        assert len(w) == 1
472    finally:
473        configuration._override_config_file = None
474        conf.reload()
475
476
477def test_no_home():
478    # "import astropy" fails when neither $HOME or $XDG_CONFIG_HOME
479    # are set.  To test, we unset those environment variables for a
480    # subprocess and try to import astropy.
481
482    test_path = os.path.dirname(__file__)
483    astropy_path = os.path.abspath(
484        os.path.join(test_path, '..', '..', '..'))
485
486    env = os.environ.copy()
487    paths = [astropy_path]
488    if env.get('PYTHONPATH'):
489        paths.append(env.get('PYTHONPATH'))
490    env['PYTHONPATH'] = os.pathsep.join(paths)
491
492    for val in ['HOME', 'XDG_CONFIG_HOME']:
493        if val in env:
494            del env[val]
495
496    retcode = subprocess.check_call(
497        [sys.executable, '-c', 'import astropy'],
498        env=env)
499
500    assert retcode == 0
501