1# Licensed under a 3-clause BSD style license - see LICENSE.rst 2"""This module contains classes and functions to standardize access to 3configuration files for Astropy and affiliated packages. 4 5.. note:: 6 The configuration system makes use of the 'configobj' package, which stores 7 configuration in a text format like that used in the standard library 8 `ConfigParser`. More information and documentation for configobj can be 9 found at https://configobj.readthedocs.io . 10""" 11 12import io 13import pkgutil 14import warnings 15import importlib 16import contextlib 17import os 18from os import path 19from textwrap import TextWrapper 20from warnings import warn 21from contextlib import contextmanager, nullcontext 22 23from astropy.extern.configobj import configobj, validate 24from astropy.utils import find_current_module, silence 25from astropy.utils.decorators import deprecated 26from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyWarning 27from astropy.utils.introspection import resolve_name 28 29from .paths import get_config_dir 30 31__all__ = ('InvalidConfigurationItemWarning', 'ConfigurationMissingWarning', 32 'get_config', 'reload_config', 'ConfigNamespace', 'ConfigItem', 33 'generate_config', 'create_config_file') 34 35 36class InvalidConfigurationItemWarning(AstropyWarning): 37 """ A Warning that is issued when the configuration value specified in the 38 astropy configuration file does not match the type expected for that 39 configuration value. 40 """ 41 42 43# This was raised with Astropy < 4.3 when the configuration file was not found. 44# It is kept for compatibility and should be removed at some point. 45@deprecated('5.0') 46class ConfigurationMissingWarning(AstropyWarning): 47 """ A Warning that is issued when the configuration directory cannot be 48 accessed (usually due to a permissions problem). If this warning appears, 49 configuration items will be set to their defaults rather than read from the 50 configuration file, and no configuration will persist across sessions. 51 """ 52 53 54# these are not in __all__ because it's not intended that a user ever see them 55class ConfigurationDefaultMissingError(ValueError): 56 """ An exception that is raised when the configuration defaults (which 57 should be generated at build-time) are missing. 58 """ 59 60 61# this is used in astropy/__init__.py 62class ConfigurationDefaultMissingWarning(AstropyWarning): 63 """ A warning that is issued when the configuration defaults (which 64 should be generated at build-time) are missing. 65 """ 66 67 68class ConfigurationChangedWarning(AstropyWarning): 69 """ 70 A warning that the configuration options have changed. 71 """ 72 73 74class _ConfigNamespaceMeta(type): 75 def __init__(cls, name, bases, dict): 76 if cls.__bases__[0] is object: 77 return 78 79 for key, val in dict.items(): 80 if isinstance(val, ConfigItem): 81 val.name = key 82 83 84class ConfigNamespace(metaclass=_ConfigNamespaceMeta): 85 """ 86 A namespace of configuration items. Each subpackage with 87 configuration items should define a subclass of this class, 88 containing `ConfigItem` instances as members. 89 90 For example:: 91 92 class Conf(_config.ConfigNamespace): 93 unicode_output = _config.ConfigItem( 94 False, 95 'Use Unicode characters when outputting values, ...') 96 use_color = _config.ConfigItem( 97 sys.platform != 'win32', 98 'When True, use ANSI color escape sequences when ...', 99 aliases=['astropy.utils.console.USE_COLOR']) 100 conf = Conf() 101 """ 102 def __iter__(self): 103 for key, val in self.__class__.__dict__.items(): 104 if isinstance(val, ConfigItem): 105 yield key 106 107 keys = __iter__ 108 """Iterate over configuration item names.""" 109 110 def values(self): 111 """Iterate over configuration item values.""" 112 for val in self.__class__.__dict__.values(): 113 if isinstance(val, ConfigItem): 114 yield val 115 116 def items(self): 117 """Iterate over configuration item ``(name, value)`` pairs.""" 118 for key, val in self.__class__.__dict__.items(): 119 if isinstance(val, ConfigItem): 120 yield key, val 121 122 def set_temp(self, attr, value): 123 """ 124 Temporarily set a configuration value. 125 126 Parameters 127 ---------- 128 attr : str 129 Configuration item name 130 131 value : object 132 The value to set temporarily. 133 134 Examples 135 -------- 136 >>> import astropy 137 >>> with astropy.conf.set_temp('use_color', False): 138 ... pass 139 ... # console output will not contain color 140 >>> # console output contains color again... 141 """ 142 if hasattr(self, attr): 143 return self.__class__.__dict__[attr].set_temp(value) 144 raise AttributeError(f"No configuration parameter '{attr}'") 145 146 def reload(self, attr=None): 147 """ 148 Reload a configuration item from the configuration file. 149 150 Parameters 151 ---------- 152 attr : str, optional 153 The name of the configuration parameter to reload. If not 154 provided, reload all configuration parameters. 155 """ 156 if attr is not None: 157 if hasattr(self, attr): 158 return self.__class__.__dict__[attr].reload() 159 raise AttributeError(f"No configuration parameter '{attr}'") 160 161 for item in self.values(): 162 item.reload() 163 164 def reset(self, attr=None): 165 """ 166 Reset a configuration item to its default. 167 168 Parameters 169 ---------- 170 attr : str, optional 171 The name of the configuration parameter to reload. If not 172 provided, reset all configuration parameters. 173 """ 174 if attr is not None: 175 if hasattr(self, attr): 176 prop = self.__class__.__dict__[attr] 177 prop.set(prop.defaultvalue) 178 return 179 raise AttributeError(f"No configuration parameter '{attr}'") 180 181 for item in self.values(): 182 item.set(item.defaultvalue) 183 184 185class ConfigItem: 186 """ 187 A setting and associated value stored in a configuration file. 188 189 These objects should be created as members of 190 `ConfigNamespace` subclasses, for example:: 191 192 class _Conf(config.ConfigNamespace): 193 unicode_output = config.ConfigItem( 194 False, 195 'Use Unicode characters when outputting values, and writing widgets ' 196 'to the console.') 197 conf = _Conf() 198 199 Parameters 200 ---------- 201 defaultvalue : object, optional 202 The default value for this item. If this is a list of strings, this 203 item will be interpreted as an 'options' value - this item must be one 204 of those values, and the first in the list will be taken as the default 205 value. 206 207 description : str or None, optional 208 A description of this item (will be shown as a comment in the 209 configuration file) 210 211 cfgtype : str or None, optional 212 A type specifier like those used as the *values* of a particular key 213 in a ``configspec`` file of ``configobj``. If None, the type will be 214 inferred from the default value. 215 216 module : str or None, optional 217 The full module name that this item is associated with. The first 218 element (e.g. 'astropy' if this is 'astropy.config.configuration') 219 will be used to determine the name of the configuration file, while 220 the remaining items determine the section. If None, the package will be 221 inferred from the package within which this object's initializer is 222 called. 223 224 aliases : str, or list of str, optional 225 The deprecated location(s) of this configuration item. If the 226 config item is not found at the new location, it will be 227 searched for at all of the old locations. 228 229 Raises 230 ------ 231 RuntimeError 232 If ``module`` is `None`, but the module this item is created from 233 cannot be determined. 234 """ 235 236 # this is used to make validation faster so a Validator object doesn't 237 # have to be created every time 238 _validator = validate.Validator() 239 cfgtype = None 240 """ 241 A type specifier like those used as the *values* of a particular key in a 242 ``configspec`` file of ``configobj``. 243 """ 244 245 rootname = 'astropy' 246 """ 247 Rootname sets the base path for all config files. 248 """ 249 250 def __init__(self, defaultvalue='', description=None, cfgtype=None, 251 module=None, aliases=None): 252 from astropy.utils import isiterable 253 254 if module is None: 255 module = find_current_module(2) 256 if module is None: 257 msg1 = 'Cannot automatically determine get_config module, ' 258 msg2 = 'because it is not called from inside a valid module' 259 raise RuntimeError(msg1 + msg2) 260 else: 261 module = module.__name__ 262 263 self.module = module 264 self.description = description 265 self.__doc__ = description 266 267 # now determine cfgtype if it is not given 268 if cfgtype is None: 269 if (isiterable(defaultvalue) and not 270 isinstance(defaultvalue, str)): 271 # it is an options list 272 dvstr = [str(v) for v in defaultvalue] 273 cfgtype = 'option(' + ', '.join(dvstr) + ')' 274 defaultvalue = dvstr[0] 275 elif isinstance(defaultvalue, bool): 276 cfgtype = 'boolean' 277 elif isinstance(defaultvalue, int): 278 cfgtype = 'integer' 279 elif isinstance(defaultvalue, float): 280 cfgtype = 'float' 281 elif isinstance(defaultvalue, str): 282 cfgtype = 'string' 283 defaultvalue = str(defaultvalue) 284 285 self.cfgtype = cfgtype 286 287 self._validate_val(defaultvalue) 288 self.defaultvalue = defaultvalue 289 290 if aliases is None: 291 self.aliases = [] 292 elif isinstance(aliases, str): 293 self.aliases = [aliases] 294 else: 295 self.aliases = aliases 296 297 def __set__(self, obj, value): 298 return self.set(value) 299 300 def __get__(self, obj, objtype=None): 301 if obj is None: 302 return self 303 return self() 304 305 def set(self, value): 306 """ 307 Sets the current value of this ``ConfigItem``. 308 309 This also updates the comments that give the description and type 310 information. 311 312 Parameters 313 ---------- 314 value 315 The value this item should be set to. 316 317 Raises 318 ------ 319 TypeError 320 If the provided ``value`` is not valid for this ``ConfigItem``. 321 """ 322 try: 323 value = self._validate_val(value) 324 except validate.ValidateError as e: 325 msg = 'Provided value for configuration item {0} not valid: {1}' 326 raise TypeError(msg.format(self.name, e.args[0])) 327 328 sec = get_config(self.module, rootname=self.rootname) 329 330 sec[self.name] = value 331 332 @contextmanager 333 def set_temp(self, value): 334 """ 335 Sets this item to a specified value only inside a with block. 336 337 Use as:: 338 339 ITEM = ConfigItem('ITEM', 'default', 'description') 340 341 with ITEM.set_temp('newval'): 342 #... do something that wants ITEM's value to be 'newval' ... 343 print(ITEM) 344 345 # ITEM is now 'default' after the with block 346 347 Parameters 348 ---------- 349 value 350 The value to set this item to inside the with block. 351 352 """ 353 initval = self() 354 self.set(value) 355 try: 356 yield 357 finally: 358 self.set(initval) 359 360 def reload(self): 361 """ Reloads the value of this ``ConfigItem`` from the relevant 362 configuration file. 363 364 Returns 365 ------- 366 val : object 367 The new value loaded from the configuration file. 368 369 """ 370 self.set(self.defaultvalue) 371 baseobj = get_config(self.module, True, rootname=self.rootname) 372 secname = baseobj.name 373 374 cobj = baseobj 375 # a ConfigObj's parent is itself, so we look for the parent with that 376 while cobj.parent is not cobj: 377 cobj = cobj.parent 378 379 newobj = configobj.ConfigObj(cobj.filename, interpolation=False) 380 if secname is not None: 381 if secname not in newobj: 382 return baseobj.get(self.name) 383 newobj = newobj[secname] 384 385 if self.name in newobj: 386 baseobj[self.name] = newobj[self.name] 387 return baseobj.get(self.name) 388 389 def __repr__(self): 390 out = '<{}: name={!r} value={!r} at 0x{:x}>'.format( 391 self.__class__.__name__, self.name, self(), id(self)) 392 return out 393 394 def __str__(self): 395 out = '\n'.join(('{0}: {1}', 396 ' cfgtype={2!r}', 397 ' defaultvalue={3!r}', 398 ' description={4!r}', 399 ' module={5}', 400 ' value={6!r}')) 401 out = out.format(self.__class__.__name__, self.name, self.cfgtype, 402 self.defaultvalue, self.description, self.module, 403 self()) 404 return out 405 406 def __call__(self): 407 """ Returns the value of this ``ConfigItem`` 408 409 Returns 410 ------- 411 val : object 412 This item's value, with a type determined by the ``cfgtype`` 413 attribute. 414 415 Raises 416 ------ 417 TypeError 418 If the configuration value as stored is not this item's type. 419 420 """ 421 def section_name(section): 422 if section == '': 423 return 'at the top-level' 424 else: 425 return f'in section [{section}]' 426 427 options = [] 428 sec = get_config(self.module, rootname=self.rootname) 429 if self.name in sec: 430 options.append((sec[self.name], self.module, self.name)) 431 432 for alias in self.aliases: 433 module, name = alias.rsplit('.', 1) 434 sec = get_config(module, rootname=self.rootname) 435 if '.' in module: 436 filename, module = module.split('.', 1) 437 else: 438 filename = module 439 module = '' 440 if name in sec: 441 if '.' in self.module: 442 new_module = self.module.split('.', 1)[1] 443 else: 444 new_module = '' 445 warn( 446 "Config parameter '{}' {} of the file '{}' " 447 "is deprecated. Use '{}' {} instead.".format( 448 name, section_name(module), get_config_filename(filename, 449 rootname=self.rootname), 450 self.name, section_name(new_module)), 451 AstropyDeprecationWarning) 452 options.append((sec[name], module, name)) 453 454 if len(options) == 0: 455 self.set(self.defaultvalue) 456 options.append((self.defaultvalue, None, None)) 457 458 if len(options) > 1: 459 filename, sec = self.module.split('.', 1) 460 warn( 461 "Config parameter '{}' {} of the file '{}' is " 462 "given by more than one alias ({}). Using the first.".format( 463 self.name, section_name(sec), get_config_filename(filename, 464 rootname=self.rootname), 465 ', '.join([ 466 '.'.join(x[1:3]) for x in options if x[1] is not None])), 467 AstropyDeprecationWarning) 468 469 val = options[0][0] 470 471 try: 472 return self._validate_val(val) 473 except validate.ValidateError as e: 474 raise TypeError('Configuration value not valid:' + e.args[0]) 475 476 def _validate_val(self, val): 477 """ Validates the provided value based on cfgtype and returns the 478 type-cast value 479 480 throws the underlying configobj exception if it fails 481 """ 482 # note that this will normally use the *class* attribute `_validator`, 483 # but if some arcane reason is needed for making a special one for an 484 # instance or sub-class, it will be used 485 return self._validator.check(self.cfgtype, val) 486 487 488# this dictionary stores the primary copy of the ConfigObj's for each 489# root package 490_cfgobjs = {} 491 492 493def get_config_filename(packageormod=None, rootname=None): 494 """ 495 Get the filename of the config file associated with the given 496 package or module. 497 """ 498 cfg = get_config(packageormod, rootname=rootname) 499 while cfg.parent is not cfg: 500 cfg = cfg.parent 501 return cfg.filename 502 503 504# This is used by testing to override the config file, so we can test 505# with various config files that exercise different features of the 506# config system. 507_override_config_file = None 508 509 510def get_config(packageormod=None, reload=False, rootname=None): 511 """ Gets the configuration object or section associated with a particular 512 package or module. 513 514 Parameters 515 ---------- 516 packageormod : str or None 517 The package for which to retrieve the configuration object. If a 518 string, it must be a valid package name, or if ``None``, the package from 519 which this function is called will be used. 520 521 reload : bool, optional 522 Reload the file, even if we have it cached. 523 524 rootname : str or None 525 Name of the root configuration directory. If ``None`` and 526 ``packageormod`` is ``None``, this defaults to be the name of 527 the package from which this function is called. If ``None`` and 528 ``packageormod`` is not ``None``, this defaults to ``astropy``. 529 530 Returns 531 ------- 532 cfgobj : ``configobj.ConfigObj`` or ``configobj.Section`` 533 If the requested package is a base package, this will be the 534 ``configobj.ConfigObj`` for that package, or if it is a subpackage or 535 module, it will return the relevant ``configobj.Section`` object. 536 537 Raises 538 ------ 539 RuntimeError 540 If ``packageormod`` is `None`, but the package this item is created 541 from cannot be determined. 542 """ 543 544 if packageormod is None: 545 packageormod = find_current_module(2) 546 if packageormod is None: 547 msg1 = 'Cannot automatically determine get_config module, ' 548 msg2 = 'because it is not called from inside a valid module' 549 raise RuntimeError(msg1 + msg2) 550 else: 551 packageormod = packageormod.__name__ 552 553 _autopkg = True 554 555 else: 556 _autopkg = False 557 558 packageormodspl = packageormod.split('.') 559 pkgname = packageormodspl[0] 560 secname = '.'.join(packageormodspl[1:]) 561 562 if rootname is None: 563 if _autopkg: 564 rootname = pkgname 565 else: 566 rootname = 'astropy' # so we don't break affiliated packages 567 568 cobj = _cfgobjs.get(pkgname, None) 569 570 if cobj is None or reload: 571 cfgfn = None 572 try: 573 # This feature is intended only for use by the unit tests 574 if _override_config_file is not None: 575 cfgfn = _override_config_file 576 else: 577 cfgfn = path.join(get_config_dir(rootname=rootname), pkgname + '.cfg') 578 cobj = configobj.ConfigObj(cfgfn, interpolation=False) 579 except OSError: 580 # This can happen when HOME is not set 581 cobj = configobj.ConfigObj(interpolation=False) 582 583 # This caches the object, so if the file becomes accessible, this 584 # function won't see it unless the module is reloaded 585 _cfgobjs[pkgname] = cobj 586 587 if secname: # not the root package 588 if secname not in cobj: 589 cobj[secname] = {} 590 return cobj[secname] 591 else: 592 return cobj 593 594 595def generate_config(pkgname='astropy', filename=None, verbose=False): 596 """Generates a configuration file, from the list of `ConfigItem` 597 objects for each subpackage. 598 599 .. versionadded:: 4.1 600 601 Parameters 602 ---------- 603 pkgname : str or None 604 The package for which to retrieve the configuration object. 605 filename : str or file-like or None 606 If None, the default configuration path is taken from `get_config`. 607 608 """ 609 if verbose: 610 verbosity = nullcontext 611 filter_warnings = AstropyDeprecationWarning 612 else: 613 verbosity = silence 614 filter_warnings = Warning 615 616 package = importlib.import_module(pkgname) 617 with verbosity(), warnings.catch_warnings(): 618 warnings.simplefilter('ignore', category=filter_warnings) 619 for mod in pkgutil.walk_packages(path=package.__path__, 620 prefix=package.__name__ + '.'): 621 622 if (mod.module_finder.path.endswith(('test', 'tests')) or 623 mod.name.endswith('setup_package')): 624 # Skip test and setup_package modules 625 continue 626 if mod.name.split('.')[-1].startswith('_'): 627 # Skip private modules 628 continue 629 630 with contextlib.suppress(ImportError): 631 importlib.import_module(mod.name) 632 633 wrapper = TextWrapper(initial_indent="## ", subsequent_indent='## ', 634 width=78) 635 636 if filename is None: 637 filename = get_config_filename(pkgname) 638 639 with contextlib.ExitStack() as stack: 640 if isinstance(filename, (str, os.PathLike)): 641 fp = stack.enter_context(open(filename, 'w')) 642 else: 643 # assume it's a file object, or io.StringIO 644 fp = filename 645 646 # Parse the subclasses, ordered by their module name 647 subclasses = ConfigNamespace.__subclasses__() 648 processed = set() 649 650 for conf in sorted(subclasses, key=lambda x: x.__module__): 651 mod = conf.__module__ 652 653 # Skip modules for other packages, e.g. astropy modules that 654 # would be imported when running the function for astroquery. 655 if mod.split('.')[0] != pkgname: 656 continue 657 658 # Check that modules are not processed twice, which can happen 659 # when they are imported in another module. 660 if mod in processed: 661 continue 662 else: 663 processed.add(mod) 664 665 print_module = True 666 for item in conf().values(): 667 if print_module: 668 # If this is the first item of the module, we print the 669 # module name, but not if this is the root package... 670 if item.module != pkgname: 671 modname = item.module.replace(f'{pkgname}.', '') 672 fp.write(f"[{modname}]\n\n") 673 print_module = False 674 675 fp.write(wrapper.fill(item.description) + '\n') 676 if isinstance(item.defaultvalue, (tuple, list)): 677 if len(item.defaultvalue) == 0: 678 fp.write(f'# {item.name} = ,\n\n') 679 elif len(item.defaultvalue) == 1: 680 fp.write(f'# {item.name} = {item.defaultvalue[0]},\n\n') 681 else: 682 fp.write(f'# {item.name} = {",".join(map(str, item.defaultvalue))}\n\n') 683 else: 684 fp.write(f'# {item.name} = {item.defaultvalue}\n\n') 685 686 687def reload_config(packageormod=None, rootname=None): 688 """ Reloads configuration settings from a configuration file for the root 689 package of the requested package/module. 690 691 This overwrites any changes that may have been made in `ConfigItem` 692 objects. This applies for any items that are based on this file, which 693 is determined by the *root* package of ``packageormod`` 694 (e.g. ``'astropy.cfg'`` for the ``'astropy.config.configuration'`` 695 module). 696 697 Parameters 698 ---------- 699 packageormod : str or None 700 The package or module name - see `get_config` for details. 701 rootname : str or None 702 Name of the root configuration directory - see `get_config` 703 for details. 704 """ 705 sec = get_config(packageormod, True, rootname=rootname) 706 # look for the section that is its own parent - that's the base object 707 while sec.parent is not sec: 708 sec = sec.parent 709 sec.reload() 710 711 712def is_unedited_config_file(content, template_content=None): 713 """ 714 Determines if a config file can be safely replaced because it doesn't 715 actually contain any meaningful content, i.e. if it contains only comments 716 or is completely empty. 717 """ 718 buffer = io.StringIO(content) 719 raw_cfg = configobj.ConfigObj(buffer, interpolation=True) 720 # If any of the items is set, return False 721 return not any(len(v) > 0 for v in raw_cfg.values()) 722 723 724# This function is no more used by astropy but it is kept for the other 725# packages that may use it (e.g. astroquery). It should be removed at some 726# point. 727# this is not in __all__ because it's not intended that a user uses it 728@deprecated('5.0') 729def update_default_config(pkg, default_cfg_dir_or_fn, version=None, rootname='astropy'): 730 """ 731 Checks if the configuration file for the specified package exists, 732 and if not, copy over the default configuration. If the 733 configuration file looks like it has already been edited, we do 734 not write over it, but instead write a file alongside it named 735 ``pkg.version.cfg`` as a "template" for the user. 736 737 Parameters 738 ---------- 739 pkg : str 740 The package to be updated. 741 default_cfg_dir_or_fn : str 742 The filename or directory name where the default configuration file is. 743 If a directory name, ``'pkg.cfg'`` will be used in that directory. 744 version : str, optional 745 The current version of the given package. If not provided, it will 746 be obtained from ``pkg.__version__``. 747 rootname : str 748 Name of the root configuration directory. 749 750 Returns 751 ------- 752 updated : bool 753 If the profile was updated, `True`, otherwise `False`. 754 755 Raises 756 ------ 757 AttributeError 758 If the version number of the package could not determined. 759 760 """ 761 762 if path.isdir(default_cfg_dir_or_fn): 763 default_cfgfn = path.join(default_cfg_dir_or_fn, pkg + '.cfg') 764 else: 765 default_cfgfn = default_cfg_dir_or_fn 766 767 if not path.isfile(default_cfgfn): 768 # There is no template configuration file, which basically 769 # means the affiliated package is not using the configuration 770 # system, so just return. 771 return False 772 773 cfgfn = get_config(pkg, rootname=rootname).filename 774 775 with open(default_cfgfn, 'rt', encoding='latin-1') as fr: 776 template_content = fr.read() 777 778 doupdate = False 779 if cfgfn is not None: 780 if path.exists(cfgfn): 781 with open(cfgfn, 'rt', encoding='latin-1') as fd: 782 content = fd.read() 783 784 identical = (content == template_content) 785 786 if not identical: 787 doupdate = is_unedited_config_file( 788 content, template_content) 789 elif path.exists(path.dirname(cfgfn)): 790 doupdate = True 791 identical = False 792 793 if version is None: 794 version = resolve_name(pkg, '__version__') 795 796 # Don't install template files for dev versions, or we'll end up 797 # spamming `~/.astropy/config`. 798 if version and 'dev' not in version and cfgfn is not None: 799 template_path = path.join( 800 get_config_dir(rootname=rootname), f'{pkg}.{version}.cfg') 801 needs_template = not path.exists(template_path) 802 else: 803 needs_template = False 804 805 if doupdate or needs_template: 806 if needs_template: 807 with open(template_path, 'wt', encoding='latin-1') as fw: 808 fw.write(template_content) 809 # If we just installed a new template file and we can't 810 # update the main configuration file because it has user 811 # changes, display a warning. 812 if not identical and not doupdate: 813 warn( 814 "The configuration options in {} {} may have changed, " 815 "your configuration file was not updated in order to " 816 "preserve local changes. A new configuration template " 817 "has been saved to '{}'.".format( 818 pkg, version, template_path), 819 ConfigurationChangedWarning) 820 821 if doupdate and not identical: 822 with open(cfgfn, 'wt', encoding='latin-1') as fw: 823 fw.write(template_content) 824 return True 825 826 return False 827 828 829def create_config_file(pkg, rootname='astropy', overwrite=False): 830 """ 831 Create the default configuration file for the specified package. 832 If the file already exists, it is updated only if it has not been 833 modified. Otherwise the ``overwrite`` flag is needed to overwrite it. 834 835 Parameters 836 ---------- 837 pkg : str 838 The package to be updated. 839 rootname : str 840 Name of the root configuration directory. 841 overwrite : bool 842 Force updating the file if it already exists. 843 844 Returns 845 ------- 846 updated : bool 847 If the profile was updated, `True`, otherwise `False`. 848 849 """ 850 851 # local import to prevent using the logger before it is configured 852 from astropy.logger import log 853 854 cfgfn = get_config_filename(pkg, rootname=rootname) 855 856 # generate the default config template 857 template_content = io.StringIO() 858 generate_config(pkg, template_content) 859 template_content.seek(0) 860 template_content = template_content.read() 861 862 doupdate = True 863 864 # if the file already exists, check that it has not been modified 865 if cfgfn is not None and path.exists(cfgfn): 866 with open(cfgfn, 'rt', encoding='latin-1') as fd: 867 content = fd.read() 868 869 doupdate = is_unedited_config_file(content, template_content) 870 871 if doupdate or overwrite: 872 with open(cfgfn, 'wt', encoding='latin-1') as fw: 873 fw.write(template_content) 874 log.info('The configuration file has been successfully written ' 875 f'to {cfgfn}') 876 return True 877 elif not doupdate: 878 log.warning('The configuration file already exists and seems to ' 879 'have been customized, so it has not been updated. ' 880 'Use overwrite=True if you really want to update it.') 881 882 return False 883