1# -*- coding: utf-8 -*- 2# Licensed under a 3-clause BSD style license - see LICENSE.rst 3""" 4A "grab bag" of relatively small general-purpose utilities that don't have 5a clear module/package to live in. 6""" 7 8import abc 9import contextlib 10import difflib 11import inspect 12import json 13import os 14import signal 15import sys 16import traceback 17import unicodedata 18import locale 19import threading 20import re 21 22from contextlib import contextmanager 23from collections import defaultdict, OrderedDict 24 25from astropy.utils.decorators import deprecated 26 27 28__all__ = ['isiterable', 'silence', 'format_exception', 'NumpyRNGContext', 29 'find_api_page', 'is_path_hidden', 'walk_skip_hidden', 30 'JsonCustomEncoder', 'indent', 'dtype_bytes_or_chars', 31 'OrderedDescriptor', 'OrderedDescriptorContainer'] 32 33 34# Because they are deprecated. 35__doctest_skip__ = ['OrderedDescriptor', 'OrderedDescriptorContainer'] 36 37 38NOT_OVERWRITING_MSG = ('File {} already exists. If you mean to replace it ' 39 'then use the argument "overwrite=True".') 40# A useful regex for tests. 41_NOT_OVERWRITING_MSG_MATCH = ('File .* already exists\. If you mean to ' 42 'replace it then use the argument ' 43 '"overwrite=True"\.') 44 45 46def isiterable(obj): 47 """Returns `True` if the given object is iterable.""" 48 49 try: 50 iter(obj) 51 return True 52 except TypeError: 53 return False 54 55 56def indent(s, shift=1, width=4): 57 """Indent a block of text. The indentation is applied to each line.""" 58 59 indented = '\n'.join(' ' * (width * shift) + l if l else '' 60 for l in s.splitlines()) 61 if s[-1] == '\n': 62 indented += '\n' 63 64 return indented 65 66 67class _DummyFile: 68 """A noop writeable object.""" 69 70 def write(self, s): 71 pass 72 73 74@contextlib.contextmanager 75def silence(): 76 """A context manager that silences sys.stdout and sys.stderr.""" 77 78 old_stdout = sys.stdout 79 old_stderr = sys.stderr 80 sys.stdout = _DummyFile() 81 sys.stderr = _DummyFile() 82 yield 83 sys.stdout = old_stdout 84 sys.stderr = old_stderr 85 86 87def format_exception(msg, *args, **kwargs): 88 """ 89 Given an exception message string, uses new-style formatting arguments 90 ``{filename}``, ``{lineno}``, ``{func}`` and/or ``{text}`` to fill in 91 information about the exception that occurred. For example: 92 93 try: 94 1/0 95 except: 96 raise ZeroDivisionError( 97 format_except('A divide by zero occurred in {filename} at ' 98 'line {lineno} of function {func}.')) 99 100 Any additional positional or keyword arguments passed to this function are 101 also used to format the message. 102 103 .. note:: 104 This uses `sys.exc_info` to gather up the information needed to fill 105 in the formatting arguments. Since `sys.exc_info` is not carried 106 outside a handled exception, it's not wise to use this 107 outside of an ``except`` clause - if it is, this will substitute 108 '<unknown>' for the 4 formatting arguments. 109 """ 110 111 tb = traceback.extract_tb(sys.exc_info()[2], limit=1) 112 if len(tb) > 0: 113 filename, lineno, func, text = tb[0] 114 else: 115 filename = lineno = func = text = '<unknown>' 116 117 return msg.format(*args, filename=filename, lineno=lineno, func=func, 118 text=text, **kwargs) 119 120 121class NumpyRNGContext: 122 """ 123 A context manager (for use with the ``with`` statement) that will seed the 124 numpy random number generator (RNG) to a specific value, and then restore 125 the RNG state back to whatever it was before. 126 127 This is primarily intended for use in the astropy testing suit, but it 128 may be useful in ensuring reproducibility of Monte Carlo simulations in a 129 science context. 130 131 Parameters 132 ---------- 133 seed : int 134 The value to use to seed the numpy RNG 135 136 Examples 137 -------- 138 A typical use case might be:: 139 140 with NumpyRNGContext(<some seed value you pick>): 141 from numpy import random 142 143 randarr = random.randn(100) 144 ... run your test using `randarr` ... 145 146 #Any code using numpy.random at this indent level will act just as it 147 #would have if it had been before the with statement - e.g. whatever 148 #the default seed is. 149 150 151 """ 152 153 def __init__(self, seed): 154 self.seed = seed 155 156 def __enter__(self): 157 from numpy import random 158 159 self.startstate = random.get_state() 160 random.seed(self.seed) 161 162 def __exit__(self, exc_type, exc_value, traceback): 163 from numpy import random 164 165 random.set_state(self.startstate) 166 167 168def find_api_page(obj, version=None, openinbrowser=True, timeout=None): 169 """ 170 Determines the URL of the API page for the specified object, and 171 optionally open that page in a web browser. 172 173 .. note:: 174 You must be connected to the internet for this to function even if 175 ``openinbrowser`` is `False`, unless you provide a local version of 176 the documentation to ``version`` (e.g., ``file:///path/to/docs``). 177 178 Parameters 179 ---------- 180 obj 181 The object to open the docs for or its fully-qualified name 182 (as a str). 183 version : str 184 The doc version - either a version number like '0.1', 'dev' for 185 the development/latest docs, or a URL to point to a specific 186 location that should be the *base* of the documentation. Defaults to 187 latest if you are on aren't on a release, otherwise, the version you 188 are on. 189 openinbrowser : bool 190 If `True`, the `webbrowser` package will be used to open the doc 191 page in a new web browser window. 192 timeout : number, optional 193 The number of seconds to wait before timing-out the query to 194 the astropy documentation. If not given, the default python 195 stdlib timeout will be used. 196 197 Returns 198 ------- 199 url : str 200 The loaded URL 201 202 Raises 203 ------ 204 ValueError 205 If the documentation can't be found 206 207 """ 208 import webbrowser 209 from zlib import decompress 210 from astropy.utils.data import get_readable_fileobj 211 212 if (not isinstance(obj, str) and 213 hasattr(obj, '__module__') and 214 hasattr(obj, '__name__')): 215 obj = obj.__module__ + '.' + obj.__name__ 216 elif inspect.ismodule(obj): 217 obj = obj.__name__ 218 219 if version is None: 220 from astropy import version 221 222 if version.release: 223 version = 'v' + version.version 224 else: 225 version = 'dev' 226 227 if '://' in version: 228 if version.endswith('index.html'): 229 baseurl = version[:-10] 230 elif version.endswith('/'): 231 baseurl = version 232 else: 233 baseurl = version + '/' 234 elif version == 'dev' or version == 'latest': 235 baseurl = 'http://devdocs.astropy.org/' 236 else: 237 baseurl = f'https://docs.astropy.org/en/{version}/' 238 239 # Custom request headers; see 240 # https://github.com/astropy/astropy/issues/8990 241 url = baseurl + 'objects.inv' 242 headers = {'User-Agent': f'Astropy/{version}'} 243 with get_readable_fileobj(url, encoding='binary', remote_timeout=timeout, 244 http_headers=headers) as uf: 245 oiread = uf.read() 246 247 # need to first read/remove the first four lines, which have info before 248 # the compressed section with the actual object inventory 249 idx = -1 250 headerlines = [] 251 for _ in range(4): 252 oldidx = idx 253 idx = oiread.index(b'\n', oldidx + 1) 254 headerlines.append(oiread[(oldidx+1):idx].decode('utf-8')) 255 256 # intersphinx version line, project name, and project version 257 ivers, proj, vers, compr = headerlines 258 if 'The remainder of this file is compressed using zlib' not in compr: 259 raise ValueError('The file downloaded from {} does not seem to be' 260 'the usual Sphinx objects.inv format. Maybe it ' 261 'has changed?'.format(baseurl + 'objects.inv')) 262 263 compressed = oiread[(idx+1):] 264 265 decompressed = decompress(compressed).decode('utf-8') 266 267 resurl = None 268 269 for l in decompressed.strip().splitlines(): 270 ls = l.split() 271 name = ls[0] 272 loc = ls[3] 273 if loc.endswith('$'): 274 loc = loc[:-1] + name 275 276 if name == obj: 277 resurl = baseurl + loc 278 break 279 280 if resurl is None: 281 raise ValueError(f'Could not find the docs for the object {obj}') 282 elif openinbrowser: 283 webbrowser.open(resurl) 284 285 return resurl 286 287 288def signal_number_to_name(signum): 289 """ 290 Given an OS signal number, returns a signal name. If the signal 291 number is unknown, returns ``'UNKNOWN'``. 292 """ 293 # Since these numbers and names are platform specific, we use the 294 # builtin signal module and build a reverse mapping. 295 296 signal_to_name_map = dict((k, v) for v, k in signal.__dict__.items() 297 if v.startswith('SIG')) 298 299 return signal_to_name_map.get(signum, 'UNKNOWN') 300 301 302if sys.platform == 'win32': 303 import ctypes 304 305 def _has_hidden_attribute(filepath): 306 """ 307 Returns True if the given filepath has the hidden attribute on 308 MS-Windows. Based on a post here: 309 https://stackoverflow.com/questions/284115/cross-platform-hidden-file-detection 310 """ 311 if isinstance(filepath, bytes): 312 filepath = filepath.decode(sys.getfilesystemencoding()) 313 try: 314 attrs = ctypes.windll.kernel32.GetFileAttributesW(filepath) 315 result = bool(attrs & 2) and attrs != -1 316 except AttributeError: 317 result = False 318 return result 319else: 320 def _has_hidden_attribute(filepath): 321 return False 322 323 324def is_path_hidden(filepath): 325 """ 326 Determines if a given file or directory is hidden. 327 328 Parameters 329 ---------- 330 filepath : str 331 The path to a file or directory 332 333 Returns 334 ------- 335 hidden : bool 336 Returns `True` if the file is hidden 337 """ 338 name = os.path.basename(os.path.abspath(filepath)) 339 if isinstance(name, bytes): 340 is_dotted = name.startswith(b'.') 341 else: 342 is_dotted = name.startswith('.') 343 return is_dotted or _has_hidden_attribute(filepath) 344 345 346def walk_skip_hidden(top, onerror=None, followlinks=False): 347 """ 348 A wrapper for `os.walk` that skips hidden files and directories. 349 350 This function does not have the parameter ``topdown`` from 351 `os.walk`: the directories must always be recursed top-down when 352 using this function. 353 354 See also 355 -------- 356 os.walk : For a description of the parameters 357 """ 358 for root, dirs, files in os.walk( 359 top, topdown=True, onerror=onerror, 360 followlinks=followlinks): 361 # These lists must be updated in-place so os.walk will skip 362 # hidden directories 363 dirs[:] = [d for d in dirs if not is_path_hidden(d)] 364 files[:] = [f for f in files if not is_path_hidden(f)] 365 yield root, dirs, files 366 367 368class JsonCustomEncoder(json.JSONEncoder): 369 """Support for data types that JSON default encoder 370 does not do. 371 372 This includes: 373 374 * Numpy array or number 375 * Complex number 376 * Set 377 * Bytes 378 * astropy.UnitBase 379 * astropy.Quantity 380 381 Examples 382 -------- 383 >>> import json 384 >>> import numpy as np 385 >>> from astropy.utils.misc import JsonCustomEncoder 386 >>> json.dumps(np.arange(3), cls=JsonCustomEncoder) 387 '[0, 1, 2]' 388 389 """ 390 391 def default(self, obj): 392 from astropy import units as u 393 import numpy as np 394 if isinstance(obj, u.Quantity): 395 return dict(value=obj.value, unit=obj.unit.to_string()) 396 if isinstance(obj, (np.number, np.ndarray)): 397 return obj.tolist() 398 elif isinstance(obj, complex): 399 return [obj.real, obj.imag] 400 elif isinstance(obj, set): 401 return list(obj) 402 elif isinstance(obj, bytes): # pragma: py3 403 return obj.decode() 404 elif isinstance(obj, (u.UnitBase, u.FunctionUnitBase)): 405 if obj == u.dimensionless_unscaled: 406 obj = 'dimensionless_unit' 407 else: 408 return obj.to_string() 409 410 return json.JSONEncoder.default(self, obj) 411 412 413def strip_accents(s): 414 """ 415 Remove accents from a Unicode string. 416 417 This helps with matching "ångström" to "angstrom", for example. 418 """ 419 return ''.join( 420 c for c in unicodedata.normalize('NFD', s) 421 if unicodedata.category(c) != 'Mn') 422 423 424def did_you_mean(s, candidates, n=3, cutoff=0.8, fix=None): 425 """ 426 When a string isn't found in a set of candidates, we can be nice 427 to provide a list of alternatives in the exception. This 428 convenience function helps to format that part of the exception. 429 430 Parameters 431 ---------- 432 s : str 433 434 candidates : sequence of str or dict of str keys 435 436 n : int 437 The maximum number of results to include. See 438 `difflib.get_close_matches`. 439 440 cutoff : float 441 In the range [0, 1]. Possibilities that don't score at least 442 that similar to word are ignored. See 443 `difflib.get_close_matches`. 444 445 fix : callable 446 A callable to modify the results after matching. It should 447 take a single string and return a sequence of strings 448 containing the fixed matches. 449 450 Returns 451 ------- 452 message : str 453 Returns the string "Did you mean X, Y, or Z?", or the empty 454 string if no alternatives were found. 455 """ 456 if isinstance(s, str): 457 s = strip_accents(s) 458 s_lower = s.lower() 459 460 # Create a mapping from the lower case name to all capitalization 461 # variants of that name. 462 candidates_lower = {} 463 for candidate in candidates: 464 candidate_lower = candidate.lower() 465 candidates_lower.setdefault(candidate_lower, []) 466 candidates_lower[candidate_lower].append(candidate) 467 468 # The heuristic here is to first try "singularizing" the word. If 469 # that doesn't match anything use difflib to find close matches in 470 # original, lower and upper case. 471 if s_lower.endswith('s') and s_lower[:-1] in candidates_lower: 472 matches = [s_lower[:-1]] 473 else: 474 matches = difflib.get_close_matches( 475 s_lower, candidates_lower, n=n, cutoff=cutoff) 476 477 if len(matches): 478 capitalized_matches = set() 479 for match in matches: 480 capitalized_matches.update(candidates_lower[match]) 481 matches = capitalized_matches 482 483 if fix is not None: 484 mapped_matches = [] 485 for match in matches: 486 mapped_matches.extend(fix(match)) 487 matches = mapped_matches 488 489 matches = list(set(matches)) 490 matches = sorted(matches) 491 492 if len(matches) == 1: 493 matches = matches[0] 494 else: 495 matches = (', '.join(matches[:-1]) + ' or ' + 496 matches[-1]) 497 return f'Did you mean {matches}?' 498 499 return '' 500 501 502_ordered_descriptor_deprecation_message = """\ 503The {func} {obj_type} is deprecated and may be removed in a future version. 504 505 You can replace its functionality with a combination of the 506 __init_subclass__ and __set_name__ magic methods introduced in Python 3.6. 507 See https://github.com/astropy/astropy/issues/11094 for recipes on how to 508 replicate their functionality. 509""" 510 511 512@deprecated('4.3', _ordered_descriptor_deprecation_message) 513class OrderedDescriptor(metaclass=abc.ABCMeta): 514 """ 515 Base class for descriptors whose order in the class body should be 516 preserved. Intended for use in concert with the 517 `OrderedDescriptorContainer` metaclass. 518 519 Subclasses of `OrderedDescriptor` must define a value for a class attribute 520 called ``_class_attribute_``. This is the name of a class attribute on the 521 *container* class for these descriptors, which will be set to an 522 `~collections.OrderedDict` at class creation time. This 523 `~collections.OrderedDict` will contain a mapping of all class attributes 524 that were assigned instances of the `OrderedDescriptor` subclass, to the 525 instances themselves. See the documentation for 526 `OrderedDescriptorContainer` for a concrete example. 527 528 Optionally, subclasses of `OrderedDescriptor` may define a value for a 529 class attribute called ``_name_attribute_``. This should be the name of 530 an attribute on instances of the subclass. When specified, during 531 creation of a class containing these descriptors, the name attribute on 532 each instance will be set to the name of the class attribute it was 533 assigned to on the class. 534 535 .. note:: 536 537 Although this class is intended for use with *descriptors* (i.e. 538 classes that define any of the ``__get__``, ``__set__``, or 539 ``__delete__`` magic methods), this base class is not itself a 540 descriptor, and technically this could be used for classes that are 541 not descriptors too. However, use with descriptors is the original 542 intended purpose. 543 """ 544 545 # This id increments for each OrderedDescriptor instance created, so they 546 # are always ordered in the order they were created. Class bodies are 547 # guaranteed to be executed from top to bottom. Not sure if this is 548 # thread-safe though. 549 _nextid = 1 550 551 @property 552 @abc.abstractmethod 553 def _class_attribute_(self): 554 """ 555 Subclasses should define this attribute to the name of an attribute on 556 classes containing this subclass. That attribute will contain the mapping 557 of all instances of that `OrderedDescriptor` subclass defined in the class 558 body. If the same descriptor needs to be used with different classes, 559 each with different names of this attribute, multiple subclasses will be 560 needed. 561 """ 562 563 _name_attribute_ = None 564 """ 565 Subclasses may optionally define this attribute to specify the name of an 566 attribute on instances of the class that should be filled with the 567 instance's attribute name at class creation time. 568 """ 569 570 def __init__(self, *args, **kwargs): 571 # The _nextid attribute is shared across all subclasses so that 572 # different subclasses of OrderedDescriptors can be sorted correctly 573 # between themselves 574 self.__order = OrderedDescriptor._nextid 575 OrderedDescriptor._nextid += 1 576 super().__init__() 577 578 def __lt__(self, other): 579 """ 580 Defined for convenient sorting of `OrderedDescriptor` instances, which 581 are defined to sort in their creation order. 582 """ 583 584 if (isinstance(self, OrderedDescriptor) and 585 isinstance(other, OrderedDescriptor)): 586 try: 587 return self.__order < other.__order 588 except AttributeError: 589 raise RuntimeError( 590 'Could not determine ordering for {} and {}; at least ' 591 'one of them is not calling super().__init__ in its ' 592 '__init__.'.format(self, other)) 593 else: 594 return NotImplemented 595 596 597@deprecated('4.3', _ordered_descriptor_deprecation_message) 598class OrderedDescriptorContainer(type): 599 """ 600 Classes should use this metaclass if they wish to use `OrderedDescriptor` 601 attributes, which are class attributes that "remember" the order in which 602 they were defined in the class body. 603 604 Every subclass of `OrderedDescriptor` has an attribute called 605 ``_class_attribute_``. For example, if we have 606 607 .. code:: python 608 609 class ExampleDecorator(OrderedDescriptor): 610 _class_attribute_ = '_examples_' 611 612 Then when a class with the `OrderedDescriptorContainer` metaclass is 613 created, it will automatically be assigned a class attribute ``_examples_`` 614 referencing an `~collections.OrderedDict` containing all instances of 615 ``ExampleDecorator`` defined in the class body, mapped to by the names of 616 the attributes they were assigned to. 617 618 When subclassing a class with this metaclass, the descriptor dict (i.e. 619 ``_examples_`` in the above example) will *not* contain descriptors 620 inherited from the base class. That is, this only works by default with 621 decorators explicitly defined in the class body. However, the subclass 622 *may* define an attribute ``_inherit_decorators_`` which lists 623 `OrderedDescriptor` classes that *should* be added from base classes. 624 See the examples section below for an example of this. 625 626 Examples 627 -------- 628 629 >>> from astropy.utils import OrderedDescriptor, OrderedDescriptorContainer 630 >>> class TypedAttribute(OrderedDescriptor): 631 ... \"\"\" 632 ... Attributes that may only be assigned objects of a specific type, 633 ... or subclasses thereof. For some reason we care about their order. 634 ... \"\"\" 635 ... 636 ... _class_attribute_ = 'typed_attributes' 637 ... _name_attribute_ = 'name' 638 ... # A default name so that instances not attached to a class can 639 ... # still be repr'd; useful for debugging 640 ... name = '<unbound>' 641 ... 642 ... def __init__(self, type): 643 ... # Make sure not to forget to call the super __init__ 644 ... super().__init__() 645 ... self.type = type 646 ... 647 ... def __get__(self, obj, objtype=None): 648 ... if obj is None: 649 ... return self 650 ... if self.name in obj.__dict__: 651 ... return obj.__dict__[self.name] 652 ... else: 653 ... raise AttributeError(self.name) 654 ... 655 ... def __set__(self, obj, value): 656 ... if not isinstance(value, self.type): 657 ... raise ValueError('{0}.{1} must be of type {2!r}'.format( 658 ... obj.__class__.__name__, self.name, self.type)) 659 ... obj.__dict__[self.name] = value 660 ... 661 ... def __delete__(self, obj): 662 ... if self.name in obj.__dict__: 663 ... del obj.__dict__[self.name] 664 ... else: 665 ... raise AttributeError(self.name) 666 ... 667 ... def __repr__(self): 668 ... if isinstance(self.type, tuple) and len(self.type) > 1: 669 ... typestr = '({0})'.format( 670 ... ', '.join(t.__name__ for t in self.type)) 671 ... else: 672 ... typestr = self.type.__name__ 673 ... return '<{0}(name={1}, type={2})>'.format( 674 ... self.__class__.__name__, self.name, typestr) 675 ... 676 677 Now let's create an example class that uses this ``TypedAttribute``:: 678 679 >>> class Point2D(metaclass=OrderedDescriptorContainer): 680 ... x = TypedAttribute((float, int)) 681 ... y = TypedAttribute((float, int)) 682 ... 683 ... def __init__(self, x, y): 684 ... self.x, self.y = x, y 685 ... 686 >>> p1 = Point2D(1.0, 2.0) 687 >>> p1.x 688 1.0 689 >>> p1.y 690 2.0 691 >>> p2 = Point2D('a', 'b') # doctest: +IGNORE_EXCEPTION_DETAIL 692 Traceback (most recent call last): 693 ... 694 ValueError: Point2D.x must be of type (float, int>) 695 696 We see that ``TypedAttribute`` works more or less as advertised, but 697 there's nothing special about that. Let's see what 698 `OrderedDescriptorContainer` did for us:: 699 700 >>> Point2D.typed_attributes 701 OrderedDict([('x', <TypedAttribute(name=x, type=(float, int))>), 702 ('y', <TypedAttribute(name=y, type=(float, int))>)]) 703 704 If we create a subclass, it does *not* by default add inherited descriptors 705 to ``typed_attributes``:: 706 707 >>> class Point3D(Point2D): 708 ... z = TypedAttribute((float, int)) 709 ... 710 >>> Point3D.typed_attributes 711 OrderedDict([('z', <TypedAttribute(name=z, type=(float, int))>)]) 712 713 However, if we specify ``_inherit_descriptors_`` from ``Point2D`` then 714 it will do so:: 715 716 >>> class Point3D(Point2D): 717 ... _inherit_descriptors_ = (TypedAttribute,) 718 ... z = TypedAttribute((float, int)) 719 ... 720 >>> Point3D.typed_attributes 721 OrderedDict([('x', <TypedAttribute(name=x, type=(float, int))>), 722 ('y', <TypedAttribute(name=y, type=(float, int))>), 723 ('z', <TypedAttribute(name=z, type=(float, int))>)]) 724 725 .. note:: 726 727 Hopefully it is clear from these examples that this construction 728 also allows a class of type `OrderedDescriptorContainer` to use 729 multiple different `OrderedDescriptor` classes simultaneously. 730 """ 731 732 _inherit_descriptors_ = () 733 734 def __init__(cls, cls_name, bases, members): 735 descriptors = defaultdict(list) 736 seen = set() 737 inherit_descriptors = () 738 descr_bases = {} 739 740 for mro_cls in cls.__mro__: 741 for name, obj in mro_cls.__dict__.items(): 742 if name in seen: 743 # Checks if we've already seen an attribute of the given 744 # name (if so it will override anything of the same name in 745 # any base class) 746 continue 747 748 seen.add(name) 749 750 if (not isinstance(obj, OrderedDescriptor) or 751 (inherit_descriptors and 752 not isinstance(obj, inherit_descriptors))): 753 # The second condition applies when checking any 754 # subclasses, to see if we can inherit any descriptors of 755 # the given type from subclasses (by default inheritance is 756 # disabled unless the class has _inherit_descriptors_ 757 # defined) 758 continue 759 760 if obj._name_attribute_ is not None: 761 setattr(obj, obj._name_attribute_, name) 762 763 # Don't just use the descriptor's class directly; instead go 764 # through its MRO and find the class on which _class_attribute_ 765 # is defined directly. This way subclasses of some 766 # OrderedDescriptor *may* override _class_attribute_ and have 767 # its own _class_attribute_, but by default all subclasses of 768 # some OrderedDescriptor are still grouped together 769 # TODO: It might be worth clarifying this in the docs 770 if obj.__class__ not in descr_bases: 771 for obj_cls_base in obj.__class__.__mro__: 772 if '_class_attribute_' in obj_cls_base.__dict__: 773 descr_bases[obj.__class__] = obj_cls_base 774 descriptors[obj_cls_base].append((obj, name)) 775 break 776 else: 777 # Make sure to put obj first for sorting purposes 778 obj_cls_base = descr_bases[obj.__class__] 779 descriptors[obj_cls_base].append((obj, name)) 780 781 if not getattr(mro_cls, '_inherit_descriptors_', False): 782 # If _inherit_descriptors_ is undefined then we don't inherit 783 # any OrderedDescriptors from any of the base classes, and 784 # there's no reason to continue through the MRO 785 break 786 else: 787 inherit_descriptors = mro_cls._inherit_descriptors_ 788 789 for descriptor_cls, instances in descriptors.items(): 790 instances.sort() 791 instances = OrderedDict((key, value) for value, key in instances) 792 setattr(cls, descriptor_cls._class_attribute_, instances) 793 794 super(OrderedDescriptorContainer, cls).__init__(cls_name, bases, 795 members) 796 797 798LOCALE_LOCK = threading.Lock() 799 800 801@contextmanager 802def _set_locale(name): 803 """ 804 Context manager to temporarily set the locale to ``name``. 805 806 An example is setting locale to "C" so that the C strtod() 807 function will use "." as the decimal point to enable consistent 808 numerical string parsing. 809 810 Note that one cannot nest multiple _set_locale() context manager 811 statements as this causes a threading lock. 812 813 This code taken from https://stackoverflow.com/questions/18593661/how-do-i-strftime-a-date-object-in-a-different-locale. 814 815 Parameters 816 ========== 817 name : str 818 Locale name, e.g. "C" or "fr_FR". 819 """ 820 name = str(name) 821 822 with LOCALE_LOCK: 823 saved = locale.setlocale(locale.LC_ALL) 824 if saved == name: 825 # Don't do anything if locale is already the requested locale 826 yield 827 else: 828 try: 829 locale.setlocale(locale.LC_ALL, name) 830 yield 831 finally: 832 locale.setlocale(locale.LC_ALL, saved) 833 834 835set_locale = deprecated('4.0')(_set_locale) 836set_locale.__doc__ = """Deprecated version of :func:`_set_locale` above. 837See https://github.com/astropy/astropy/issues/9196 838""" 839 840 841def dtype_bytes_or_chars(dtype): 842 """ 843 Parse the number out of a dtype.str value like '<U5' or '<f8'. 844 845 See #5819 for discussion on the need for this function for getting 846 the number of characters corresponding to a string dtype. 847 848 Parameters 849 ---------- 850 dtype : numpy dtype object 851 Input dtype 852 853 Returns 854 ------- 855 bytes_or_chars : int or None 856 Bits (for numeric types) or characters (for string types) 857 """ 858 match = re.search(r'(\d+)$', dtype.str) 859 out = int(match.group(1)) if match else None 860 return out 861 862 863def _hungry_for(option): # pragma: no cover 864 """ 865 Open browser loaded with ``option`` options near you. 866 867 *Disclaimers: Payments not included. Astropy is not 868 responsible for any liability from using this function.* 869 870 .. note:: Accuracy depends on your browser settings. 871 872 """ 873 import webbrowser 874 webbrowser.open(f'https://www.google.com/search?q={option}+near+me') 875 876 877def pizza(): # pragma: no cover 878 """``/pizza``""" 879 _hungry_for('pizza') 880 881 882def coffee(is_adam=False, is_brigitta=False): # pragma: no cover 883 """``/coffee``""" 884 if is_adam and is_brigitta: 885 raise ValueError('There can be only one!') 886 if is_adam: 887 option = 'fresh+third+wave+coffee' 888 elif is_brigitta: 889 option = 'decent+espresso' 890 else: 891 option = 'coffee' 892 _hungry_for(option) 893