codekingpro/portable-devtools
115k
1"""2Monkey patching of distutils.3"""4 5from __future__ import annotations6 7import inspect8import platform9import sys10import types11from typing import TypeVar, cast, overload12 13import distutils.filelist14 15_T = TypeVar("_T")16_UnpatchT = TypeVar("_UnpatchT", type, types.FunctionType)17 18 19__all__: list[str] = []20"""21Everything is private. Contact the project team22if you think you need this functionality.23"""24 25 26def _get_mro(cls):27 """28 Returns the bases classes for cls sorted by the MRO.29 30 Works around an issue on Jython where inspect.getmro will not return all31 base classes if multiple classes share the same name. Instead, this32 function will return a tuple containing the class itself, and the contents33 of cls.__bases__. See https://github.com/pypa/setuptools/issues/1024.34 """35 if platform.python_implementation() == "Jython":36 return (cls,) + cls.__bases__37 return inspect.getmro(cls)38 39 40@overload41def get_unpatched(item: _UnpatchT) -> _UnpatchT: ...42@overload43def get_unpatched(item: object) -> None: ...44def get_unpatched(45 item: type | types.FunctionType | object,46) -> type | types.FunctionType | None:47 if isinstance(item, type):48 return get_unpatched_class(item)49 if isinstance(item, types.FunctionType):50 return get_unpatched_function(item)51 return None52 53 54def get_unpatched_class(cls: type[_T]) -> type[_T]:55 """Protect against re-patching the distutils if reloaded56 57 Also ensures that no other distutils extension monkeypatched the distutils58 first.59 """60 external_bases = (61 cast(type[_T], cls)62 for cls in _get_mro(cls)63 if not cls.__module__.startswith('setuptools')64 )65 base = next(external_bases)66 if not base.__module__.startswith('distutils'):67 msg = f"distutils has already been patched by {cls!r}"68 raise AssertionError(msg)69 return base70 71 72def patch_all() -> None:73 import setuptools74 75 # we can't patch distutils.cmd, alas76 distutils.core.Command = setuptools.Command # type: ignore[misc,assignment] # monkeypatching77 78 _patch_distribution_metadata()79 80 # Install Distribution throughout the distutils81 for module in distutils.dist, distutils.core, distutils.cmd:82 module.Distribution = setuptools.dist.Distribution83 84 # Install the patched Extension85 distutils.core.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching86 distutils.extension.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching87 if 'distutils.command.build_ext' in sys.modules:88 sys.modules[89 'distutils.command.build_ext'90 ].Extension = setuptools.extension.Extension91 92 93def _patch_distribution_metadata():94 from . import _core_metadata95 96 """Patch write_pkg_file and read_pkg_file for higher metadata standards"""97 for attr in (98 'write_pkg_info',99 'write_pkg_file',100 'read_pkg_file',101 'get_metadata_version',102 'get_fullname',103 ):104 new_val = getattr(_core_metadata, attr)105 setattr(distutils.dist.DistributionMetadata, attr, new_val)106 107 108def patch_func(replacement, target_mod, func_name) -> None:109 """110 Patch func_name in target_mod with replacement111 112 Important - original must be resolved by name to avoid113 patching an already patched function.114 """115 original = getattr(target_mod, func_name)116 117 # set the 'unpatched' attribute on the replacement to118 # point to the original.119 vars(replacement).setdefault('unpatched', original)120 121 # replace the function in the original module122 setattr(target_mod, func_name, replacement)123 124 125def get_unpatched_function(candidate):126 return candidate.unpatched127 