Skip to content

Commit

Permalink
Log code changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
eskildsf committed Dec 23, 2024
1 parent edb40da commit d09e75e
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 3 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function = reloading(function)

## Additional Options

### Iterate Forever in For Loop
To iterate forever in a `for` loop you can omit the argument:
```python
from reloading import reloading
Expand All @@ -81,6 +82,31 @@ for _ in reloading():
pass
```

### Code Changes Logged
On Python 3.9 and newer, a diff is logged when the source code is updated.
Consider the following code as an example.
```python
from reloading import reloading
from time import sleep
import logging

log = logging.getLogger("reloading")
log.setLevel(logging.DEBUG)

for i in reloading(range(100)):
print(i)
sleep(1.0)
```
After some time the code is edited. `i = 2*i` is added before `print(i)`,
resulting in the following log output:
```console
INFO:reloading:For loop at line 10 of file "../example.py" has been reloaded.
DEBUG:reloading:Code changes:
+i = i * 2
print(i)
sleep(1.0)
```

## Known Issus

On Python version [less than 3.13](https://docs.python.org/3/reference/datamodel.html#frame.f_locals) it is not possible to properly export the local variables from a loop to parent locals. The following example demonstrates this:
Expand All @@ -95,14 +121,19 @@ def function():

function() # Prints 0. Not 10 as expected. Fixed in Python 3.13.
```
A warning is emitted when the issue arises: `WARNING:reloading:Variable(s) "i" in reloaded loop were not exported to the scope which called the reloaded loop at line...`.
A warning is emitted when the issue arises:
```console
WARNING:reloading:Variable(s) "i" in reloaded loop were not exported to the scope which called the reloaded loop at line...
```

## Lint, Type Check and Testing

Run:
```console
$ pip install -e ".[development]"
$ ruff check .
$ flake8
$ pyright
$ mypy .
$ python -m unittest
```
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,4 @@ development = [
include = ["reloading"]
exclude = [
"**/__pycache__",
"examples",
]
29 changes: 28 additions & 1 deletion reloading/reloading.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,29 @@
overload)
from itertools import chain
import logging
import difflib

log = logging.getLogger("reloading")
logging.basicConfig(level=logging.INFO)


def get_diff_text(ast_before: ast.Module, ast_after: ast.Module):
"""
Calculate difference between two versions of reloaded code.
"""
# Unparse was introduced in Python 3.9.
if sys.version_info.major >= 3 and sys.version_info.minor >= 9:
code_before = ast.unparse(ast_before)
code_after = ast.unparse(ast_after)
diff = difflib.unified_diff(code_before.splitlines(),
code_after.splitlines(),
lineterm="")
# Omit first three lines because they contain superfluous information.
return "\n".join(["Code changes:"]+list(diff)[3:])
else:
return "Cannot compute code changes. Requires Python > 3.9."


class ReloadingException(Exception):
pass

Expand Down Expand Up @@ -344,10 +362,13 @@ def execute_for_loop(seq: Iterable,
if i > 0:
log.info(f'For loop at line {loop_frame_info.lineno} of file '
f'"{filename}" has been reloaded.')
ast_before = for_loop.ast
for_loop = get_loop_code(
loop_frame_info, loop_id=for_loop.id, filename=filename
)
assert isinstance(for_loop, ForLoop)
ast_after = for_loop.ast
log.debug(get_diff_text(ast_before, ast_after))
file_stat = file_stat_
# Make up a name for a variable which is not already present in
# the global or local namespace.
Expand Down Expand Up @@ -400,9 +421,12 @@ def condition(while_loop):
if file_stat != file_stat_:
log.info(f'While loop at line {loop_frame_info.lineno} of file '
f'"{filename}" has been reloaded.')
ast_before = while_loop.ast
while_loop = get_loop_code(
loop_frame_info, loop_id=while_loop.id, filename=filename
)
ast_after = while_loop.ast
log.debug(get_diff_text(ast_before, ast_after))
file_stat = file_stat_
try:
exec(while_loop.compiled_body, caller_globals, caller_locals)
Expand Down Expand Up @@ -534,7 +558,7 @@ def __init__(self,
function_frame_info: inspect.FrameInfo,
ast_module: ast.Module,
id: str):
self.ast_module = ast_module
self.ast = ast_module
self.id = id
self.name = function_name
caller_locals = function_frame_info.frame.f_locals
Expand Down Expand Up @@ -660,10 +684,13 @@ def wrapped(*args, **kwargs):
log.info(f'Function "{function.__name__}" initially defined at '
f'line {function_frame_info.lineno} '
f'of file "{filename}" has been reloaded.')
ast_before = function_object.ast
function_object = get_reloaded_function(function_frame_info,
function,
filename,
function_object.id)
ast_after = function_object.ast
log.debug(get_diff_text(ast_before, ast_after))
file_stat = file_stat_
i += 1
while True:
Expand Down

0 comments on commit d09e75e

Please sign in to comment.