descriptions.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. from __future__ import annotations
  2. import inspect
  3. import re
  4. import types
  5. from typing import Any
  6. def describe(
  7. article: str | None,
  8. value: Any,
  9. name: str | None = None,
  10. verbose: bool = False,
  11. capital: bool = False,
  12. ) -> str:
  13. """Return string that describes a value
  14. Parameters
  15. ----------
  16. article : str or None
  17. A definite or indefinite article. If the article is
  18. indefinite (i.e. "a" or "an") the appropriate one
  19. will be inferred. Thus, the arguments of ``describe``
  20. can themselves represent what the resulting string
  21. will actually look like. If None, then no article
  22. will be prepended to the result. For non-articled
  23. description, values that are instances are treated
  24. definitely, while classes are handled indefinitely.
  25. value : any
  26. The value which will be named.
  27. name : str or None (default: None)
  28. Only applies when ``article`` is "the" - this
  29. ``name`` is a definite reference to the value.
  30. By default one will be inferred from the value's
  31. type and repr methods.
  32. verbose : bool (default: False)
  33. Whether the name should be concise or verbose. When
  34. possible, verbose names include the module, and/or
  35. class name where an object was defined.
  36. capital : bool (default: False)
  37. Whether the first letter of the article should
  38. be capitalized or not. By default it is not.
  39. Examples
  40. --------
  41. Indefinite description:
  42. >>> describe("a", object())
  43. 'an object'
  44. >>> describe("a", object)
  45. 'an object'
  46. >>> describe("a", type(object))
  47. 'a type'
  48. Definite description:
  49. >>> describe("the", object())
  50. "the object at '...'"
  51. >>> describe("the", object)
  52. 'the object object'
  53. >>> describe("the", type(object))
  54. 'the type type'
  55. Definitely named description:
  56. >>> describe("the", object(), "I made")
  57. 'the object I made'
  58. >>> describe("the", object, "I will use")
  59. 'the object I will use'
  60. """
  61. if isinstance(article, str):
  62. article = article.lower()
  63. if not inspect.isclass(value):
  64. typename = type(value).__name__
  65. else:
  66. typename = value.__name__
  67. if verbose:
  68. typename = _prefix(value) + typename
  69. if article == "the" or (article is None and not inspect.isclass(value)):
  70. if name is not None:
  71. result = f"{typename} {name}"
  72. if article is not None:
  73. return add_article(result, True, capital)
  74. else:
  75. return result
  76. else:
  77. tick_wrap = False
  78. if inspect.isclass(value):
  79. name = value.__name__
  80. elif isinstance(value, types.FunctionType):
  81. name = value.__name__
  82. tick_wrap = True
  83. elif isinstance(value, types.MethodType):
  84. name = value.__func__.__name__
  85. tick_wrap = True
  86. elif type(value).__repr__ in (
  87. object.__repr__,
  88. type.__repr__,
  89. ): # type:ignore[comparison-overlap]
  90. name = "at '%s'" % hex(id(value))
  91. verbose = False
  92. else:
  93. name = repr(value)
  94. verbose = False
  95. if verbose:
  96. name = _prefix(value) + name
  97. if tick_wrap:
  98. name = name.join("''")
  99. return describe(article, value, name=name, verbose=verbose, capital=capital)
  100. elif article in ("a", "an") or article is None:
  101. if article is None:
  102. return typename
  103. return add_article(typename, False, capital)
  104. else:
  105. raise ValueError(
  106. "The 'article' argument should be 'the', 'a', 'an', or None not %r" % article
  107. )
  108. def _prefix(value: Any) -> str:
  109. if isinstance(value, types.MethodType):
  110. name = describe(None, value.__self__, verbose=True) + "."
  111. else:
  112. module = inspect.getmodule(value)
  113. if module is not None and module.__name__ != "builtins":
  114. name = module.__name__ + "."
  115. else:
  116. name = ""
  117. return name
  118. def class_of(value: Any) -> Any:
  119. """Returns a string of the value's type with an indefinite article.
  120. For example 'an Image' or 'a PlotValue'.
  121. """
  122. if inspect.isclass(value):
  123. return add_article(value.__name__)
  124. else:
  125. return class_of(type(value))
  126. def add_article(name: str, definite: bool = False, capital: bool = False) -> str:
  127. """Returns the string with a prepended article.
  128. The input does not need to begin with a character.
  129. Parameters
  130. ----------
  131. name : str
  132. Name to which to prepend an article
  133. definite : bool (default: False)
  134. Whether the article is definite or not.
  135. Indefinite articles being 'a' and 'an',
  136. while 'the' is definite.
  137. capital : bool (default: False)
  138. Whether the added article should have
  139. its first letter capitalized or not.
  140. """
  141. if definite:
  142. result = "the " + name
  143. else:
  144. first_letters = re.compile(r"[\W_]+").sub("", name)
  145. if first_letters[:1].lower() in "aeiou":
  146. result = "an " + name
  147. else:
  148. result = "a " + name
  149. if capital:
  150. return result[0].upper() + result[1:]
  151. else:
  152. return result
  153. def repr_type(obj: Any) -> str:
  154. """Return a string representation of a value and its type for readable
  155. error messages.
  156. """
  157. the_type = type(obj)
  158. return f"{obj!r} {the_type!r}"