This version of autoreload detects when we can leverage targeted recompilation of a subset of a module and patching existing function/method objects to reflect these changes. Detects what functions/methods can be reloaded by recursively comparing the old/new AST of module-level classes
| 172 | |
| 173 | |
| 174 | class DeduperReloader(DeduperReloaderPatchingMixin): |
| 175 | """ |
| 176 | This version of autoreload detects when we can leverage targeted recompilation of a subset of a module and patching |
| 177 | existing function/method objects to reflect these changes. |
| 178 | |
| 179 | Detects what functions/methods can be reloaded by recursively comparing the old/new AST of module-level classes, |
| 180 | module-level classes' methods, recursing through nested classes' methods. If other changes are made, original |
| 181 | autoreload algorithm is called directly. |
| 182 | """ |
| 183 | |
| 184 | def __init__(self) -> None: |
| 185 | self._to_autoreload: AutoreloadTree = AutoreloadTree() |
| 186 | self.source_by_modname: dict[str, str] = {} |
| 187 | self.dependency_graph: dict[tuple[str, ...], list[DependencyNode]] = {} |
| 188 | self._enabled = True |
| 189 | |
| 190 | @property |
| 191 | def enabled(self) -> bool: |
| 192 | return self._enabled and platform.python_implementation() == "CPython" |
| 193 | |
| 194 | @enabled.setter |
| 195 | def enabled(self, value: bool) -> None: |
| 196 | self._enabled = value |
| 197 | |
| 198 | def update_sources(self) -> None: |
| 199 | """ |
| 200 | Update dictionary source_by_modname with current modules' source codes. |
| 201 | """ |
| 202 | if not self.enabled: |
| 203 | return |
| 204 | for new_modname in sys.modules.keys() - self.source_by_modname.keys(): |
| 205 | new_module = sys.modules[new_modname] |
| 206 | if ( |
| 207 | (fname := get_module_file_name(new_module)) is None |
| 208 | or "site-packages" in fname |
| 209 | or "dist-packages" in fname |
| 210 | or not os.access(fname, os.R_OK) |
| 211 | ): |
| 212 | self.source_by_modname[new_modname] = "" |
| 213 | continue |
| 214 | # Skip binary files (e.g., .so, .pyd, .pyo) |
| 215 | if not fname.endswith(".py"): |
| 216 | self.source_by_modname[new_modname] = "" |
| 217 | continue |
| 218 | try: |
| 219 | # tokenize.open honors PEP 263 coding cookies and defaults |
| 220 | # to utf-8, like the import system does. |
| 221 | with tokenize.open(fname) as f: |
| 222 | self.source_by_modname[new_modname] = f.read() |
| 223 | except Exception as e: |
| 224 | logger = logging.getLogger("autoreload") |
| 225 | logger.exception( |
| 226 | f"Failed to read module file '{fname}' for module '{new_modname}': {type(e).__name__}" |
| 227 | ) |
| 228 | self.source_by_modname[new_modname] = "" |
| 229 | |
| 230 | constexpr_detector = ConstexprDetector() |
| 231 |
no test coverage detected
searching dependent graphs…