Repository: microsoft/pybryt

Issue:
Change the use of `tracing_off` + `tracing_on` to be a context manager
Basically, the idea is to make calling `tracing_off` and `tracing_on` directly an anti-pattern and use context managers to control tracing. Currently, the time complexity check (#33) and individual question checks (#38) use context managers, and a unified approach is probably the best idea.

For example, to stop code from being traced during grading, something like

```python
with pybryt.no_tracing():
    # some code
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8065050..797b2cd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file, and this project adheres to 
 [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## Unreleased
+
+* Added the context manager `pybryt.no_tracing`
+
 ## 0.1.0 - 2021-05-27
 
 * Added time complexity annotations and checking
diff --git a/docs/student_implementations.rst b/docs/student_implementations.rst
index 453d92c..9f58be4 100644
--- a/docs/student_implementations.rst
+++ b/docs/student_implementations.rst
@@ -44,6 +44,16 @@ statements that show the values at various stages of execution.
 
     stu = pybryt.StudentImplementation("subm.ipynb", output="executed-subm.ipynb")
 
+If there is code in a student notebook that should not be traced by PyBryt, wrap it PyBryt's
+:py:class:`no_tracing<pybryt.execution.no_tracing>` context manager. Any code inside this context
+will not be traced (if PyBryt is tracing the call stack). If no tracing is occurring, no action is
+taken.
+
+.. code-block:: python
+
+    with pybryt.no_tracing():
+        foo(1)
+
 
 Checking Implementations
 ------------------------
diff --git a/pybryt/execution/__init__.py b/pybryt/execution/__init__.py
index 89ce8a2..64be0d7 100644
--- a/pybryt/execution/__init__.py
+++ b/pybryt/execution/__init__.py
@@ -1,6 +1,6 @@
 """Submission execution internals for PyBryt"""
 
-__all__ = ["check_time_complexity", "tracing_off", "tracing_on"]
+__all__ = ["check_time_complexity", "no_tracing"]
 
 import os
 import dill
@@ -13,7 +13,9 @@ from typing import Any, List, Tuple, Optional
 from textwrap import dedent
 
 from .complexity import check_time_complexity, TimeComplexityResult
-from .tracing import create_collector, _get_tracing_frame, tracing_off, tracing_on, TRACING_VARNAME
+from .tracing import (
+    create_collector, _get_tracing_frame, no_tracing, tracing_off, tracing_on, TRACING_VARNAME
+)
 from ..preprocessors import IntermediateVariablePreprocessor
 from ..utils import make_secret
 
diff --git a/pybryt/execution/tracing.py b/pybryt/execution/tracing.py
index c733705..a45f047 100644
--- a/pybryt/execution/tracing.py
+++ b/pybryt/execution/tracing.py
@@ -216,3 +216,28 @@ def tracing_on(frame=None, tracing_func=None):
     vn2 = f"sys_{make_secret()}"
     frame.f_globals[vn] = tracing_func
     exec(f"import sys as {vn2}\n{vn2}.settrace({vn})", frame.f_globals, frame.f_locals)
+
+
+class no_tracing:
+    """
+    A context manager for turning tracing off for a block of code in a submission.
+
+    If PyBryt is tracing code, any code inside this context will not be traced for values in memory.
+    If PyBryt is not tracing, no action is taken.
+
+    .. code-block:: python
+
+        with pybryt.no_tracing():
+            # this code is not traced
+            foo(1)
+        
+        # this code is traced
+        foo(2)
+    """
+
+    def __enter__(self):
+        tracing_off()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        tracing_on()
+        return False


Test patch:
diff --git a/tests/execution/test_tracing.py b/tests/execution/test_tracing.py
index cd61c2a..46e722c 100644
--- a/tests/execution/test_tracing.py
+++ b/tests/execution/test_tracing.py
@@ -7,7 +7,7 @@ import numpy as np
 from unittest import mock
 
 from pybryt import *
-from pybryt.execution import create_collector, TRACING_VARNAME
+from pybryt.execution import create_collector, tracing_off, tracing_on, TRACING_VARNAME
 
 from .utils import generate_mocked_frame
 
@@ -144,4 +144,13 @@ def test_tracing_control():
         tracing_on()
         mocked_settrace.assert_not_called()
 
-    # assert inspect.currentframe().f_trace is not trace
+
+def test_tracing_context_manager():
+    """
+    """
+    with mock.patch("pybryt.execution.tracing.tracing_off") as mocked_off, \
+            mock.patch("pybryt.execution.tracing.tracing_on") as mocked_on:
+        with no_tracing():
+            mocked_off.assert_called()
+            mocked_on.assert_not_called()
+        mocked_on.assert_called()