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