1# Licensed under a 3-clause BSD style license - see PYFITS.rst 2 3import collections 4import copy 5import itertools 6import numbers 7import re 8import warnings 9 10from .card import Card, _pad, KEYWORD_LENGTH, UNDEFINED 11from .file import _File 12from .util import (encode_ascii, decode_ascii, fileobj_closed, 13 fileobj_is_binary, path_like) 14from ._utils import parse_header 15 16from astropy.utils import isiterable 17from astropy.utils.exceptions import AstropyUserWarning 18from astropy.utils.decorators import deprecated_renamed_argument 19 20 21BLOCK_SIZE = 2880 # the FITS block size 22 23# This regular expression can match a *valid* END card which just consists of 24# the string 'END' followed by all spaces, or an *invalid* end card which 25# consists of END, followed by any character that is *not* a valid character 26# for a valid FITS keyword (that is, this is not a keyword like 'ENDER' which 27# starts with 'END' but is not 'END'), followed by any arbitrary bytes. An 28# invalid end card may also consist of just 'END' with no trailing bytes. 29HEADER_END_RE = re.compile(encode_ascii( 30 r'(?:(?P<valid>END {77}) *)|(?P<invalid>END$|END {0,76}[^A-Z0-9_-])')) 31 32 33# According to the FITS standard the only characters that may appear in a 34# header record are the restricted ASCII chars from 0x20 through 0x7E. 35VALID_HEADER_CHARS = set(map(chr, range(0x20, 0x7F))) 36END_CARD = 'END' + ' ' * 77 37 38 39__doctest_skip__ = ['Header', 'Header.comments', 'Header.fromtextfile', 40 'Header.totextfile', 'Header.set', 'Header.update'] 41 42 43class Header: 44 """ 45 FITS header class. This class exposes both a dict-like interface and a 46 list-like interface to FITS headers. 47 48 The header may be indexed by keyword and, like a dict, the associated value 49 will be returned. When the header contains cards with duplicate keywords, 50 only the value of the first card with the given keyword will be returned. 51 It is also possible to use a 2-tuple as the index in the form (keyword, 52 n)--this returns the n-th value with that keyword, in the case where there 53 are duplicate keywords. 54 55 For example:: 56 57 >>> header['NAXIS'] 58 0 59 >>> header[('FOO', 1)] # Return the value of the second FOO keyword 60 'foo' 61 62 The header may also be indexed by card number:: 63 64 >>> header[0] # Return the value of the first card in the header 65 'T' 66 67 Commentary keywords such as HISTORY and COMMENT are special cases: When 68 indexing the Header object with either 'HISTORY' or 'COMMENT' a list of all 69 the HISTORY/COMMENT values is returned:: 70 71 >>> header['HISTORY'] 72 This is the first history entry in this header. 73 This is the second history entry in this header. 74 ... 75 76 See the Astropy documentation for more details on working with headers. 77 78 Notes 79 ----- 80 Although FITS keywords must be exclusively upper case, retrieving an item 81 in a `Header` object is case insensitive. 82 """ 83 84 def __init__(self, cards=[], copy=False): 85 """ 86 Construct a `Header` from an iterable and/or text file. 87 88 Parameters 89 ---------- 90 cards : list of `Card`, optional 91 The cards to initialize the header with. Also allowed are other 92 `Header` (or `dict`-like) objects. 93 94 .. versionchanged:: 1.2 95 Allowed ``cards`` to be a `dict`-like object. 96 97 copy : bool, optional 98 99 If ``True`` copies the ``cards`` if they were another `Header` 100 instance. 101 Default is ``False``. 102 103 .. versionadded:: 1.3 104 """ 105 self.clear() 106 107 if isinstance(cards, Header): 108 if copy: 109 cards = cards.copy() 110 cards = cards.cards 111 elif isinstance(cards, dict): 112 cards = cards.items() 113 114 for card in cards: 115 self.append(card, end=True) 116 117 self._modified = False 118 119 def __len__(self): 120 return len(self._cards) 121 122 def __iter__(self): 123 for card in self._cards: 124 yield card.keyword 125 126 def __contains__(self, keyword): 127 if keyword in self._keyword_indices or keyword in self._rvkc_indices: 128 # For the most common case (single, standard form keyword lookup) 129 # this will work and is an O(1) check. If it fails that doesn't 130 # guarantee absence, just that we have to perform the full set of 131 # checks in self._cardindex 132 return True 133 try: 134 self._cardindex(keyword) 135 except (KeyError, IndexError): 136 return False 137 return True 138 139 def __getitem__(self, key): 140 if isinstance(key, slice): 141 return self.__class__([copy.copy(c) for c in self._cards[key]]) 142 elif self._haswildcard(key): 143 return self.__class__([copy.copy(self._cards[idx]) 144 for idx in self._wildcardmatch(key)]) 145 elif isinstance(key, str): 146 key = key.strip() 147 if key.upper() in Card._commentary_keywords: 148 key = key.upper() 149 # Special case for commentary cards 150 return _HeaderCommentaryCards(self, key) 151 152 if isinstance(key, tuple): 153 keyword = key[0] 154 else: 155 keyword = key 156 157 card = self._cards[self._cardindex(key)] 158 159 if card.field_specifier is not None and keyword == card.rawkeyword: 160 # This is RVKC; if only the top-level keyword was specified return 161 # the raw value, not the parsed out float value 162 return card.rawvalue 163 164 value = card.value 165 if value == UNDEFINED: 166 return None 167 return value 168 169 def __setitem__(self, key, value): 170 if self._set_slice(key, value, self): 171 return 172 173 if isinstance(value, tuple): 174 if len(value) > 2: 175 raise ValueError( 176 'A Header item may be set with either a scalar value, ' 177 'a 1-tuple containing a scalar value, or a 2-tuple ' 178 'containing a scalar value and comment string.') 179 if len(value) == 1: 180 value, comment = value[0], None 181 if value is None: 182 value = UNDEFINED 183 elif len(value) == 2: 184 value, comment = value 185 if value is None: 186 value = UNDEFINED 187 if comment is None: 188 comment = '' 189 else: 190 comment = None 191 192 card = None 193 if isinstance(key, numbers.Integral): 194 card = self._cards[key] 195 elif isinstance(key, tuple): 196 card = self._cards[self._cardindex(key)] 197 if value is None: 198 value = UNDEFINED 199 if card: 200 card.value = value 201 if comment is not None: 202 card.comment = comment 203 if card._modified: 204 self._modified = True 205 else: 206 # If we get an IndexError that should be raised; we don't allow 207 # assignment to non-existing indices 208 self._update((key, value, comment)) 209 210 def __delitem__(self, key): 211 if isinstance(key, slice) or self._haswildcard(key): 212 # This is very inefficient but it's not a commonly used feature. 213 # If someone out there complains that they make heavy use of slice 214 # deletions and it's too slow, well, we can worry about it then 215 # [the solution is not too complicated--it would be wait 'til all 216 # the cards are deleted before updating _keyword_indices rather 217 # than updating it once for each card that gets deleted] 218 if isinstance(key, slice): 219 indices = range(*key.indices(len(self))) 220 # If the slice step is backwards we want to reverse it, because 221 # it will be reversed in a few lines... 222 if key.step and key.step < 0: 223 indices = reversed(indices) 224 else: 225 indices = self._wildcardmatch(key) 226 for idx in reversed(indices): 227 del self[idx] 228 return 229 elif isinstance(key, str): 230 # delete ALL cards with the same keyword name 231 key = Card.normalize_keyword(key) 232 indices = self._keyword_indices 233 if key not in self._keyword_indices: 234 indices = self._rvkc_indices 235 236 if key not in indices: 237 # if keyword is not present raise KeyError. 238 # To delete keyword without caring if they were present, 239 # Header.remove(Keyword) can be used with optional argument ignore_missing as True 240 raise KeyError(f"Keyword '{key}' not found.") 241 242 for idx in reversed(indices[key]): 243 # Have to copy the indices list since it will be modified below 244 del self[idx] 245 return 246 247 idx = self._cardindex(key) 248 card = self._cards[idx] 249 keyword = card.keyword 250 del self._cards[idx] 251 keyword = Card.normalize_keyword(keyword) 252 indices = self._keyword_indices[keyword] 253 indices.remove(idx) 254 if not indices: 255 del self._keyword_indices[keyword] 256 257 # Also update RVKC indices if necessary :/ 258 if card.field_specifier is not None: 259 indices = self._rvkc_indices[card.rawkeyword] 260 indices.remove(idx) 261 if not indices: 262 del self._rvkc_indices[card.rawkeyword] 263 264 # We also need to update all other indices 265 self._updateindices(idx, increment=False) 266 self._modified = True 267 268 def __repr__(self): 269 return self.tostring(sep='\n', endcard=False, padding=False) 270 271 def __str__(self): 272 return self.tostring() 273 274 def __eq__(self, other): 275 """ 276 Two Headers are equal only if they have the exact same string 277 representation. 278 """ 279 280 return str(self) == str(other) 281 282 def __add__(self, other): 283 temp = self.copy(strip=False) 284 temp.extend(other) 285 return temp 286 287 def __iadd__(self, other): 288 self.extend(other) 289 return self 290 291 def _ipython_key_completions_(self): 292 return self.__iter__() 293 294 @property 295 def cards(self): 296 """ 297 The underlying physical cards that make up this Header; it can be 298 looked at, but it should not be modified directly. 299 """ 300 301 return _CardAccessor(self) 302 303 @property 304 def comments(self): 305 """ 306 View the comments associated with each keyword, if any. 307 308 For example, to see the comment on the NAXIS keyword: 309 310 >>> header.comments['NAXIS'] 311 number of data axes 312 313 Comments can also be updated through this interface: 314 315 >>> header.comments['NAXIS'] = 'Number of data axes' 316 317 """ 318 319 return _HeaderComments(self) 320 321 @property 322 def _modified(self): 323 """ 324 Whether or not the header has been modified; this is a property so that 325 it can also check each card for modifications--cards may have been 326 modified directly without the header containing it otherwise knowing. 327 """ 328 329 modified_cards = any(c._modified for c in self._cards) 330 if modified_cards: 331 # If any cards were modified then by definition the header was 332 # modified 333 self.__dict__['_modified'] = True 334 335 return self.__dict__['_modified'] 336 337 @_modified.setter 338 def _modified(self, val): 339 self.__dict__['_modified'] = val 340 341 @classmethod 342 def fromstring(cls, data, sep=''): 343 """ 344 Creates an HDU header from a byte string containing the entire header 345 data. 346 347 Parameters 348 ---------- 349 data : str or bytes 350 String or bytes containing the entire header. In the case of bytes 351 they will be decoded using latin-1 (only plain ASCII characters are 352 allowed in FITS headers but latin-1 allows us to retain any invalid 353 bytes that might appear in malformatted FITS files). 354 355 sep : str, optional 356 The string separating cards from each other, such as a newline. By 357 default there is no card separator (as is the case in a raw FITS 358 file). In general this is only used in cases where a header was 359 printed as text (e.g. with newlines after each card) and you want 360 to create a new `Header` from it by copy/pasting. 361 362 Examples 363 -------- 364 365 >>> from astropy.io.fits import Header 366 >>> hdr = Header({'SIMPLE': True}) 367 >>> Header.fromstring(hdr.tostring()) == hdr 368 True 369 370 If you want to create a `Header` from printed text it's not necessary 371 to have the exact binary structure as it would appear in a FITS file, 372 with the full 80 byte card length. Rather, each "card" can end in a 373 newline and does not have to be padded out to a full card length as 374 long as it "looks like" a FITS header: 375 376 >>> hdr = Header.fromstring(\"\"\"\\ 377 ... SIMPLE = T / conforms to FITS standard 378 ... BITPIX = 8 / array data type 379 ... NAXIS = 0 / number of array dimensions 380 ... EXTEND = T 381 ... \"\"\", sep='\\n') 382 >>> hdr['SIMPLE'] 383 True 384 >>> hdr['BITPIX'] 385 8 386 >>> len(hdr) 387 4 388 389 Returns 390 ------- 391 `Header` 392 A new `Header` instance. 393 """ 394 395 cards = [] 396 397 # If the card separator contains characters that may validly appear in 398 # a card, the only way to unambiguously distinguish between cards is to 399 # require that they be Card.length long. However, if the separator 400 # contains non-valid characters (namely \n) the cards may be split 401 # immediately at the separator 402 require_full_cardlength = set(sep).issubset(VALID_HEADER_CHARS) 403 404 if isinstance(data, bytes): 405 # FITS supports only ASCII, but decode as latin1 and just take all 406 # bytes for now; if it results in mojibake due to e.g. UTF-8 407 # encoded data in a FITS header that's OK because it shouldn't be 408 # there in the first place--accepting it here still gives us the 409 # opportunity to display warnings later during validation 410 CONTINUE = b'CONTINUE' 411 END = b'END' 412 end_card = END_CARD.encode('ascii') 413 sep = sep.encode('latin1') 414 empty = b'' 415 else: 416 CONTINUE = 'CONTINUE' 417 END = 'END' 418 end_card = END_CARD 419 empty = '' 420 421 # Split the header into individual cards 422 idx = 0 423 image = [] 424 425 while idx < len(data): 426 if require_full_cardlength: 427 end_idx = idx + Card.length 428 else: 429 try: 430 end_idx = data.index(sep, idx) 431 except ValueError: 432 end_idx = len(data) 433 434 next_image = data[idx:end_idx] 435 idx = end_idx + len(sep) 436 437 if image: 438 if next_image[:8] == CONTINUE: 439 image.append(next_image) 440 continue 441 cards.append(Card.fromstring(empty.join(image))) 442 443 if require_full_cardlength: 444 if next_image == end_card: 445 image = [] 446 break 447 else: 448 if next_image.split(sep)[0].rstrip() == END: 449 image = [] 450 break 451 452 image = [next_image] 453 454 # Add the last image that was found before the end, if any 455 if image: 456 cards.append(Card.fromstring(empty.join(image))) 457 458 return cls._fromcards(cards) 459 460 @classmethod 461 def fromfile(cls, fileobj, sep='', endcard=True, padding=True): 462 """ 463 Similar to :meth:`Header.fromstring`, but reads the header string from 464 a given file-like object or filename. 465 466 Parameters 467 ---------- 468 fileobj : str, file-like 469 A filename or an open file-like object from which a FITS header is 470 to be read. For open file handles the file pointer must be at the 471 beginning of the header. 472 473 sep : str, optional 474 The string separating cards from each other, such as a newline. By 475 default there is no card separator (as is the case in a raw FITS 476 file). 477 478 endcard : bool, optional 479 If True (the default) the header must end with an END card in order 480 to be considered valid. If an END card is not found an 481 `OSError` is raised. 482 483 padding : bool, optional 484 If True (the default) the header will be required to be padded out 485 to a multiple of 2880, the FITS header block size. Otherwise any 486 padding, or lack thereof, is ignored. 487 488 Returns 489 ------- 490 `Header` 491 A new `Header` instance. 492 """ 493 494 close_file = False 495 496 if isinstance(fileobj, path_like): 497 # If sep is non-empty we are trying to read a header printed to a 498 # text file, so open in text mode by default to support newline 499 # handling; if a binary-mode file object is passed in, the user is 500 # then on their own w.r.t. newline handling. 501 # 502 # Otherwise assume we are reading from an actual FITS file and open 503 # in binary mode. 504 if sep: 505 fileobj = open(fileobj, 'r', encoding='latin1') 506 else: 507 fileobj = open(fileobj, 'rb') 508 509 close_file = True 510 511 try: 512 is_binary = fileobj_is_binary(fileobj) 513 514 def block_iter(nbytes): 515 while True: 516 data = fileobj.read(nbytes) 517 518 if data: 519 yield data 520 else: 521 break 522 523 return cls._from_blocks(block_iter, is_binary, sep, endcard, 524 padding)[1] 525 finally: 526 if close_file: 527 fileobj.close() 528 529 @classmethod 530 def _fromcards(cls, cards): 531 header = cls() 532 for idx, card in enumerate(cards): 533 header._cards.append(card) 534 keyword = Card.normalize_keyword(card.keyword) 535 header._keyword_indices[keyword].append(idx) 536 if card.field_specifier is not None: 537 header._rvkc_indices[card.rawkeyword].append(idx) 538 539 header._modified = False 540 return header 541 542 @classmethod 543 def _from_blocks(cls, block_iter, is_binary, sep, endcard, padding): 544 """ 545 The meat of `Header.fromfile`; in a separate method so that 546 `Header.fromfile` itself is just responsible for wrapping file 547 handling. Also used by `_BaseHDU.fromstring`. 548 549 ``block_iter`` should be a callable which, given a block size n 550 (typically 2880 bytes as used by the FITS standard) returns an iterator 551 of byte strings of that block size. 552 553 ``is_binary`` specifies whether the returned blocks are bytes or text 554 555 Returns both the entire header *string*, and the `Header` object 556 returned by Header.fromstring on that string. 557 """ 558 559 actual_block_size = _block_size(sep) 560 clen = Card.length + len(sep) 561 562 blocks = block_iter(actual_block_size) 563 564 # Read the first header block. 565 try: 566 block = next(blocks) 567 except StopIteration: 568 raise EOFError() 569 570 if not is_binary: 571 # TODO: There needs to be error handling at *this* level for 572 # non-ASCII characters; maybe at this stage decoding latin-1 might 573 # be safer 574 block = encode_ascii(block) 575 576 read_blocks = [] 577 is_eof = False 578 end_found = False 579 580 # continue reading header blocks until END card or EOF is reached 581 while True: 582 # find the END card 583 end_found, block = cls._find_end_card(block, clen) 584 585 read_blocks.append(decode_ascii(block)) 586 587 if end_found: 588 break 589 590 try: 591 block = next(blocks) 592 except StopIteration: 593 is_eof = True 594 break 595 596 if not block: 597 is_eof = True 598 break 599 600 if not is_binary: 601 block = encode_ascii(block) 602 603 header_str = ''.join(read_blocks) 604 _check_padding(header_str, actual_block_size, is_eof, 605 check_block_size=padding) 606 607 if not end_found and is_eof and endcard: 608 # TODO: Pass this error to validation framework as an ERROR, 609 # rather than raising an exception 610 raise OSError('Header missing END card.') 611 612 return header_str, cls.fromstring(header_str, sep=sep) 613 614 @classmethod 615 def _find_end_card(cls, block, card_len): 616 """ 617 Utility method to search a header block for the END card and handle 618 invalid END cards. 619 620 This method can also returned a modified copy of the input header block 621 in case an invalid end card needs to be sanitized. 622 """ 623 624 for mo in HEADER_END_RE.finditer(block): 625 # Ensure the END card was found, and it started on the 626 # boundary of a new card (see ticket #142) 627 if mo.start() % card_len != 0: 628 continue 629 630 # This must be the last header block, otherwise the 631 # file is malformatted 632 if mo.group('invalid'): 633 offset = mo.start() 634 trailing = block[offset + 3:offset + card_len - 3].rstrip() 635 if trailing: 636 trailing = repr(trailing).lstrip('ub') 637 # TODO: Pass this warning up to the validation framework 638 warnings.warn( 639 'Unexpected bytes trailing END keyword: {}; these ' 640 'bytes will be replaced with spaces on write.'.format( 641 trailing), AstropyUserWarning) 642 else: 643 # TODO: Pass this warning up to the validation framework 644 warnings.warn( 645 'Missing padding to end of the FITS block after the ' 646 'END keyword; additional spaces will be appended to ' 647 'the file upon writing to pad out to {} ' 648 'bytes.'.format(BLOCK_SIZE), AstropyUserWarning) 649 650 # Sanitize out invalid END card now that the appropriate 651 # warnings have been issued 652 block = (block[:offset] + encode_ascii(END_CARD) + 653 block[offset + len(END_CARD):]) 654 655 return True, block 656 657 return False, block 658 659 def tostring(self, sep='', endcard=True, padding=True): 660 r""" 661 Returns a string representation of the header. 662 663 By default this uses no separator between cards, adds the END card, and 664 pads the string with spaces to the next multiple of 2880 bytes. That 665 is, it returns the header exactly as it would appear in a FITS file. 666 667 Parameters 668 ---------- 669 sep : str, optional 670 The character or string with which to separate cards. By default 671 there is no separator, but one could use ``'\\n'``, for example, to 672 separate each card with a new line 673 674 endcard : bool, optional 675 If True (default) adds the END card to the end of the header 676 string 677 678 padding : bool, optional 679 If True (default) pads the string with spaces out to the next 680 multiple of 2880 characters 681 682 Returns 683 ------- 684 str 685 A string representing a FITS header. 686 """ 687 688 lines = [] 689 for card in self._cards: 690 s = str(card) 691 # Cards with CONTINUE cards may be longer than 80 chars; so break 692 # them into multiple lines 693 while s: 694 lines.append(s[:Card.length]) 695 s = s[Card.length:] 696 697 s = sep.join(lines) 698 if endcard: 699 s += sep + _pad('END') 700 if padding: 701 s += ' ' * _pad_length(len(s)) 702 return s 703 704 @deprecated_renamed_argument('clobber', 'overwrite', '2.0', 705 message='"clobber" was deprecated in version ' 706 '2.0 and will be removed in version ' 707 '5.1. Use argument "overwrite" ' 708 'instead.') 709 def tofile(self, fileobj, sep='', endcard=True, padding=True, 710 overwrite=False): 711 r""" 712 Writes the header to file or file-like object. 713 714 By default this writes the header exactly as it would be written to a 715 FITS file, with the END card included and padding to the next multiple 716 of 2880 bytes. However, aspects of this may be controlled. 717 718 Parameters 719 ---------- 720 fileobj : path-like or file-like, optional 721 Either the pathname of a file, or an open file handle or file-like 722 object. 723 724 sep : str, optional 725 The character or string with which to separate cards. By default 726 there is no separator, but one could use ``'\\n'``, for example, to 727 separate each card with a new line 728 729 endcard : bool, optional 730 If `True` (default) adds the END card to the end of the header 731 string 732 733 padding : bool, optional 734 If `True` (default) pads the string with spaces out to the next 735 multiple of 2880 characters 736 737 overwrite : bool, optional 738 If ``True``, overwrite the output file if it exists. Raises an 739 ``OSError`` if ``False`` and the output file exists. Default is 740 ``False``. 741 742 .. versionchanged:: 1.3 743 ``overwrite`` replaces the deprecated ``clobber`` argument. 744 """ 745 746 close_file = fileobj_closed(fileobj) 747 748 if not isinstance(fileobj, _File): 749 fileobj = _File(fileobj, mode='ostream', overwrite=overwrite) 750 751 try: 752 blocks = self.tostring(sep=sep, endcard=endcard, padding=padding) 753 actual_block_size = _block_size(sep) 754 if padding and len(blocks) % actual_block_size != 0: 755 raise OSError( 756 'Header size ({}) is not a multiple of block ' 757 'size ({}).'.format( 758 len(blocks) - actual_block_size + BLOCK_SIZE, 759 BLOCK_SIZE)) 760 761 fileobj.flush() 762 fileobj.write(blocks.encode('ascii')) 763 fileobj.flush() 764 finally: 765 if close_file: 766 fileobj.close() 767 768 @classmethod 769 def fromtextfile(cls, fileobj, endcard=False): 770 """ 771 Read a header from a simple text file or file-like object. 772 773 Equivalent to:: 774 775 >>> Header.fromfile(fileobj, sep='\\n', endcard=False, 776 ... padding=False) 777 778 See Also 779 -------- 780 fromfile 781 """ 782 783 return cls.fromfile(fileobj, sep='\n', endcard=endcard, padding=False) 784 785 @deprecated_renamed_argument('clobber', 'overwrite', '2.0', 786 message='"clobber" was deprecated in version ' 787 '2.0 and will be removed in version ' 788 '5.1. Use argument "overwrite" ' 789 'instead.') 790 def totextfile(self, fileobj, endcard=False, overwrite=False): 791 """ 792 Write the header as text to a file or a file-like object. 793 794 Equivalent to:: 795 796 >>> Header.tofile(fileobj, sep='\\n', endcard=False, 797 ... padding=False, overwrite=overwrite) 798 799 .. versionchanged:: 1.3 800 ``overwrite`` replaces the deprecated ``clobber`` argument. 801 802 See Also 803 -------- 804 tofile 805 """ 806 807 self.tofile(fileobj, sep='\n', endcard=endcard, padding=False, 808 overwrite=overwrite) 809 810 def clear(self): 811 """ 812 Remove all cards from the header. 813 """ 814 815 self._cards = [] 816 self._keyword_indices = collections.defaultdict(list) 817 self._rvkc_indices = collections.defaultdict(list) 818 819 def copy(self, strip=False): 820 """ 821 Make a copy of the :class:`Header`. 822 823 .. versionchanged:: 1.3 824 `copy.copy` and `copy.deepcopy` on a `Header` will call this 825 method. 826 827 Parameters 828 ---------- 829 strip : bool, optional 830 If `True`, strip any headers that are specific to one of the 831 standard HDU types, so that this header can be used in a different 832 HDU. 833 834 Returns 835 ------- 836 `Header` 837 A new :class:`Header` instance. 838 """ 839 840 tmp = self.__class__((copy.copy(card) for card in self._cards)) 841 if strip: 842 tmp.strip() 843 return tmp 844 845 def __copy__(self): 846 return self.copy() 847 848 def __deepcopy__(self, *args, **kwargs): 849 return self.copy() 850 851 @classmethod 852 def fromkeys(cls, iterable, value=None): 853 """ 854 Similar to :meth:`dict.fromkeys`--creates a new `Header` from an 855 iterable of keywords and an optional default value. 856 857 This method is not likely to be particularly useful for creating real 858 world FITS headers, but it is useful for testing. 859 860 Parameters 861 ---------- 862 iterable 863 Any iterable that returns strings representing FITS keywords. 864 865 value : optional 866 A default value to assign to each keyword; must be a valid type for 867 FITS keywords. 868 869 Returns 870 ------- 871 `Header` 872 A new `Header` instance. 873 """ 874 875 d = cls() 876 if not isinstance(value, tuple): 877 value = (value,) 878 for key in iterable: 879 d.append((key,) + value) 880 return d 881 882 def get(self, key, default=None): 883 """ 884 Similar to :meth:`dict.get`--returns the value associated with keyword 885 in the header, or a default value if the keyword is not found. 886 887 Parameters 888 ---------- 889 key : str 890 A keyword that may or may not be in the header. 891 892 default : optional 893 A default value to return if the keyword is not found in the 894 header. 895 896 Returns 897 ------- 898 value: str, number, complex, bool, or ``astropy.io.fits.card.Undefined`` 899 The value associated with the given keyword, or the default value 900 if the keyword is not in the header. 901 """ 902 903 try: 904 return self[key] 905 except (KeyError, IndexError): 906 return default 907 908 def set(self, keyword, value=None, comment=None, before=None, after=None): 909 """ 910 Set the value and/or comment and/or position of a specified keyword. 911 912 If the keyword does not already exist in the header, a new keyword is 913 created in the specified position, or appended to the end of the header 914 if no position is specified. 915 916 This method is similar to :meth:`Header.update` prior to Astropy v0.1. 917 918 .. note:: 919 It should be noted that ``header.set(keyword, value)`` and 920 ``header.set(keyword, value, comment)`` are equivalent to 921 ``header[keyword] = value`` and 922 ``header[keyword] = (value, comment)`` respectively. 923 924 New keywords can also be inserted relative to existing keywords 925 using, for example:: 926 927 >>> header.insert('NAXIS1', ('NAXIS', 2, 'Number of axes')) 928 929 to insert before an existing keyword, or:: 930 931 >>> header.insert('NAXIS', ('NAXIS1', 4096), after=True) 932 933 to insert after an existing keyword. 934 935 The only advantage of using :meth:`Header.set` is that it 936 easily replaces the old usage of :meth:`Header.update` both 937 conceptually and in terms of function signature. 938 939 Parameters 940 ---------- 941 keyword : str 942 A header keyword 943 944 value : str, optional 945 The value to set for the given keyword; if None the existing value 946 is kept, but '' may be used to set a blank value 947 948 comment : str, optional 949 The comment to set for the given keyword; if None the existing 950 comment is kept, but ``''`` may be used to set a blank comment 951 952 before : str, int, optional 953 Name of the keyword, or index of the `Card` before which this card 954 should be located in the header. The argument ``before`` takes 955 precedence over ``after`` if both specified. 956 957 after : str, int, optional 958 Name of the keyword, or index of the `Card` after which this card 959 should be located in the header. 960 961 """ 962 963 # Create a temporary card that looks like the one being set; if the 964 # temporary card turns out to be a RVKC this will make it easier to 965 # deal with the idiosyncrasies thereof 966 # Don't try to make a temporary card though if they keyword looks like 967 # it might be a HIERARCH card or is otherwise invalid--this step is 968 # only for validating RVKCs. 969 if (len(keyword) <= KEYWORD_LENGTH and 970 Card._keywd_FSC_RE.match(keyword) and 971 keyword not in self._keyword_indices): 972 new_card = Card(keyword, value, comment) 973 new_keyword = new_card.keyword 974 else: 975 new_keyword = keyword 976 977 if (new_keyword not in Card._commentary_keywords and 978 new_keyword in self): 979 if comment is None: 980 comment = self.comments[keyword] 981 if value is None: 982 value = self[keyword] 983 984 self[keyword] = (value, comment) 985 986 if before is not None or after is not None: 987 card = self._cards[self._cardindex(keyword)] 988 self._relativeinsert(card, before=before, after=after, 989 replace=True) 990 elif before is not None or after is not None: 991 self._relativeinsert((keyword, value, comment), before=before, 992 after=after) 993 else: 994 self[keyword] = (value, comment) 995 996 def items(self): 997 """Like :meth:`dict.items`.""" 998 999 for card in self._cards: 1000 yield card.keyword, None if card.value == UNDEFINED else card.value 1001 1002 def keys(self): 1003 """ 1004 Like :meth:`dict.keys`--iterating directly over the `Header` 1005 instance has the same behavior. 1006 """ 1007 1008 for card in self._cards: 1009 yield card.keyword 1010 1011 def values(self): 1012 """Like :meth:`dict.values`.""" 1013 1014 for card in self._cards: 1015 yield None if card.value == UNDEFINED else card.value 1016 1017 def pop(self, *args): 1018 """ 1019 Works like :meth:`list.pop` if no arguments or an index argument are 1020 supplied; otherwise works like :meth:`dict.pop`. 1021 """ 1022 1023 if len(args) > 2: 1024 raise TypeError(f'Header.pop expected at most 2 arguments, got {len(args)}') 1025 1026 if len(args) == 0: 1027 key = -1 1028 else: 1029 key = args[0] 1030 1031 try: 1032 value = self[key] 1033 except (KeyError, IndexError): 1034 if len(args) == 2: 1035 return args[1] 1036 raise 1037 1038 del self[key] 1039 return value 1040 1041 def popitem(self): 1042 """Similar to :meth:`dict.popitem`.""" 1043 1044 try: 1045 k, v = next(self.items()) 1046 except StopIteration: 1047 raise KeyError('Header is empty') 1048 del self[k] 1049 return k, v 1050 1051 def setdefault(self, key, default=None): 1052 """Similar to :meth:`dict.setdefault`.""" 1053 1054 try: 1055 return self[key] 1056 except (KeyError, IndexError): 1057 self[key] = default 1058 return default 1059 1060 def update(self, *args, **kwargs): 1061 """ 1062 Update the Header with new keyword values, updating the values of 1063 existing keywords and appending new keywords otherwise; similar to 1064 `dict.update`. 1065 1066 `update` accepts either a dict-like object or an iterable. In the 1067 former case the keys must be header keywords and the values may be 1068 either scalar values or (value, comment) tuples. In the case of an 1069 iterable the items must be (keyword, value) tuples or (keyword, value, 1070 comment) tuples. 1071 1072 Arbitrary arguments are also accepted, in which case the update() is 1073 called again with the kwargs dict as its only argument. That is, 1074 1075 :: 1076 1077 >>> header.update(NAXIS1=100, NAXIS2=100) 1078 1079 is equivalent to:: 1080 1081 header.update({'NAXIS1': 100, 'NAXIS2': 100}) 1082 1083 .. warning:: 1084 As this method works similarly to `dict.update` it is very 1085 different from the ``Header.update()`` method in Astropy v0.1. 1086 Use of the old API was 1087 **deprecated** for a long time and is now removed. Most uses of the 1088 old API can be replaced as follows: 1089 1090 * Replace :: 1091 1092 header.update(keyword, value) 1093 1094 with :: 1095 1096 header[keyword] = value 1097 1098 * Replace :: 1099 1100 header.update(keyword, value, comment=comment) 1101 1102 with :: 1103 1104 header[keyword] = (value, comment) 1105 1106 * Replace :: 1107 1108 header.update(keyword, value, before=before_keyword) 1109 1110 with :: 1111 1112 header.insert(before_keyword, (keyword, value)) 1113 1114 * Replace :: 1115 1116 header.update(keyword, value, after=after_keyword) 1117 1118 with :: 1119 1120 header.insert(after_keyword, (keyword, value), 1121 after=True) 1122 1123 See also :meth:`Header.set` which is a new method that provides an 1124 interface similar to the old ``Header.update()`` and may help make 1125 transition a little easier. 1126 1127 """ 1128 1129 if args: 1130 other = args[0] 1131 else: 1132 other = None 1133 1134 def update_from_dict(k, v): 1135 if not isinstance(v, tuple): 1136 card = Card(k, v) 1137 elif 0 < len(v) <= 2: 1138 card = Card(*((k,) + v)) 1139 else: 1140 raise ValueError( 1141 'Header update value for key %r is invalid; the ' 1142 'value must be either a scalar, a 1-tuple ' 1143 'containing the scalar value, or a 2-tuple ' 1144 'containing the value and a comment string.' % k) 1145 self._update(card) 1146 1147 if other is None: 1148 pass 1149 elif isinstance(other, Header): 1150 for card in other.cards: 1151 self._update(card) 1152 elif hasattr(other, 'items'): 1153 for k, v in other.items(): 1154 update_from_dict(k, v) 1155 elif hasattr(other, 'keys'): 1156 for k in other.keys(): 1157 update_from_dict(k, other[k]) 1158 else: 1159 for idx, card in enumerate(other): 1160 if isinstance(card, Card): 1161 self._update(card) 1162 elif isinstance(card, tuple) and (1 < len(card) <= 3): 1163 self._update(Card(*card)) 1164 else: 1165 raise ValueError( 1166 'Header update sequence item #{} is invalid; ' 1167 'the item must either be a 2-tuple containing ' 1168 'a keyword and value, or a 3-tuple containing ' 1169 'a keyword, value, and comment string.'.format(idx)) 1170 if kwargs: 1171 self.update(kwargs) 1172 1173 def append(self, card=None, useblanks=True, bottom=False, end=False): 1174 """ 1175 Appends a new keyword+value card to the end of the Header, similar 1176 to `list.append`. 1177 1178 By default if the last cards in the Header have commentary keywords, 1179 this will append the new keyword before the commentary (unless the new 1180 keyword is also commentary). 1181 1182 Also differs from `list.append` in that it can be called with no 1183 arguments: In this case a blank card is appended to the end of the 1184 Header. In the case all the keyword arguments are ignored. 1185 1186 Parameters 1187 ---------- 1188 card : str, tuple 1189 A keyword or a (keyword, value, [comment]) tuple representing a 1190 single header card; the comment is optional in which case a 1191 2-tuple may be used 1192 1193 useblanks : bool, optional 1194 If there are blank cards at the end of the Header, replace the 1195 first blank card so that the total number of cards in the Header 1196 does not increase. Otherwise preserve the number of blank cards. 1197 1198 bottom : bool, optional 1199 If True, instead of appending after the last non-commentary card, 1200 append after the last non-blank card. 1201 1202 end : bool, optional 1203 If True, ignore the useblanks and bottom options, and append at the 1204 very end of the Header. 1205 1206 """ 1207 1208 if isinstance(card, str): 1209 card = Card(card) 1210 elif isinstance(card, tuple): 1211 card = Card(*card) 1212 elif card is None: 1213 card = Card() 1214 elif not isinstance(card, Card): 1215 raise ValueError( 1216 'The value appended to a Header must be either a keyword or ' 1217 '(keyword, value, [comment]) tuple; got: {!r}'.format(card)) 1218 1219 if not end and card.is_blank: 1220 # Blank cards should always just be appended to the end 1221 end = True 1222 1223 if end: 1224 self._cards.append(card) 1225 idx = len(self._cards) - 1 1226 else: 1227 idx = len(self._cards) - 1 1228 while idx >= 0 and self._cards[idx].is_blank: 1229 idx -= 1 1230 1231 if not bottom and card.keyword not in Card._commentary_keywords: 1232 while (idx >= 0 and 1233 self._cards[idx].keyword in Card._commentary_keywords): 1234 idx -= 1 1235 1236 idx += 1 1237 self._cards.insert(idx, card) 1238 self._updateindices(idx) 1239 1240 keyword = Card.normalize_keyword(card.keyword) 1241 self._keyword_indices[keyword].append(idx) 1242 if card.field_specifier is not None: 1243 self._rvkc_indices[card.rawkeyword].append(idx) 1244 1245 if not end: 1246 # If the appended card was a commentary card, and it was appended 1247 # before existing cards with the same keyword, the indices for 1248 # cards with that keyword may have changed 1249 if not bottom and card.keyword in Card._commentary_keywords: 1250 self._keyword_indices[keyword].sort() 1251 1252 # Finally, if useblanks, delete a blank cards from the end 1253 if useblanks and self._countblanks(): 1254 # Don't do this unless there is at least one blanks at the end 1255 # of the header; we need to convert the card to its string 1256 # image to see how long it is. In the vast majority of cases 1257 # this will just be 80 (Card.length) but it may be longer for 1258 # CONTINUE cards 1259 self._useblanks(len(str(card)) // Card.length) 1260 1261 self._modified = True 1262 1263 def extend(self, cards, strip=True, unique=False, update=False, 1264 update_first=False, useblanks=True, bottom=False, end=False): 1265 """ 1266 Appends multiple keyword+value cards to the end of the header, similar 1267 to `list.extend`. 1268 1269 Parameters 1270 ---------- 1271 cards : iterable 1272 An iterable of (keyword, value, [comment]) tuples; see 1273 `Header.append`. 1274 1275 strip : bool, optional 1276 Remove any keywords that have meaning only to specific types of 1277 HDUs, so that only more general keywords are added from extension 1278 Header or Card list (default: `True`). 1279 1280 unique : bool, optional 1281 If `True`, ensures that no duplicate keywords are appended; 1282 keywords already in this header are simply discarded. The 1283 exception is commentary keywords (COMMENT, HISTORY, etc.): they are 1284 only treated as duplicates if their values match. 1285 1286 update : bool, optional 1287 If `True`, update the current header with the values and comments 1288 from duplicate keywords in the input header. This supersedes the 1289 ``unique`` argument. Commentary keywords are treated the same as 1290 if ``unique=True``. 1291 1292 update_first : bool, optional 1293 If the first keyword in the header is 'SIMPLE', and the first 1294 keyword in the input header is 'XTENSION', the 'SIMPLE' keyword is 1295 replaced by the 'XTENSION' keyword. Likewise if the first keyword 1296 in the header is 'XTENSION' and the first keyword in the input 1297 header is 'SIMPLE', the 'XTENSION' keyword is replaced by the 1298 'SIMPLE' keyword. This behavior is otherwise dumb as to whether or 1299 not the resulting header is a valid primary or extension header. 1300 This is mostly provided to support backwards compatibility with the 1301 old ``Header.fromTxtFile`` method, and only applies if 1302 ``update=True``. 1303 1304 useblanks, bottom, end : bool, optional 1305 These arguments are passed to :meth:`Header.append` while appending 1306 new cards to the header. 1307 """ 1308 1309 temp = self.__class__(cards) 1310 if strip: 1311 temp.strip() 1312 1313 if len(self): 1314 first = self._cards[0].keyword 1315 else: 1316 first = None 1317 1318 # We don't immediately modify the header, because first we need to sift 1319 # out any duplicates in the new header prior to adding them to the 1320 # existing header, but while *allowing* duplicates from the header 1321 # being extended from (see ticket #156) 1322 extend_cards = [] 1323 1324 for idx, card in enumerate(temp.cards): 1325 keyword = card.keyword 1326 if keyword not in Card._commentary_keywords: 1327 if unique and not update and keyword in self: 1328 continue 1329 elif update: 1330 if idx == 0 and update_first: 1331 # Dumbly update the first keyword to either SIMPLE or 1332 # XTENSION as the case may be, as was in the case in 1333 # Header.fromTxtFile 1334 if ((keyword == 'SIMPLE' and first == 'XTENSION') or 1335 (keyword == 'XTENSION' and first == 'SIMPLE')): 1336 del self[0] 1337 self.insert(0, card) 1338 else: 1339 self[keyword] = (card.value, card.comment) 1340 elif keyword in self: 1341 self[keyword] = (card.value, card.comment) 1342 else: 1343 extend_cards.append(card) 1344 else: 1345 extend_cards.append(card) 1346 else: 1347 if (unique or update) and keyword in self: 1348 if card.is_blank: 1349 extend_cards.append(card) 1350 continue 1351 1352 for value in self[keyword]: 1353 if value == card.value: 1354 break 1355 else: 1356 extend_cards.append(card) 1357 else: 1358 extend_cards.append(card) 1359 1360 for card in extend_cards: 1361 self.append(card, useblanks=useblanks, bottom=bottom, end=end) 1362 1363 def count(self, keyword): 1364 """ 1365 Returns the count of the given keyword in the header, similar to 1366 `list.count` if the Header object is treated as a list of keywords. 1367 1368 Parameters 1369 ---------- 1370 keyword : str 1371 The keyword to count instances of in the header 1372 1373 """ 1374 1375 keyword = Card.normalize_keyword(keyword) 1376 1377 # We have to look before we leap, since otherwise _keyword_indices, 1378 # being a defaultdict, will create an entry for the nonexistent keyword 1379 if keyword not in self._keyword_indices: 1380 raise KeyError(f"Keyword {keyword!r} not found.") 1381 1382 return len(self._keyword_indices[keyword]) 1383 1384 def index(self, keyword, start=None, stop=None): 1385 """ 1386 Returns the index if the first instance of the given keyword in the 1387 header, similar to `list.index` if the Header object is treated as a 1388 list of keywords. 1389 1390 Parameters 1391 ---------- 1392 keyword : str 1393 The keyword to look up in the list of all keywords in the header 1394 1395 start : int, optional 1396 The lower bound for the index 1397 1398 stop : int, optional 1399 The upper bound for the index 1400 1401 """ 1402 1403 if start is None: 1404 start = 0 1405 1406 if stop is None: 1407 stop = len(self._cards) 1408 1409 if stop < start: 1410 step = -1 1411 else: 1412 step = 1 1413 1414 norm_keyword = Card.normalize_keyword(keyword) 1415 1416 for idx in range(start, stop, step): 1417 if self._cards[idx].keyword.upper() == norm_keyword: 1418 return idx 1419 else: 1420 raise ValueError(f'The keyword {keyword!r} is not in the header.') 1421 1422 def insert(self, key, card, useblanks=True, after=False): 1423 """ 1424 Inserts a new keyword+value card into the Header at a given location, 1425 similar to `list.insert`. 1426 1427 Parameters 1428 ---------- 1429 key : int, str, or tuple 1430 The index into the list of header keywords before which the 1431 new keyword should be inserted, or the name of a keyword before 1432 which the new keyword should be inserted. Can also accept a 1433 (keyword, index) tuple for inserting around duplicate keywords. 1434 1435 card : str, tuple 1436 A keyword or a (keyword, value, [comment]) tuple; see 1437 `Header.append` 1438 1439 useblanks : bool, optional 1440 If there are blank cards at the end of the Header, replace the 1441 first blank card so that the total number of cards in the Header 1442 does not increase. Otherwise preserve the number of blank cards. 1443 1444 after : bool, optional 1445 If set to `True`, insert *after* the specified index or keyword, 1446 rather than before it. Defaults to `False`. 1447 """ 1448 1449 if not isinstance(key, numbers.Integral): 1450 # Don't pass through ints to _cardindex because it will not take 1451 # kindly to indices outside the existing number of cards in the 1452 # header, which insert needs to be able to support (for example 1453 # when inserting into empty headers) 1454 idx = self._cardindex(key) 1455 else: 1456 idx = key 1457 1458 if after: 1459 if idx == -1: 1460 idx = len(self._cards) 1461 else: 1462 idx += 1 1463 1464 if idx >= len(self._cards): 1465 # This is just an append (Though it must be an append absolutely to 1466 # the bottom, ignoring blanks, etc.--the point of the insert method 1467 # is that you get exactly what you asked for with no surprises) 1468 self.append(card, end=True) 1469 return 1470 1471 if isinstance(card, str): 1472 card = Card(card) 1473 elif isinstance(card, tuple): 1474 card = Card(*card) 1475 elif not isinstance(card, Card): 1476 raise ValueError( 1477 'The value inserted into a Header must be either a keyword or ' 1478 '(keyword, value, [comment]) tuple; got: {!r}'.format(card)) 1479 1480 self._cards.insert(idx, card) 1481 1482 keyword = card.keyword 1483 1484 # If idx was < 0, determine the actual index according to the rules 1485 # used by list.insert() 1486 if idx < 0: 1487 idx += len(self._cards) - 1 1488 if idx < 0: 1489 idx = 0 1490 1491 # All the keyword indices above the insertion point must be updated 1492 self._updateindices(idx) 1493 1494 keyword = Card.normalize_keyword(keyword) 1495 self._keyword_indices[keyword].append(idx) 1496 count = len(self._keyword_indices[keyword]) 1497 if count > 1: 1498 # There were already keywords with this same name 1499 if keyword not in Card._commentary_keywords: 1500 warnings.warn( 1501 'A {!r} keyword already exists in this header. Inserting ' 1502 'duplicate keyword.'.format(keyword), AstropyUserWarning) 1503 self._keyword_indices[keyword].sort() 1504 1505 if card.field_specifier is not None: 1506 # Update the index of RVKC as well 1507 rvkc_indices = self._rvkc_indices[card.rawkeyword] 1508 rvkc_indices.append(idx) 1509 rvkc_indices.sort() 1510 1511 if useblanks: 1512 self._useblanks(len(str(card)) // Card.length) 1513 1514 self._modified = True 1515 1516 def remove(self, keyword, ignore_missing=False, remove_all=False): 1517 """ 1518 Removes the first instance of the given keyword from the header similar 1519 to `list.remove` if the Header object is treated as a list of keywords. 1520 1521 Parameters 1522 ---------- 1523 keyword : str 1524 The keyword of which to remove the first instance in the header. 1525 1526 ignore_missing : bool, optional 1527 When True, ignores missing keywords. Otherwise, if the keyword 1528 is not present in the header a KeyError is raised. 1529 1530 remove_all : bool, optional 1531 When True, all instances of keyword will be removed. 1532 Otherwise only the first instance of the given keyword is removed. 1533 1534 """ 1535 keyword = Card.normalize_keyword(keyword) 1536 if keyword in self._keyword_indices: 1537 del self[self._keyword_indices[keyword][0]] 1538 if remove_all: 1539 while keyword in self._keyword_indices: 1540 del self[self._keyword_indices[keyword][0]] 1541 elif not ignore_missing: 1542 raise KeyError(f"Keyword '{keyword}' not found.") 1543 1544 def rename_keyword(self, oldkeyword, newkeyword, force=False): 1545 """ 1546 Rename a card's keyword in the header. 1547 1548 Parameters 1549 ---------- 1550 oldkeyword : str or int 1551 Old keyword or card index 1552 1553 newkeyword : str 1554 New keyword 1555 1556 force : bool, optional 1557 When `True`, if the new keyword already exists in the header, force 1558 the creation of a duplicate keyword. Otherwise a 1559 `ValueError` is raised. 1560 """ 1561 1562 oldkeyword = Card.normalize_keyword(oldkeyword) 1563 newkeyword = Card.normalize_keyword(newkeyword) 1564 1565 if newkeyword == 'CONTINUE': 1566 raise ValueError('Can not rename to CONTINUE') 1567 1568 if (newkeyword in Card._commentary_keywords or 1569 oldkeyword in Card._commentary_keywords): 1570 if not (newkeyword in Card._commentary_keywords and 1571 oldkeyword in Card._commentary_keywords): 1572 raise ValueError('Regular and commentary keys can not be ' 1573 'renamed to each other.') 1574 elif not force and newkeyword in self: 1575 raise ValueError(f'Intended keyword {newkeyword} already exists in header.') 1576 1577 idx = self.index(oldkeyword) 1578 card = self._cards[idx] 1579 del self[idx] 1580 self.insert(idx, (newkeyword, card.value, card.comment)) 1581 1582 def add_history(self, value, before=None, after=None): 1583 """ 1584 Add a ``HISTORY`` card. 1585 1586 Parameters 1587 ---------- 1588 value : str 1589 History text to be added. 1590 1591 before : str or int, optional 1592 Same as in `Header.update` 1593 1594 after : str or int, optional 1595 Same as in `Header.update` 1596 """ 1597 1598 self._add_commentary('HISTORY', value, before=before, after=after) 1599 1600 def add_comment(self, value, before=None, after=None): 1601 """ 1602 Add a ``COMMENT`` card. 1603 1604 Parameters 1605 ---------- 1606 value : str 1607 Text to be added. 1608 1609 before : str or int, optional 1610 Same as in `Header.update` 1611 1612 after : str or int, optional 1613 Same as in `Header.update` 1614 """ 1615 1616 self._add_commentary('COMMENT', value, before=before, after=after) 1617 1618 def add_blank(self, value='', before=None, after=None): 1619 """ 1620 Add a blank card. 1621 1622 Parameters 1623 ---------- 1624 value : str, optional 1625 Text to be added. 1626 1627 before : str or int, optional 1628 Same as in `Header.update` 1629 1630 after : str or int, optional 1631 Same as in `Header.update` 1632 """ 1633 1634 self._add_commentary('', value, before=before, after=after) 1635 1636 def strip(self): 1637 """ 1638 Strip cards specific to a certain kind of header. 1639 1640 Strip cards like ``SIMPLE``, ``BITPIX``, etc. so the rest of 1641 the header can be used to reconstruct another kind of header. 1642 """ 1643 1644 # TODO: Previously this only deleted some cards specific to an HDU if 1645 # _hdutype matched that type. But it seemed simple enough to just 1646 # delete all desired cards anyways, and just ignore the KeyErrors if 1647 # they don't exist. 1648 # However, it might be desirable to make this extendable somehow--have 1649 # a way for HDU classes to specify some headers that are specific only 1650 # to that type, and should be removed otherwise. 1651 1652 naxis = self.get('NAXIS', 0) 1653 tfields = self.get('TFIELDS', 0) 1654 1655 for idx in range(naxis): 1656 self.remove('NAXIS' + str(idx + 1), ignore_missing=True) 1657 1658 for name in ('TFORM', 'TSCAL', 'TZERO', 'TNULL', 'TTYPE', 1659 'TUNIT', 'TDISP', 'TDIM', 'THEAP', 'TBCOL'): 1660 for idx in range(tfields): 1661 self.remove(name + str(idx + 1), ignore_missing=True) 1662 1663 for name in ('SIMPLE', 'XTENSION', 'BITPIX', 'NAXIS', 'EXTEND', 1664 'PCOUNT', 'GCOUNT', 'GROUPS', 'BSCALE', 'BZERO', 1665 'TFIELDS'): 1666 self.remove(name, ignore_missing=True) 1667 1668 def _update(self, card): 1669 """ 1670 The real update code. If keyword already exists, its value and/or 1671 comment will be updated. Otherwise a new card will be appended. 1672 1673 This will not create a duplicate keyword except in the case of 1674 commentary cards. The only other way to force creation of a duplicate 1675 is to use the insert(), append(), or extend() methods. 1676 """ 1677 1678 keyword, value, comment = card 1679 1680 # Lookups for existing/known keywords are case-insensitive 1681 keyword = keyword.strip().upper() 1682 if keyword.startswith('HIERARCH '): 1683 keyword = keyword[9:] 1684 1685 if (keyword not in Card._commentary_keywords and 1686 keyword in self._keyword_indices): 1687 # Easy; just update the value/comment 1688 idx = self._keyword_indices[keyword][0] 1689 existing_card = self._cards[idx] 1690 existing_card.value = value 1691 if comment is not None: 1692 # '' should be used to explicitly blank a comment 1693 existing_card.comment = comment 1694 if existing_card._modified: 1695 self._modified = True 1696 elif keyword in Card._commentary_keywords: 1697 cards = self._splitcommentary(keyword, value) 1698 if keyword in self._keyword_indices: 1699 # Append after the last keyword of the same type 1700 idx = self.index(keyword, start=len(self) - 1, stop=-1) 1701 isblank = not (keyword or value or comment) 1702 for c in reversed(cards): 1703 self.insert(idx + 1, c, useblanks=(not isblank)) 1704 else: 1705 for c in cards: 1706 self.append(c, bottom=True) 1707 else: 1708 # A new keyword! self.append() will handle updating _modified 1709 self.append(card) 1710 1711 def _cardindex(self, key): 1712 """Returns an index into the ._cards list given a valid lookup key.""" 1713 1714 # This used to just set key = (key, 0) and then go on to act as if the 1715 # user passed in a tuple, but it's much more common to just be given a 1716 # string as the key, so optimize more for that case 1717 if isinstance(key, str): 1718 keyword = key 1719 n = 0 1720 elif isinstance(key, numbers.Integral): 1721 # If < 0, determine the actual index 1722 if key < 0: 1723 key += len(self._cards) 1724 if key < 0 or key >= len(self._cards): 1725 raise IndexError('Header index out of range.') 1726 return key 1727 elif isinstance(key, slice): 1728 return key 1729 elif isinstance(key, tuple): 1730 if (len(key) != 2 or not isinstance(key[0], str) or 1731 not isinstance(key[1], numbers.Integral)): 1732 raise ValueError( 1733 'Tuple indices must be 2-tuples consisting of a ' 1734 'keyword string and an integer index.') 1735 keyword, n = key 1736 else: 1737 raise ValueError( 1738 'Header indices must be either a string, a 2-tuple, or ' 1739 'an integer.') 1740 1741 keyword = Card.normalize_keyword(keyword) 1742 # Returns the index into _cards for the n-th card with the given 1743 # keyword (where n is 0-based) 1744 indices = self._keyword_indices.get(keyword, None) 1745 1746 if keyword and not indices: 1747 if len(keyword) > KEYWORD_LENGTH or '.' in keyword: 1748 raise KeyError(f"Keyword {keyword!r} not found.") 1749 else: 1750 # Maybe it's a RVKC? 1751 indices = self._rvkc_indices.get(keyword, None) 1752 1753 if not indices: 1754 raise KeyError(f"Keyword {keyword!r} not found.") 1755 1756 try: 1757 return indices[n] 1758 except IndexError: 1759 raise IndexError('There are only {} {!r} cards in the ' 1760 'header.'.format(len(indices), keyword)) 1761 1762 def _keyword_from_index(self, idx): 1763 """ 1764 Given an integer index, return the (keyword, repeat) tuple that index 1765 refers to. For most keywords the repeat will always be zero, but it 1766 may be greater than zero for keywords that are duplicated (especially 1767 commentary keywords). 1768 1769 In a sense this is the inverse of self.index, except that it also 1770 supports duplicates. 1771 """ 1772 1773 if idx < 0: 1774 idx += len(self._cards) 1775 1776 keyword = self._cards[idx].keyword 1777 keyword = Card.normalize_keyword(keyword) 1778 repeat = self._keyword_indices[keyword].index(idx) 1779 return keyword, repeat 1780 1781 def _relativeinsert(self, card, before=None, after=None, replace=False): 1782 """ 1783 Inserts a new card before or after an existing card; used to 1784 implement support for the legacy before/after keyword arguments to 1785 Header.update(). 1786 1787 If replace=True, move an existing card with the same keyword. 1788 """ 1789 1790 if before is None: 1791 insertionkey = after 1792 else: 1793 insertionkey = before 1794 1795 def get_insertion_idx(): 1796 if not (isinstance(insertionkey, numbers.Integral) and 1797 insertionkey >= len(self._cards)): 1798 idx = self._cardindex(insertionkey) 1799 else: 1800 idx = insertionkey 1801 1802 if before is None: 1803 idx += 1 1804 1805 return idx 1806 1807 if replace: 1808 # The card presumably already exists somewhere in the header. 1809 # Check whether or not we actually have to move it; if it does need 1810 # to be moved we just delete it and then it will be reinserted 1811 # below 1812 old_idx = self._cardindex(card.keyword) 1813 insertion_idx = get_insertion_idx() 1814 1815 if (insertion_idx >= len(self._cards) and 1816 old_idx == len(self._cards) - 1): 1817 # The card would be appended to the end, but it's already at 1818 # the end 1819 return 1820 1821 if before is not None: 1822 if old_idx == insertion_idx - 1: 1823 return 1824 elif after is not None and old_idx == insertion_idx: 1825 return 1826 1827 del self[old_idx] 1828 1829 # Even if replace=True, the insertion idx may have changed since the 1830 # old card was deleted 1831 idx = get_insertion_idx() 1832 1833 if card[0] in Card._commentary_keywords: 1834 cards = reversed(self._splitcommentary(card[0], card[1])) 1835 else: 1836 cards = [card] 1837 for c in cards: 1838 self.insert(idx, c) 1839 1840 def _updateindices(self, idx, increment=True): 1841 """ 1842 For all cards with index above idx, increment or decrement its index 1843 value in the keyword_indices dict. 1844 """ 1845 if idx > len(self._cards): 1846 # Save us some effort 1847 return 1848 1849 increment = 1 if increment else -1 1850 1851 for index_sets in (self._keyword_indices, self._rvkc_indices): 1852 for indices in index_sets.values(): 1853 for jdx, keyword_index in enumerate(indices): 1854 if keyword_index >= idx: 1855 indices[jdx] += increment 1856 1857 def _countblanks(self): 1858 """Returns the number of blank cards at the end of the Header.""" 1859 1860 for idx in range(1, len(self._cards)): 1861 if not self._cards[-idx].is_blank: 1862 return idx - 1 1863 return 0 1864 1865 def _useblanks(self, count): 1866 for _ in range(count): 1867 if self._cards[-1].is_blank: 1868 del self[-1] 1869 else: 1870 break 1871 1872 def _haswildcard(self, keyword): 1873 """Return `True` if the input keyword contains a wildcard pattern.""" 1874 1875 return (isinstance(keyword, str) and 1876 (keyword.endswith('...') or '*' in keyword or '?' in keyword)) 1877 1878 def _wildcardmatch(self, pattern): 1879 """ 1880 Returns a list of indices of the cards matching the given wildcard 1881 pattern. 1882 1883 * '*' matches 0 or more characters 1884 * '?' matches a single character 1885 * '...' matches 0 or more of any non-whitespace character 1886 """ 1887 1888 pattern = pattern.replace('*', r'.*').replace('?', r'.') 1889 pattern = pattern.replace('...', r'\S*') + '$' 1890 pattern_re = re.compile(pattern, re.I) 1891 1892 return [idx for idx, card in enumerate(self._cards) 1893 if pattern_re.match(card.keyword)] 1894 1895 def _set_slice(self, key, value, target): 1896 """ 1897 Used to implement Header.__setitem__ and CardAccessor.__setitem__. 1898 """ 1899 1900 if isinstance(key, slice) or self._haswildcard(key): 1901 if isinstance(key, slice): 1902 indices = range(*key.indices(len(target))) 1903 else: 1904 indices = self._wildcardmatch(key) 1905 1906 if isinstance(value, str) or not isiterable(value): 1907 value = itertools.repeat(value, len(indices)) 1908 1909 for idx, val in zip(indices, value): 1910 target[idx] = val 1911 1912 return True 1913 1914 return False 1915 1916 def _splitcommentary(self, keyword, value): 1917 """ 1918 Given a commentary keyword and value, returns a list of the one or more 1919 cards needed to represent the full value. This is primarily used to 1920 create the multiple commentary cards needed to represent a long value 1921 that won't fit into a single commentary card. 1922 """ 1923 1924 # The maximum value in each card can be the maximum card length minus 1925 # the maximum key length (which can include spaces if they key length 1926 # less than 8 1927 maxlen = Card.length - KEYWORD_LENGTH 1928 valuestr = str(value) 1929 1930 if len(valuestr) <= maxlen: 1931 # The value can fit in a single card 1932 cards = [Card(keyword, value)] 1933 else: 1934 # The value must be split across multiple consecutive commentary 1935 # cards 1936 idx = 0 1937 cards = [] 1938 while idx < len(valuestr): 1939 cards.append(Card(keyword, valuestr[idx:idx + maxlen])) 1940 idx += maxlen 1941 return cards 1942 1943 def _add_commentary(self, key, value, before=None, after=None): 1944 """ 1945 Add a commentary card. 1946 1947 If ``before`` and ``after`` are `None`, add to the last occurrence 1948 of cards of the same name (except blank card). If there is no 1949 card (or blank card), append at the end. 1950 """ 1951 1952 if before is not None or after is not None: 1953 self._relativeinsert((key, value), before=before, 1954 after=after) 1955 else: 1956 self[key] = value 1957 1958 1959collections.abc.MutableSequence.register(Header) 1960collections.abc.MutableMapping.register(Header) 1961 1962 1963class _DelayedHeader: 1964 """ 1965 Descriptor used to create the Header object from the header string that 1966 was stored in HDU._header_str when parsing the file. 1967 """ 1968 1969 def __get__(self, obj, owner=None): 1970 try: 1971 return obj.__dict__['_header'] 1972 except KeyError: 1973 if obj._header_str is not None: 1974 hdr = Header.fromstring(obj._header_str) 1975 obj._header_str = None 1976 else: 1977 raise AttributeError("'{}' object has no attribute '_header'" 1978 .format(obj.__class__.__name__)) 1979 1980 obj.__dict__['_header'] = hdr 1981 return hdr 1982 1983 def __set__(self, obj, val): 1984 obj.__dict__['_header'] = val 1985 1986 def __delete__(self, obj): 1987 del obj.__dict__['_header'] 1988 1989 1990class _BasicHeaderCards: 1991 """ 1992 This class allows to access cards with the _BasicHeader.cards attribute. 1993 1994 This is needed because during the HDU class detection, some HDUs uses 1995 the .cards interface. Cards cannot be modified here as the _BasicHeader 1996 object will be deleted once the HDU object is created. 1997 1998 """ 1999 2000 def __init__(self, header): 2001 self.header = header 2002 2003 def __getitem__(self, key): 2004 # .cards is a list of cards, so key here is an integer. 2005 # get the keyword name from its index. 2006 key = self.header._keys[key] 2007 # then we get the card from the _BasicHeader._cards list, or parse it 2008 # if needed. 2009 try: 2010 return self.header._cards[key] 2011 except KeyError: 2012 cardstr = self.header._raw_cards[key] 2013 card = Card.fromstring(cardstr) 2014 self.header._cards[key] = card 2015 return card 2016 2017 2018class _BasicHeader(collections.abc.Mapping): 2019 """This class provides a fast header parsing, without all the additional 2020 features of the Header class. Here only standard keywords are parsed, no 2021 support for CONTINUE, HIERARCH, COMMENT, HISTORY, or rvkc. 2022 2023 The raw card images are stored and parsed only if needed. The idea is that 2024 to create the HDU objects, only a small subset of standard cards is needed. 2025 Once a card is parsed, which is deferred to the Card class, the Card object 2026 is kept in a cache. This is useful because a small subset of cards is used 2027 a lot in the HDU creation process (NAXIS, XTENSION, ...). 2028 2029 """ 2030 2031 def __init__(self, cards): 2032 # dict of (keywords, card images) 2033 self._raw_cards = cards 2034 self._keys = list(cards.keys()) 2035 # dict of (keyword, Card object) storing the parsed cards 2036 self._cards = {} 2037 # the _BasicHeaderCards object allows to access Card objects from 2038 # keyword indices 2039 self.cards = _BasicHeaderCards(self) 2040 2041 self._modified = False 2042 2043 def __getitem__(self, key): 2044 if isinstance(key, numbers.Integral): 2045 key = self._keys[key] 2046 2047 try: 2048 return self._cards[key].value 2049 except KeyError: 2050 # parse the Card and store it 2051 cardstr = self._raw_cards[key] 2052 self._cards[key] = card = Card.fromstring(cardstr) 2053 return card.value 2054 2055 def __len__(self): 2056 return len(self._raw_cards) 2057 2058 def __iter__(self): 2059 return iter(self._raw_cards) 2060 2061 def index(self, keyword): 2062 return self._keys.index(keyword) 2063 2064 @classmethod 2065 def fromfile(cls, fileobj): 2066 """The main method to parse a FITS header from a file. The parsing is 2067 done with the parse_header function implemented in Cython.""" 2068 2069 close_file = False 2070 if isinstance(fileobj, str): 2071 fileobj = open(fileobj, 'rb') 2072 close_file = True 2073 2074 try: 2075 header_str, cards = parse_header(fileobj) 2076 _check_padding(header_str, BLOCK_SIZE, False) 2077 return header_str, cls(cards) 2078 finally: 2079 if close_file: 2080 fileobj.close() 2081 2082 2083class _CardAccessor: 2084 """ 2085 This is a generic class for wrapping a Header in such a way that you can 2086 use the header's slice/filtering capabilities to return a subset of cards 2087 and do something with them. 2088 2089 This is sort of the opposite notion of the old CardList class--whereas 2090 Header used to use CardList to get lists of cards, this uses Header to get 2091 lists of cards. 2092 """ 2093 2094 # TODO: Consider giving this dict/list methods like Header itself 2095 def __init__(self, header): 2096 self._header = header 2097 2098 def __repr__(self): 2099 return '\n'.join(repr(c) for c in self._header._cards) 2100 2101 def __len__(self): 2102 return len(self._header._cards) 2103 2104 def __iter__(self): 2105 return iter(self._header._cards) 2106 2107 def __eq__(self, other): 2108 # If the `other` item is a scalar we will still treat it as equal if 2109 # this _CardAccessor only contains one item 2110 if not isiterable(other) or isinstance(other, str): 2111 if len(self) == 1: 2112 other = [other] 2113 else: 2114 return False 2115 2116 for a, b in itertools.zip_longest(self, other): 2117 if a != b: 2118 return False 2119 else: 2120 return True 2121 2122 def __ne__(self, other): 2123 return not (self == other) 2124 2125 def __getitem__(self, item): 2126 if isinstance(item, slice) or self._header._haswildcard(item): 2127 return self.__class__(self._header[item]) 2128 2129 idx = self._header._cardindex(item) 2130 return self._header._cards[idx] 2131 2132 def _setslice(self, item, value): 2133 """ 2134 Helper for implementing __setitem__ on _CardAccessor subclasses; slices 2135 should always be handled in this same way. 2136 """ 2137 2138 if isinstance(item, slice) or self._header._haswildcard(item): 2139 if isinstance(item, slice): 2140 indices = range(*item.indices(len(self))) 2141 else: 2142 indices = self._header._wildcardmatch(item) 2143 if isinstance(value, str) or not isiterable(value): 2144 value = itertools.repeat(value, len(indices)) 2145 for idx, val in zip(indices, value): 2146 self[idx] = val 2147 return True 2148 return False 2149 2150 2151class _HeaderComments(_CardAccessor): 2152 """ 2153 A class used internally by the Header class for the Header.comments 2154 attribute access. 2155 2156 This object can be used to display all the keyword comments in the Header, 2157 or look up the comments on specific keywords. It allows all the same forms 2158 of keyword lookup as the Header class itself, but returns comments instead 2159 of values. 2160 """ 2161 2162 def __iter__(self): 2163 for card in self._header._cards: 2164 yield card.comment 2165 2166 def __repr__(self): 2167 """Returns a simple list of all keywords and their comments.""" 2168 2169 keyword_length = KEYWORD_LENGTH 2170 for card in self._header._cards: 2171 keyword_length = max(keyword_length, len(card.keyword)) 2172 return '\n'.join('{:>{len}} {}'.format(c.keyword, c.comment, 2173 len=keyword_length) 2174 for c in self._header._cards) 2175 2176 def __getitem__(self, item): 2177 """ 2178 Slices and filter strings return a new _HeaderComments containing the 2179 returned cards. Otherwise the comment of a single card is returned. 2180 """ 2181 2182 item = super().__getitem__(item) 2183 if isinstance(item, _HeaderComments): 2184 # The item key was a slice 2185 return item 2186 return item.comment 2187 2188 def __setitem__(self, item, comment): 2189 """ 2190 Set/update the comment on specified card or cards. 2191 2192 Slice/filter updates work similarly to how Header.__setitem__ works. 2193 """ 2194 2195 if self._header._set_slice(item, comment, self): 2196 return 2197 2198 # In this case, key/index errors should be raised; don't update 2199 # comments of nonexistent cards 2200 idx = self._header._cardindex(item) 2201 value = self._header[idx] 2202 self._header[idx] = (value, comment) 2203 2204 2205class _HeaderCommentaryCards(_CardAccessor): 2206 """ 2207 This is used to return a list-like sequence over all the values in the 2208 header for a given commentary keyword, such as HISTORY. 2209 """ 2210 2211 def __init__(self, header, keyword=''): 2212 super().__init__(header) 2213 self._keyword = keyword 2214 self._count = self._header.count(self._keyword) 2215 self._indices = slice(self._count).indices(self._count) 2216 2217 # __len__ and __iter__ need to be overridden from the base class due to the 2218 # different approach this class has to take for slicing 2219 def __len__(self): 2220 return len(range(*self._indices)) 2221 2222 def __iter__(self): 2223 for idx in range(*self._indices): 2224 yield self._header[(self._keyword, idx)] 2225 2226 def __repr__(self): 2227 return '\n'.join(str(x) for x in self) 2228 2229 def __getitem__(self, idx): 2230 if isinstance(idx, slice): 2231 n = self.__class__(self._header, self._keyword) 2232 n._indices = idx.indices(self._count) 2233 return n 2234 elif not isinstance(idx, numbers.Integral): 2235 raise ValueError(f'{self._keyword} index must be an integer') 2236 2237 idx = list(range(*self._indices))[idx] 2238 return self._header[(self._keyword, idx)] 2239 2240 def __setitem__(self, item, value): 2241 """ 2242 Set the value of a specified commentary card or cards. 2243 2244 Slice/filter updates work similarly to how Header.__setitem__ works. 2245 """ 2246 2247 if self._header._set_slice(item, value, self): 2248 return 2249 2250 # In this case, key/index errors should be raised; don't update 2251 # comments of nonexistent cards 2252 self._header[(self._keyword, item)] = value 2253 2254 2255def _block_size(sep): 2256 """ 2257 Determine the size of a FITS header block if a non-blank separator is used 2258 between cards. 2259 """ 2260 2261 return BLOCK_SIZE + (len(sep) * (BLOCK_SIZE // Card.length - 1)) 2262 2263 2264def _pad_length(stringlen): 2265 """Bytes needed to pad the input stringlen to the next FITS block.""" 2266 2267 return (BLOCK_SIZE - (stringlen % BLOCK_SIZE)) % BLOCK_SIZE 2268 2269 2270def _check_padding(header_str, block_size, is_eof, check_block_size=True): 2271 # Strip any zero-padding (see ticket #106) 2272 if header_str and header_str[-1] == '\0': 2273 if is_eof and header_str.strip('\0') == '': 2274 # TODO: Pass this warning to validation framework 2275 warnings.warn( 2276 'Unexpected extra padding at the end of the file. This ' 2277 'padding may not be preserved when saving changes.', 2278 AstropyUserWarning) 2279 raise EOFError() 2280 else: 2281 # Replace the illegal null bytes with spaces as required by 2282 # the FITS standard, and issue a nasty warning 2283 # TODO: Pass this warning to validation framework 2284 warnings.warn( 2285 'Header block contains null bytes instead of spaces for ' 2286 'padding, and is not FITS-compliant. Nulls may be ' 2287 'replaced with spaces upon writing.', AstropyUserWarning) 2288 header_str.replace('\0', ' ') 2289 2290 if check_block_size and (len(header_str) % block_size) != 0: 2291 # This error message ignores the length of the separator for 2292 # now, but maybe it shouldn't? 2293 actual_len = len(header_str) - block_size + BLOCK_SIZE 2294 # TODO: Pass this error to validation framework 2295 raise ValueError(f'Header size is not multiple of {BLOCK_SIZE}: {actual_len}') 2296