m2m模型翻译
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
9.3 KiB

6 months ago
  1. # Human friendly input/output in Python.
  2. #
  3. # Author: Peter Odding <peter@peterodding.com>
  4. # Last Change: March 2, 2020
  5. # URL: https://humanfriendly.readthedocs.io
  6. """
  7. Support for deprecation warnings when importing names from old locations.
  8. When software evolves, things tend to move around. This is usually detrimental
  9. to backwards compatibility (in Python this primarily manifests itself as
  10. :exc:`~exceptions.ImportError` exceptions).
  11. While backwards compatibility is very important, it should not get in the way
  12. of progress. It would be great to have the agility to move things around
  13. without breaking backwards compatibility.
  14. This is where the :mod:`humanfriendly.deprecation` module comes in: It enables
  15. the definition of backwards compatible aliases that emit a deprecation warning
  16. when they are accessed.
  17. The way it works is that it wraps the original module in an :class:`DeprecationProxy`
  18. object that defines a :func:`~DeprecationProxy.__getattr__()` special method to
  19. override attribute access of the module.
  20. """
  21. # Standard library modules.
  22. import collections
  23. import functools
  24. import importlib
  25. import inspect
  26. import sys
  27. import types
  28. import warnings
  29. # Modules included in our package.
  30. from humanfriendly.text import format
  31. # Registry of known aliases (used by humanfriendly.sphinx).
  32. REGISTRY = collections.defaultdict(dict)
  33. # Public identifiers that require documentation.
  34. __all__ = ("DeprecationProxy", "define_aliases", "deprecated_args", "get_aliases", "is_method")
  35. def define_aliases(module_name, **aliases):
  36. """
  37. Update a module with backwards compatible aliases.
  38. :param module_name: The ``__name__`` of the module (a string).
  39. :param aliases: Each keyword argument defines an alias. The values
  40. are expected to be "dotted paths" (strings).
  41. The behavior of this function depends on whether the Sphinx documentation
  42. generator is active, because the use of :class:`DeprecationProxy` to shadow the
  43. real module in :data:`sys.modules` has the unintended side effect of
  44. breaking autodoc support for ``:data:`` members (module variables).
  45. To avoid breaking Sphinx the proxy object is omitted and instead the
  46. aliased names are injected into the original module namespace, to make sure
  47. that imports can be satisfied when the documentation is being rendered.
  48. If you run into cyclic dependencies caused by :func:`define_aliases()` when
  49. running Sphinx, you can try moving the call to :func:`define_aliases()` to
  50. the bottom of the Python module you're working on.
  51. """
  52. module = sys.modules[module_name]
  53. proxy = DeprecationProxy(module, aliases)
  54. # Populate the registry of aliases.
  55. for name, target in aliases.items():
  56. REGISTRY[module.__name__][name] = target
  57. # Avoid confusing Sphinx.
  58. if "sphinx" in sys.modules:
  59. for name, target in aliases.items():
  60. setattr(module, name, proxy.resolve(target))
  61. else:
  62. # Install a proxy object to raise DeprecationWarning.
  63. sys.modules[module_name] = proxy
  64. def get_aliases(module_name):
  65. """
  66. Get the aliases defined by a module.
  67. :param module_name: The ``__name__`` of the module (a string).
  68. :returns: A dictionary with string keys and values:
  69. 1. Each key gives the name of an alias
  70. created for backwards compatibility.
  71. 2. Each value gives the dotted path of
  72. the proper location of the identifier.
  73. An empty dictionary is returned for modules that
  74. don't define any backwards compatible aliases.
  75. """
  76. return REGISTRY.get(module_name, {})
  77. def deprecated_args(*names):
  78. """
  79. Deprecate positional arguments without dropping backwards compatibility.
  80. :param names:
  81. The positional arguments to :func:`deprecated_args()` give the names of
  82. the positional arguments that the to-be-decorated function should warn
  83. about being deprecated and translate to keyword arguments.
  84. :returns: A decorator function specialized to `names`.
  85. The :func:`deprecated_args()` decorator function was created to make it
  86. easy to switch from positional arguments to keyword arguments [#]_ while
  87. preserving backwards compatibility [#]_ and informing call sites
  88. about the change.
  89. .. [#] Increased flexibility is the main reason why I find myself switching
  90. from positional arguments to (optional) keyword arguments as my code
  91. evolves to support more use cases.
  92. .. [#] In my experience positional argument order implicitly becomes part
  93. of API compatibility whether intended or not. While this makes sense
  94. for functions that over time adopt more and more optional arguments,
  95. at a certain point it becomes an inconvenience to code maintenance.
  96. Here's an example of how to use the decorator::
  97. @deprecated_args('text')
  98. def report_choice(**options):
  99. print(options['text'])
  100. When the decorated function is called with positional arguments
  101. a deprecation warning is given::
  102. >>> report_choice('this will give a deprecation warning')
  103. DeprecationWarning: report_choice has deprecated positional arguments, please switch to keyword arguments
  104. this will give a deprecation warning
  105. But when the function is called with keyword arguments no deprecation
  106. warning is emitted::
  107. >>> report_choice(text='this will not give a deprecation warning')
  108. this will not give a deprecation warning
  109. """
  110. def decorator(function):
  111. def translate(args, kw):
  112. # Raise TypeError when too many positional arguments are passed to the decorated function.
  113. if len(args) > len(names):
  114. raise TypeError(
  115. format(
  116. "{name} expected at most {limit} arguments, got {count}",
  117. name=function.__name__,
  118. limit=len(names),
  119. count=len(args),
  120. )
  121. )
  122. # Emit a deprecation warning when positional arguments are used.
  123. if args:
  124. warnings.warn(
  125. format(
  126. "{name} has deprecated positional arguments, please switch to keyword arguments",
  127. name=function.__name__,
  128. ),
  129. category=DeprecationWarning,
  130. stacklevel=3,
  131. )
  132. # Translate positional arguments to keyword arguments.
  133. for name, value in zip(names, args):
  134. kw[name] = value
  135. if is_method(function):
  136. @functools.wraps(function)
  137. def wrapper(*args, **kw):
  138. """Wrapper for instance methods."""
  139. args = list(args)
  140. self = args.pop(0)
  141. translate(args, kw)
  142. return function(self, **kw)
  143. else:
  144. @functools.wraps(function)
  145. def wrapper(*args, **kw):
  146. """Wrapper for module level functions."""
  147. translate(args, kw)
  148. return function(**kw)
  149. return wrapper
  150. return decorator
  151. def is_method(function):
  152. """Check if the expected usage of the given function is as an instance method."""
  153. try:
  154. # Python 3.3 and newer.
  155. signature = inspect.signature(function)
  156. return "self" in signature.parameters
  157. except AttributeError:
  158. # Python 3.2 and older.
  159. metadata = inspect.getargspec(function)
  160. return "self" in metadata.args
  161. class DeprecationProxy(types.ModuleType):
  162. """Emit deprecation warnings for imports that should be updated."""
  163. def __init__(self, module, aliases):
  164. """
  165. Initialize an :class:`DeprecationProxy` object.
  166. :param module: The original module object.
  167. :param aliases: A dictionary of aliases.
  168. """
  169. # Initialize our superclass.
  170. super(DeprecationProxy, self).__init__(name=module.__name__)
  171. # Store initializer arguments.
  172. self.module = module
  173. self.aliases = aliases
  174. def __getattr__(self, name):
  175. """
  176. Override module attribute lookup.
  177. :param name: The name to look up (a string).
  178. :returns: The attribute value.
  179. """
  180. # Check if the given name is an alias.
  181. target = self.aliases.get(name)
  182. if target is not None:
  183. # Emit the deprecation warning.
  184. warnings.warn(
  185. format("%s.%s was moved to %s, please update your imports", self.module.__name__, name, target),
  186. category=DeprecationWarning,
  187. stacklevel=2,
  188. )
  189. # Resolve the dotted path.
  190. return self.resolve(target)
  191. # Look up the name in the original module namespace.
  192. value = getattr(self.module, name, None)
  193. if value is not None:
  194. return value
  195. # Fall back to the default behavior.
  196. raise AttributeError(format("module '%s' has no attribute '%s'", self.module.__name__, name))
  197. def resolve(self, target):
  198. """
  199. Look up the target of an alias.
  200. :param target: The fully qualified dotted path (a string).
  201. :returns: The value of the given target.
  202. """
  203. module_name, _, member = target.rpartition(".")
  204. module = importlib.import_module(module_name)
  205. return getattr(module, member)