Skip to content
Snippets Groups Projects
Commit fbc84c18 authored by Maurice Herné's avatar Maurice Herné
Browse files

Merge branch 'dynamic-actions' into 'main'

BREAKING CHANGES!

See merge request maurice.herne/notebook_processor!5
parents 8ab2f4cb 388630d2
No related branches found
No related tags found
No related merge requests found
Showing
with 372 additions and 298 deletions
......@@ -10,6 +10,7 @@ from Notebook_Processor import (
NotebookMetadataModel,
CellModifier,
CellSelector,
actions,
)
julia_metadata = DesiredState(
......@@ -73,27 +74,27 @@ common = DesiredState(
selectors=[
CellSelector(cell_type="markdown"),
],
action=CellModifier.Action.REPLACE_IN_SOURCE,
action=actions.replace_in_source,
extra_arguments={"old": "Old text", "new": "New text"},
),
CellModifier(
selectors=[
CellSelector(cell_type="markdown", cell_source_contains="> **Solution:**"),
],
action=CellModifier.Action.DELETE,
action=actions.delete,
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Start your code here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Start your code here:"]},
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Test your function here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Test your function here:"]},
),
],
......
......@@ -6,6 +6,7 @@ from Notebook_Processor import (
DesiredState,
CellModifier,
CellSelector,
actions,
)
......@@ -15,7 +16,7 @@ COMMON = DesiredState(
selectors=[
CellSelector(cell_type="markdown")
],
action=CellModifier.Action.REPLACE_IN_SOURCE,
action=actions.replace_in_source,
extra_arguments={
"old": "Old text",
"new": "New text",
......
......@@ -6,6 +6,7 @@ from Notebook_Processor import (
DesiredState,
CellModifier,
CellSelector,
actions,
)
......@@ -15,26 +16,24 @@ REMOVE_SOLUTIONS = DesiredState(
selectors=[
CellSelector(cell_type="markdown", cell_source_contains="> **Solution:**"),
],
action=CellModifier.Action.DELETE,
action=actions.delete,
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Start your code here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={
"source": ["# Start your code here:"],
"preserve_metadata": False,
},
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Test your function here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={
"source": ["# Test your function here:"],
"preserve_metadata": False,
},
),
],
......
......@@ -16,6 +16,7 @@ from Notebook_Processor import (
NotebookMetadataModel,
CellModifier,
CellSelector,
actions,
)
```
......@@ -87,27 +88,27 @@ common = DesiredState(
selectors=[
CellSelector(cell_type="markdown"),
],
action=CellModifier.Action.REPLACE_IN_SOURCE,
action=actions.replace_in_source,
extra_arguments={"old": "Old text", "new": "New text"},
),
CellModifier(
selectors=[
CellSelector(cell_type="markdown", cell_source_contains="> **Solution:**"),
],
action=CellModifier.Action.DELETE,
action=actions.delete,
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Start your code here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Start your code here:"]},
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Test your function here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Test your function here:"]},
),
],
......
......@@ -11,6 +11,7 @@ from Notebook_Processor import (
NotebookMetadataModel,
CellModifier,
CellSelector,
actions,
)
julia_metadata = DesiredState(
......@@ -74,27 +75,27 @@ common = DesiredState(
selectors=[
CellSelector(cell_type="markdown"),
],
action=CellModifier.Action.REPLACE_IN_SOURCE,
action=actions.replace_in_source,
extra_arguments={"old": "Old text", "new": "New text"},
),
CellModifier(
selectors=[
CellSelector(cell_type="markdown", cell_source_contains="> **Solution:**"),
],
action=CellModifier.Action.DELETE,
action=actions.delete,
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Start your code here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Start your code here:"]},
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Test your function here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Test your function here:"]},
),
],
......
......@@ -31,4 +31,5 @@ from .desired_state import (
NotebookSelector,
CellSelector,
CellModifier,
actions,
)
......@@ -29,7 +29,9 @@ class ConfigurationModel(ConfiguredBaseModel):
Causes defaults to get validated as a side effect.
Since this only happens during the startup of the application, I consider this to be a positive side effect that protects against errors.
"""
assert isinstance(data, dict)
if not isinstance(data, dict):
raise TypeError(f"data must be a dict, not {type(data)}") # pragma: no cover # I don't know of any way to trigger this error but I feel better having it here just in case.
for field, info in cls.model_fields.items():
data.setdefault(field, environ.get(field, info.default))
return data
"""
This module contains the actions that can be performed on the notebook cells.
It is a collection of functions that take a cell and return a new cell with the desired modifications.
All functions have the following signature:
```python
def action_name(cell: NotebookCellAnnotation, *,
mandatory_keyword_argument: type,
optional_keyword_argument: type = default_value,
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
\"\"\"
Description of the action
:param cell NotebookCellAnnotation:
The cell to be modified
:param mandatory_keyword_argument type:
Description of the mandatory keyword argument
:param optional_keyword_argument type:
Description of the optional keyword argument
:param kwargs dict:
Unused keyword arguments
:return NotebookCellAnnotation:
The modified cell
\"\"\"
# Perform the action
return cell
```
"""
from .action_clear_outputs import clear_outputs
from .action_clear import clear
from .action_delete import delete
# from .action_replace_fields import replace_fields
from .action_replace_in_source import replace_in_source
from .action_report import report
from .action_update_metadata import update_metadata
"""
Provides the `clear` action
"""
from logging import (
getLogger,
)
from Notebook_Processor.notebook_models import (
NotebookCellAnnotation,
)
module_logger = getLogger(__name__)
module_logger.debug("Loaded %s", __name__)
def clear(cell: NotebookCellAnnotation, preserve_id: bool = True, preserve_metadata: bool = False, **kwargs) -> NotebookCellAnnotation:
"""
Clear the cell (returns a fresh instance of the cell)
The cell will be initialized with the given keyword arguments
:param preserve_id bool:
Preserve the id of the cell
:param preserve_metadata bool:
Preserve the metadata of the cell
:param kwargs dict:
Additional keyword arguments to initialize the cell. Directly passed to the cell constructor.
"""
cell_class = type(cell)
if kwargs.get("cell_type", cell.cell_type) != cell.cell_type:
module_logger.warning("Clear doesn't support changing the cell type. Using %s", cell.cell_type)
kwargs["cell_type"] = cell.cell_type
if preserve_id:
kwargs.setdefault("id", cell.id)
if preserve_metadata:
kwargs.setdefault("metadata", cell.metadata)
return cell_class(**kwargs)
"""
Provides the `clear_outputs` action
"""
from logging import (
getLogger,
)
from Notebook_Processor.notebook_models import (
NotebookCellAnnotation,
)
module_logger = getLogger(__name__)
module_logger.debug("Loaded %s", __name__)
def clear_outputs(cell: NotebookCellAnnotation,
**kwargs # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Clear the output of the cell
:param cell NotebookCellAnnotation:
The cell to be modified
:param kwargs dict:
Unused keyword arguments
:return NotebookCellAnnotation:
"""
if hasattr(cell, "outputs"):
return cell.model_copy(
deep=True,
update={
"outputs": []
}
)
# The following could happen for cells that don't have an output such as Markdown Cells.
module_logger.warning("Cell '%s' has no field 'outputs'. Skipping clearing of outputs.", cell.id)
return cell
"""
Provides the `delete` action
"""
from typing import (
Optional,
)
from Notebook_Processor.notebook_models import (
NotebookCellAnnotation,
)
def delete(cell: Optional[NotebookCellAnnotation] = None, # pylint: disable=unused-argument
**kwargs, # pylint: disable=unused-argument
) -> None:
"""
Delete the cell (always returns None)
"""
return None
# """
# Provides the `replace_fields` action
# """
# from Notebook_Processor.notebook_models import (
# NotebookCellAnnotation,
# )
# I don't want to remove this yet but I am unsure of the value of this function.
# It is currently also broken due to isinstance not being able to handle complex annotations.
# @staticmethod
# def replace_fields(cell: NotebookCellAnnotation, *,
# fields: dict[str, Any],
# clear_undefined: bool = False,
# **kwargs, # pylint: disable=unused-argument
# ) -> NotebookCellAnnotation:
# """
# Replace the fields in the cell with the given values
# """
# for field in cell.model_fields_set:
# if new := fields.get(field, False):
# if not isinstance(new, cell.model_fields[field].annotation):
# try:
# new = cell.model_fields[field].annotation(new)
# except Exception as exc:
# raise ValidationError(f"Expected {cell.model_fields[field].annotation}, got {type(new)}") from exc
# cell.__dict__[field] = new
# elif clear_undefined:
# cell.__dict__[field] = None
# return cell
"""
Provides the `replace_in_source` action
"""
from Notebook_Processor.notebook_models import (
NotebookCellAnnotation,
)
def replace_in_source(cell: NotebookCellAnnotation, *,
old: str,
new: str,
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Replace the given string in the source of the cell
"""
return cell.model_copy(
deep=True,
update={
"source": [line.replace(old, new) for line in cell.source]
}
)
"""
Provides the `report` action
"""
from typing import (
Callable,
Any,
)
from Notebook_Processor.notebook_models import (
NotebookCellAnnotation,
)
def report(cell: NotebookCellAnnotation, *,
logger: Callable[[str], None | Any] = print,
field: str | None = "id",
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Report the cell (returns the cell without any modifications)
"""
try:
logger(cell.__dict__[field])
except KeyError:
logger(f"Requested field {field} not found in cell, logging the cell itself")
logger(cell)
return cell
"""
Provides the `update_metadata` action
"""
from typing import (
Any,
)
from Notebook_Processor.notebook_models import (
NotebookCellAnnotation,
)
def update_metadata(cell: NotebookCellAnnotation, *,
metadata: dict[str, Any],
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Update the metadata of the cell
"""
return cell.model_copy(
deep=True,
update={
"metadata": metadata
}
)
......@@ -7,15 +7,11 @@ It also provides some built-in actions that can be applied to cells.
import inspect
from typing import (
Optional,
Any,
Callable,
Literal,
Self,
)
from enum import (
Enum,
)
from logging import (
getLogger,
)
......@@ -39,7 +35,7 @@ from ..notebook_models import (
from .selector import (
CellSelector,
)
from . import actions
module_logger = getLogger(__name__)
module_logger.debug("Loaded %s", __name__)
......@@ -56,7 +52,7 @@ class CellModifier(BaseModel):
The mode to use when applying the selectors. "ALL" requires all selectors to match, "ANY" requires at least one selector to match.
:param action Callable: The action to be applied to the cell.
Can be a custom callable or one fo the provided actions from the Action enum. The `action` will be called with the cell as the first and only positional argument.
Can be a custom callable or one of the provided actions from the actions submodule. The `action` will be called with the cell as the first and only positional argument.
:param extra_arguments dict: will be passed as keyword arguments to the action.
Ensure to supply all required arguments in the `extra_arguments` dictionary.
......@@ -64,210 +60,39 @@ class CellModifier(BaseModel):
### If you want to use a custom function as an action, ensure that it satisfies the following form:
```python
def custom_action(cell: NotebookCellAnnotation, *,
extra_arg_1: annotation,
extra_arg_2: annotation,
extra_arg_n: annotation,
**kwargs) -> NotebookCellAnnotation:
\"\"\"
# Custom Action Name
Custom action to be applied to the cell
:param cell NotebookCellAnnotation: The cell to be modified
:param extra_arg_1 annotation: Needed for something
:param extra_arg_2 annotation: Needed for something else
:param extra_arg_n annotation: Needed for something else
:param kwargs dict: Additional keyword arguments needed so no errors are thrown for additional unused arguments
\"\"\"
# Your code here
return cell
```
The function signature is checked during model validation to ensure the function signature is not offending.
"""
class Action(Enum):
"""
An Enum of builtin actions that can be applied to cells
"""
@staticmethod
def delete(cell: Optional[NotebookCellAnnotation] = None, # pylint: disable=unused-argument
def action_name(cell: NotebookCellAnnotation, *,
mandatory_keyword_argument: type,
optional_keyword_argument: type = default_value,
**kwargs, # pylint: disable=unused-argument
):
"""
Delete the cell (always returns None)
"""
return None
@staticmethod
def clear(cell: NotebookCellAnnotation, preserve_id: bool = True, preserve_metadata: bool = False, **kwargs) -> NotebookCellAnnotation:
"""
Clear the cell (returns a fresh instance of the cell)
The cell will be initialized with the given keyword arguments
:param preserve_id bool:
Preserve the id of the cell
:param preserve_metadata bool:
Preserve the metadata of the cell
:param kwargs dict:
Additional keyword arguments to initialize the cell. Directly passed to the cell constructor.
"""
cell_class = type(cell)
if kwargs.get("cell_type", cell.cell_type) != cell.cell_type:
module_logger.warning("Clear doesn't support changing the cell type. Using %s", cell.cell_type)
kwargs["cell_type"] = cell.cell_type
if preserve_id:
kwargs.setdefault("id", cell.id)
if preserve_metadata:
kwargs.setdefault("metadata", cell.metadata)
return cell_class(**kwargs)
@staticmethod
def clear_outputs(cell: NotebookCellAnnotation,
**kwargs # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Clear the output of the cell
\"\"\"
Description of the action
:param cell NotebookCellAnnotation:
The cell to be modified
:param mandatory_keyword_argument type:
Description of the mandatory keyword argument
:param optional_keyword_argument type:
Description of the optional keyword argument
:param kwargs dict:
Unused keyword arguments
:return NotebookCellAnnotation:
"""
if hasattr(cell, "outputs"):
return cell.model_copy(
deep=True,
update={
"outputs": []
}
)
# The following could happen for cells that don't have an output such as Markdown Cells.
module_logger.warning("Cell '%s' has no field 'outputs'. Skipping clearing of outputs.", cell.id)
return cell
# I don't want to remove this yet but I am unsure of the value of this function.
# It is currently also broken due to isinstance not being able to handle complex annotations.
# @staticmethod
# def replace_fields(cell: NotebookCellAnnotation, *,
# fields: dict[str, Any],
# clear_undefined: bool = False,
# **kwargs, # pylint: disable=unused-argument
# ) -> NotebookCellAnnotation:
# """
# Replace the fields in the cell with the given values
# """
# for field in cell.model_fields_set:
# if new := fields.get(field, False):
# if not isinstance(new, cell.model_fields[field].annotation):
# try:
# new = cell.model_fields[field].annotation(new)
# except Exception as exc:
# raise ValidationError(f"Expected {cell.model_fields[field].annotation}, got {type(new)}") from exc
# cell.__dict__[field] = new
# elif clear_undefined:
# cell.__dict__[field] = None
# return cell
@staticmethod
def replace_in_source(cell: NotebookCellAnnotation, *,
old: str,
new: str,
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Replace the given string in the source of the cell
"""
return cell.model_copy(
deep=True,
update={
"source": [line.replace(old, new) for line in cell.source]
}
)
@staticmethod
def update_metadata(cell: NotebookCellAnnotation, *,
metadata: dict[str, Any],
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Update the metadata of the cell
"""
return cell.model_copy(
deep=True,
update={
"metadata": metadata
}
)
@staticmethod
def report(cell: NotebookCellAnnotation, *,
logger: Callable[[str], None | Any] = print,
field: str | None = "id",
**kwargs, # pylint: disable=unused-argument
) -> NotebookCellAnnotation:
"""
Report the cell (returns the cell without any modifications)
"""
try:
logger(cell.__dict__[field])
except KeyError:
logger(f"Requested field {field} not found in cell, logging the cell itself")
logger(cell)
The modified cell
\"\"\"
# Perform the action
return cell
```
DELETE = delete
CLEAR = clear
CLEAR_OUTPUTS = clear_outputs
# REPLACE_FIELDS = replace_fields
REPLACE_IN_SOURCE = replace_in_source
UPDATE_METADATA = update_metadata
REPORT = report
@classmethod
def get_members(cls):
"""
# A workaround for the Enum not having a working __members__ attribute.
__members__ returns an empty list because Enum aparrently doesn't list mappings to Callable
"""
return [
"DELETE",
"CLEAR",
# "REPLACE_FIELDS",
"REPLACE_IN_SOURCE",
"UPDATE_METADATA",
"REPORT",
]
@classmethod
def custom_init(cls, action: str) -> Callable:
"""
# A workaround for the Enum not having a working __members__ attribute.
__members__ returns an empty list because Enum aparrently doesn't list mappings to Callable
The function signature is checked during model validation to ensure the function signature is not offending.
"""
match action:
case "DELETE":
return cls.DELETE
case "CLEAR":
return cls.CLEAR
# case "REPLACE_FIELDS":
# return cls.REPLACE_FIELDS
case "REPLACE_IN_SOURCE":
return cls.REPLACE_IN_SOURCE
case "UPDATE_METADATA":
return cls.UPDATE_METADATA
case "REPORT":
return cls.REPORT
case _:
raise ValueError(f"Action {action} not found in the Action Enum")
selectors: list[CellSelector] = []
selector_mode: Literal["ALL", "ANY"] = config.DEFAULT_SELECTOR_MODE
action: Action | Callable
action: Callable
extra_arguments: dict[str, Any] = {}
def __call__(self, cell: NotebookCellAnnotation) -> NotebookCellAnnotation:
......@@ -287,13 +112,14 @@ class CellModifier(BaseModel):
@field_serializer("action")
def _serialize_action(self, value: Callable) -> str:
if value.__name__.upper() in self.Action.get_members():
return value.__name__.upper()
"""
Serialize the action to a string when saving to JSON
"""
return value.__name__
@field_validator("action", mode="before")
@classmethod
def _validate_action(cls, value: Any) -> Action | Callable:
def _validate_action(cls, value: Any) -> Callable:
"""
A function to deserialize an action when loading from JSON
......@@ -303,8 +129,8 @@ class CellModifier(BaseModel):
"""
if isinstance(value, str):
try:
value = cls.Action.custom_init(value)
except ValueError as exc:
value = getattr(actions, value.lower())
except AttributeError as exc:
raise NotImplementedError(f"Custom Actions can currently not be loaded from a JSON state. Offending Action: {value}") from exc
return value
......@@ -329,13 +155,13 @@ class CellModifier(BaseModel):
if signature[sig_keys[0]].kind not in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
raise ValueError(
f"Offending function signature: Action {self.action.__name__} must have the first parameter 'cell' as a positional argument.",
f"Found: {sig_keys[0], signature[sig_keys[0]]}",
f"Found: {sig_keys[0]}, {signature[sig_keys[0]]}",
)
if signature[sig_keys[-1]].kind != inspect.Parameter.VAR_KEYWORD:
raise ValueError(
f"Offending function signature: Action {self.action.__name__} must accept keyword arguments.",
f"The last paramter should be '**kwargs', found: {sig_keys[-1], signature[sig_keys[-1]].kind}",
f"The last paramter should be '**kwargs', found: {sig_keys[-1]}, {signature[sig_keys[-1]].kind}",
)
missing_annotations = [
......
......@@ -3,7 +3,14 @@ import shutil
import pytest
from Notebook_Processor import DesiredState, NotebookSelector, NotebookMetadataModel, CellModifier, CellSelector
from Notebook_Processor import (
DesiredState,
NotebookSelector,
NotebookMetadataModel,
CellModifier,
CellSelector,
actions,
)
from Notebook_Processor.notebook_models import (
NotebookModel,
NotebookCodeCellModel,
......@@ -106,27 +113,27 @@ def desired_states() -> list[DesiredState]:
selectors=[
CellSelector(cell_type="markdown"),
],
action=CellModifier.Action.REPLACE_IN_SOURCE,
action=actions.replace_in_source,
extra_arguments={"old": "Murwan Siddig, Stefan Pilot", "new": ""},
),
CellModifier(
selectors=[
CellSelector(cell_type="markdown", cell_source_contains="> **Solution:**"),
],
action=CellModifier.Action.DELETE,
action=actions.delete,
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Start your code here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Start your code here:"]},
),
CellModifier(
selectors=[
CellSelector(cell_type="code", cell_source_contains="# Test your function here:"),
],
action=CellModifier.Action.CLEAR,
action=actions.clear,
extra_arguments={"source": ["# Test your function here:"]},
),
],
......
......@@ -14,6 +14,7 @@ from Notebook_Processor.notebook_models import (
from Notebook_Processor.desired_state import (
CellModifier,
CellSelector,
actions,
)
from Notebook_Processor.configuration import (
config,
......@@ -24,28 +25,28 @@ def test_init():
with pytest.raises(ValidationError):
CellModifier()
modifier = CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
)
assert modifier.selectors == []
assert modifier.selector_mode == config.DEFAULT_SELECTOR_MODE
assert modifier.action == CellModifier.Action.REPORT
assert modifier.action == actions.report
assert modifier.extra_arguments == {}
def test_override_default_selector_mode():
assert CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
selector_mode="ALL",
).selector_mode == "ALL"
assert CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
selector_mode="ANY",
).selector_mode == "ANY"
def test_selector_ANY(run_code_cell: NotebookCellAnnotation, markdown_solution_cell: NotebookCellAnnotation, capsys):
modifier = CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
selectors=[CellSelector(cell_type="code"), CellSelector(cell_type="markdown")],
selector_mode="ANY",
)
......@@ -59,7 +60,7 @@ def test_selector_ANY(run_code_cell: NotebookCellAnnotation, markdown_solution_c
def test_selector_ALL_impossible(run_code_cell: NotebookCellAnnotation, markdown_solution_cell: NotebookCellAnnotation, capsys):
modifier_impossible = CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
selectors=[CellSelector(cell_type="code"), CellSelector(cell_type="markdown")],
selector_mode="ALL",
)
......@@ -73,7 +74,7 @@ def test_selector_ALL_impossible(run_code_cell: NotebookCellAnnotation, markdown
def test_selector_ALL_possible(run_code_cell: NotebookCellAnnotation, markdown_solution_cell: NotebookCellAnnotation, capsys):
modifier_possible = CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
selectors=[CellSelector(cell_type="code")],
selector_mode="ALL",
)
......@@ -87,7 +88,7 @@ def test_selector_ALL_possible(run_code_cell: NotebookCellAnnotation, markdown_s
def test_modifier_call(markdown_solution_cell, capsys):
modifier = CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
)
capsys.readouterr()
assert modifier(markdown_solution_cell) == markdown_solution_cell
......@@ -96,7 +97,7 @@ def test_modifier_call(markdown_solution_cell, capsys):
def test_action_validator_additional():
CellModifier(
action=CellModifier.Action.REPORT,
action=actions.report,
extra_arguments={"noise": "LOUD"},
)
......@@ -104,7 +105,7 @@ def test_action_validator_additional():
def test_action_validator_missing():
with pytest.raises(ValidationError):
CellModifier(
action=CellModifier.Action.UPDATE_METADATA,
action=actions.update_metadata,
# extra_arguments={"metadata": {}}, # Minimal required extra_arguments
)
......@@ -112,14 +113,14 @@ def test_action_validator_missing():
def test_action_validator_wrong_type():
with pytest.raises(ValidationError):
CellModifier(
action=CellModifier.Action.UPDATE_METADATA,
action=actions.update_metadata,
extra_arguments={"metadata": "metadatastring"},
)
def test_action_validator_correct_extra_arguments():
CellModifier(
action=CellModifier.Action.UPDATE_METADATA,
action=actions.update_metadata,
extra_arguments={"metadata": {}},
)
......@@ -261,25 +262,3 @@ def test_action_validator_offending_signature_VAR_POSITIONAL(run_code_cell):
extra_arguments={"args": ("x", "y")},
)
config.ACTION_SIGNATURE_VALIDATION = "STRICT"
def test_action_get_members():
assert CellModifier.Action.get_members() == [
"DELETE",
"CLEAR",
# "REPLACE_FIELDS",
"REPLACE_IN_SOURCE",
"UPDATE_METADATA",
"REPORT",
]
def test_action_custom_init():
assert CellModifier.Action.custom_init("DELETE") == CellModifier.Action.DELETE
assert CellModifier.Action.custom_init("CLEAR") == CellModifier.Action.CLEAR
# assert CellModifier.Action.custom_init("REPLACE_FIELDS") == CellModifier.Action.REPLACE_FIELDS
assert CellModifier.Action.custom_init("REPLACE_IN_SOURCE") == CellModifier.Action.REPLACE_IN_SOURCE
assert CellModifier.Action.custom_init("UPDATE_METADATA") == CellModifier.Action.UPDATE_METADATA
assert CellModifier.Action.custom_init("REPORT") == CellModifier.Action.REPORT
with pytest.raises(ValueError):
CellModifier.Action.custom_init("INVALID")
......@@ -2,8 +2,13 @@ from typing import Callable
import pytest
from Notebook_Processor.notebook_models import NotebookCodeCellModel, NotebookMarkdownCellModel
from Notebook_Processor.desired_state import CellModifier
from Notebook_Processor.notebook_models import (
NotebookCodeCellModel,
NotebookMarkdownCellModel,
)
from Notebook_Processor.desired_state import (
actions,
)
@pytest.fixture
......@@ -57,16 +62,16 @@ def extra_args() -> dict:
}
def test_DELETE(code_cell, markdown_cell, extra_args):
assert isinstance(CellModifier.Action.DELETE, Callable)
assert CellModifier.Action.DELETE(code_cell, **extra_args) is None
assert CellModifier.Action.DELETE(markdown_cell, **extra_args) is None
def test_delete(code_cell, markdown_cell, extra_args):
assert isinstance(actions.delete, Callable)
assert actions.delete(code_cell, **extra_args) is None
assert actions.delete(markdown_cell, **extra_args) is None
def test_CLEAR_default(code_cell: NotebookCodeCellModel, markdown_cell: NotebookMarkdownCellModel, extra_args):
assert isinstance(CellModifier.Action.CLEAR, Callable)
def test_clear_default(code_cell: NotebookCodeCellModel, markdown_cell: NotebookMarkdownCellModel, extra_args):
assert isinstance(actions.clear, Callable)
# Coce Cell
modified_code_cell = CellModifier.Action.CLEAR(code_cell, **extra_args)
modified_code_cell = actions.clear(code_cell, **extra_args)
code_cell_defaults = {key: value.default for key, value in code_cell.model_fields.items()}
assert isinstance(modified_code_cell, NotebookCodeCellModel)
assert modified_code_cell.id == code_cell.id
......@@ -77,7 +82,7 @@ def test_CLEAR_default(code_cell: NotebookCodeCellModel, markdown_cell: Notebook
assert modified_code_cell.outputs == code_cell_defaults["outputs"]
# Markdown Cell
modified_markdown_cell = CellModifier.Action.CLEAR(markdown_cell, **extra_args)
modified_markdown_cell = actions.clear(markdown_cell, **extra_args)
markdown_cell_defaults = {key: value.default for key, value in markdown_cell.model_fields.items()}
assert isinstance(modified_markdown_cell, NotebookMarkdownCellModel)
assert modified_markdown_cell.id == markdown_cell.id
......@@ -86,13 +91,13 @@ def test_CLEAR_default(code_cell: NotebookCodeCellModel, markdown_cell: Notebook
assert modified_markdown_cell.source == markdown_cell_defaults["source"]
def test_CLEAR_args(code_cell: NotebookCodeCellModel, markdown_cell: NotebookMarkdownCellModel, extra_args, caplog):
assert isinstance(CellModifier.Action.CLEAR, Callable)
def test_clear_args(code_cell: NotebookCodeCellModel, markdown_cell: NotebookMarkdownCellModel, extra_args, caplog):
assert isinstance(actions.clear, Callable)
# Code Cell
caplog.clear()
caplog.set_level("INFO")
modified_code_cell = CellModifier.Action.CLEAR(
modified_code_cell = actions.clear(
code_cell,
cell_type="markdown", # Should log a warning and internally be replaced with the correct cell_type
preserve_id=False,
......@@ -112,7 +117,7 @@ def test_CLEAR_args(code_cell: NotebookCodeCellModel, markdown_cell: NotebookMar
assert caplog.messages[1].startswith("Cell doesn't have an ID (mandatory field: id).")
# Markdown Cell
modified_markdown_cell = CellModifier.Action.CLEAR(
modified_markdown_cell = actions.clear(
markdown_cell,
cell_type="code", # Should log a warning and internally be replaced with the correct cell_type
preserve_id=False,
......@@ -126,9 +131,9 @@ def test_CLEAR_args(code_cell: NotebookCodeCellModel, markdown_cell: NotebookMar
assert modified_markdown_cell.source == ["Goodnight, World!"]
def test_CLEAR_OUTPUTS_code(code_cell, metadata):
assert isinstance(CellModifier.Action.CLEAR_OUTPUTS, Callable)
modified_code_cell = CellModifier.Action.CLEAR_OUTPUTS(code_cell)
def test_clear_outputs_code(code_cell, metadata):
assert isinstance(actions.clear_outputs, Callable)
modified_code_cell = actions.clear_outputs(code_cell)
assert modified_code_cell.outputs == []
assert modified_code_cell == NotebookCodeCellModel(
id="test_code",
......@@ -139,23 +144,23 @@ def test_CLEAR_OUTPUTS_code(code_cell, metadata):
)
def test_CLEAR_OUTPUTS_markdown(markdown_cell, metadata, caplog):
assert isinstance(CellModifier.Action.CLEAR_OUTPUTS, Callable)
def test_clear_outputs_markdown(markdown_cell, caplog):
assert isinstance(actions.clear_outputs, Callable)
caplog.clear()
modified_markdown_cell = CellModifier.Action.CLEAR_OUTPUTS(markdown_cell)
modified_markdown_cell = actions.clear_outputs(markdown_cell)
assert caplog.messages[0] == "Cell '" + markdown_cell.id + "' has no field 'outputs'. Skipping clearing of outputs."
assert modified_markdown_cell == markdown_cell
def test_REPLACE_IN_SOURCE(code_cell, markdown_cell, metadata, extra_args):
assert isinstance(CellModifier.Action.REPLACE_IN_SOURCE, Callable)
modified_code_cell = CellModifier.Action.REPLACE_IN_SOURCE(
def test_replace_in_source(code_cell, markdown_cell, metadata, extra_args):
assert isinstance(actions.replace_in_source, Callable)
modified_code_cell = actions.replace_in_source(
code_cell,
old="Hello",
new="Goodbye",
**extra_args
)
modified_markdown_cell = CellModifier.Action.REPLACE_IN_SOURCE(
modified_markdown_cell = actions.replace_in_source(
markdown_cell,
old="Hello",
new="Goodbye",
......@@ -186,9 +191,9 @@ def test_REPLACE_IN_SOURCE(code_cell, markdown_cell, metadata, extra_args):
)
def test_UPDATE_METADATA(markdown_cell, extra_args):
assert isinstance(CellModifier.Action.UPDATE_METADATA, Callable)
assert CellModifier.Action.UPDATE_METADATA(
def test_update_metadata(markdown_cell, extra_args):
assert isinstance(actions.update_metadata, Callable)
assert actions.update_metadata(
markdown_cell,
metadata={"tags": ["new"]},
**extra_args
......@@ -197,17 +202,17 @@ def test_UPDATE_METADATA(markdown_cell, extra_args):
}
def test_REPORT(code_cell, markdown_cell, capsys, extra_args):
assert isinstance(CellModifier.Action.REPORT, Callable)
assert CellModifier.Action.REPORT(code_cell, logger=print, **extra_args) == code_cell
def test_report(code_cell, markdown_cell, capsys, extra_args):
assert isinstance(actions.report, Callable)
assert actions.report(code_cell, logger=print, **extra_args) == code_cell
captured = capsys.readouterr()
assert captured.out == "test_code\n"
assert CellModifier.Action.REPORT(markdown_cell, logger=print, **extra_args) == markdown_cell
assert actions.report(markdown_cell, logger=print, **extra_args) == markdown_cell
captured = capsys.readouterr()
assert captured.out == "test_md\n"
def test_REPORT_nonexistent_field(code_cell, capsys):
assert CellModifier.Action.REPORT(code_cell, field="nonexistent_field") == code_cell
def test_report_nonexistent_field(code_cell, capsys):
assert actions.report(code_cell, field="nonexistent_field") == code_cell
captured = capsys.readouterr()
assert captured.out.startswith("Requested field nonexistent_field not found in cell, logging the cell itself")
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment