Is it possible to deep watch a single python function for changes?
Shallow Watch
keeping track of a python functions hash is quite simple. There is a__hash__
method attached to every python function. Calling it will return a hash of the function. If the function changes the hash will change.
[ins] In [1]: def test():
...: return "hello"
[ins] In [2]: test.__hash__()
Out[2]: 8760526380347
[ins] In [3]: test.__hash__()
Out[3]: 8760526380347
[ins] In [4]: def test():
...: return "hello world"
[ins] In [5]: test.__hash__()
Out[5]: 8760525617988
[ins] In [6]: def test():
...: return "hello"
[ins] In [7]: test.__hash__()
Out[7]: 8760526380491
Using hashlib provides a consistent hash.
import inspect
import hashlib
def test():
return "hello"
[ins] In [17]: m.update(inspect.getsource(test).encode())
[ins] In [18]: m
Out[18]: <sha256 HASH object @ 0x7f7b7b70fde0>
[ins] In [19]: m.hexdigest()
Out[19]: '1f2ff4c69eb69b545469686edd6f849136e104cd535785891586d90620328757'
[ins] In [20]: m.update(inspect.getsource(test).encode())
[ins] In [21]: m.hexdigest()
Out[21]: '93638f2c944f34a9069af9242657b7de556fcc63742f4c27c4c8deedeb976a5f'
[ins] In [22]: m = hashlib.sha256()
[ins] In [23]: m.update(inspect.getsource(test).encode())
[ins] In [24]: m.update(inspect.getsource(test).encode())
[ins] In [25]: m = hashlib.sha256()
[ins] In [26]: m.update(inspect.getsource(test).encode())
[ins] In [27]: m.hexdigest()
Out[27]: '1f2ff4c69eb69b545469686edd6f849136e104cd535785891586d90620328757'
[ins] In [28]: def test():
...: return "hello world"
[ins] In [29]: m = hashlib.sha256()
[ins] In [30]: m.update(inspect.getsource(test).encode())
[ins] In [31]: m.hexdigest()
Out[31]: '121fa3a3f295d49d4609505bc5e96d8b6a8ed3b496e4f3dc6c0ead73bef4e3c7'
[ins] In [32]: def test():
...: return "hello"
[ins] In [33]: m = hashlib.sha256()
[ins] In [34]: m.update(inspect.getsource(test).encode())
[ins] In [35]: m.hexdigest()
Out[35]: '1f2ff4c69eb69b545469686edd6f849136e104cd535785891586d90620328757'
Deep hashing
Find dependencies
setup a function in a module with a dependency
│ File: one.py
───────┼────────────────────────────────
1 │ def one():
2 │ return 1
3 │
4 │ def two():
5 │ return one() + one()
>>> import one
>>> one.one.__code__.co_names
()
>>> one.two.__code__.co_names
('one', )
Create Generic module importer by filepath
import importlib
import importlib.util
import os
def _import(path: Path, directory: Path, verbose: bool = False):
"""dynamically imports module given a path"""
cwd = os.getcwd()
os.chdir(directory)
name = path.name
# path = str(path).replace(str(directory) + "/", "")
path = _make_path_relative(path, directory)
try:
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except (ModuleNotFoundError, ValueError):
module = _use_importmodule(
str(path).replace(os.sep, ".").replace(".py", ""), verbose=verbose
)
os.chdir(cwd)
return module
def _use_importmodule(path: Path, verbose: bool = False):
"""
relative imports do not work well with importlib.util.spec_from_file_location,
and require a sys.path.append to be imported correctly. For this reason
importlib.import_module is the second option.
"""
# Not sure if this is needed, but it was never hit in a test
# if path[0] == ".":
# path = path[1:]
sys.path.append(os.getcwd())
mod = importlib.import_module(path)
sys.path.pop() # clean up path, do not permananatly change users path
return mod
get code of dependency
the inspect module can tell us the filename of our current module.
import inspect
module_path = inspect.getfile(one.one)
module = _import(module_path)
now we can hash the dependency
nested_function = eval(f'module.{one.two.__code__.co_names[0]}"