diff --git a/.github/workflows/pypi_publish.yml b/.github/workflows/pypi_publish.yml new file mode 100644 index 0000000..4e1ef42 --- /dev/null +++ b/.github/workflows/pypi_publish.yml @@ -0,0 +1,31 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/README.md b/README.md index b1fa940..2a84343 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ By default, existing notebooks will not be overwritten. Specifying `--force` or ## Forms of Delimiters -Any instructor-only markdown file should be properly delimited. To delimit blocks of Python code, use: +Any instructor-only markdown file should be properly delimited. To delimit blocks of Python code, use ``: ```` ```python # set-up code here @@ -77,7 +77,7 @@ Running Cogbooks will then yield: # STUDENT CODE HERE ``` -Alternatively, to remove single lines of code, use: +Alternatively, to remove single lines of code, use ``: ```` ```python # set-up code here @@ -90,6 +90,19 @@ Applying Cogbooks will again result in: # STUDENT CODE HERE ``` +To leave a "stubbed" assignment expression, one can use `` to the right of an assignment (i.e. an expression using `=`): + +```` +```python +x = 33.1 # compute `x` +``` +```` +Applying Cogbooks will leave behind a "stub" for that assignment: + +```python +x = # compute `x` +``` + In markdown, use: ```markdown diff --git a/src/cogbooks/_functions.py b/src/cogbooks/_functions.py index 3ef941a..636bf23 100644 --- a/src/cogbooks/_functions.py +++ b/src/cogbooks/_functions.py @@ -5,6 +5,9 @@ from jupytext.cli import jupytext +JUPYTEXT_HEADER = """--- +jupyter: + jupytext:""" def strip_text(text: str) -> str: """ @@ -37,7 +40,15 @@ def strip_text(text: str) -> str: # Remove single lines from code (with `` addendum), preserving whitespace # and replace with a STUDENT CODE HERE comment - return re.sub(r"\S(?", "# STUDENT CODE HERE", stu_notebook) + stu_notebook = re.sub(r"\S(?", "# STUDENT CODE HERE", stu_notebook) + + # Replace expression to the right of a `=` and left of `` with a comment. + # z = 1 # compute `z` + # z = # compute `z` + stu_notebook = re.sub(r"=.\S(?", "= #", stu_notebook) + + return stu_notebook + def make_student_files(path: Path, outdir: Path, force: bool) -> bool: @@ -62,7 +73,14 @@ def make_student_files(path: Path, outdir: Path, force: bool) -> bool: if path.is_file() and path.suffix == ".md" and path.stem != "README": with path.open(mode="r") as f: - student_notebook_text = strip_text(f.read()) + file_contents = f.read() + + if not file_contents.startswith(JUPYTEXT_HEADER): + print(path.name + " is not a jupytext-formatted markdown file") + return False + + student_notebook_text = strip_text(file_contents) + del file_contents student_markdown_path = outdir / (path.stem + "_STUDENT.md") student_notebook_path = student_markdown_path.parent / ( diff --git a/tests/conftest.py b/tests/conftest.py index 4acc50b..826ebd5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,7 @@ def cleandir() -> str: os.mkdir('test_files') dest = Path.cwd() / 'test_files' shutil.copy(data_dir / 'test.md', dest) + shutil.copy(data_dir / 'not_jupytext.md', dest) shutil.copy(data_dir / 'test_STUDENT.ipynb', dest) yield tmpdirname os.chdir(old_dir) diff --git a/tests/test_file_handling.py b/tests/test_file_handling.py index 96dbc2d..cec7590 100644 --- a/tests/test_file_handling.py +++ b/tests/test_file_handling.py @@ -13,6 +13,7 @@ def test_dir_ipynb_doesnt_exist(): assert not dest.exists() os.system(f"cogbooks test_files") assert dest.exists() + assert len(list((Path(".") / "test_files").glob("*.ipynb"))) == 1 @pytest.mark.usefixtures("cleandir") diff --git a/tests/test_files/not_jupytext.md b/tests/test_files/not_jupytext.md new file mode 100644 index 0000000..a3c6a8f --- /dev/null +++ b/tests/test_files/not_jupytext.md @@ -0,0 +1 @@ +Should not be parsed by Cogbooks \ No newline at end of file diff --git a/tests/test_strip_text.py b/tests/test_strip_text.py index 307580f..a8548e4 100644 --- a/tests/test_strip_text.py +++ b/tests/test_strip_text.py @@ -1,21 +1,30 @@ from cogbooks import strip_text from hypothesis import given import hypothesis.strategies as st +from functools import wraps + + +def contains_cogbook_tag(text: str) -> bool: + return all(item not in text for item in ["", "", "", "", "", ""]) + + +def filtered_text(*arg, **kwargs): + return st.text(*arg, **kwargs).filter(contains_cogbook_tag) @given( - pre_code_delim=st.characters(), - code_delim=st.characters(), - post_code_delim=st.characters(), - pre_md_delim=st.characters(), - md_delim=st.characters(), - post_md_delim=st.characters(), - pre_note_delim=st.characters(), - note_delim=st.characters(), - post_note_delim=st.characters(), - pre_line_delim=st.characters(), - line_delim=st.characters(blacklist_characters="\n"), - post_line_delim=st.characters(), + pre_code_delim=filtered_text(), + code_delim=filtered_text(), + post_code_delim=filtered_text(), + pre_md_delim=filtered_text(), + md_delim=filtered_text(), + post_md_delim=filtered_text(), + pre_note_delim=filtered_text(), + note_delim=filtered_text(), + post_note_delim=filtered_text(), + pre_line_delim=filtered_text(), + line_delim=filtered_text(st.characters(blacklist_characters="\n")), + post_line_delim=filtered_text(), ) def test_combined_rand_text( pre_code_delim, @@ -72,6 +81,20 @@ def test_combined_rand_text( assert strip_text(text) == filtered_text +text_strat = filtered_text().filter(lambda x: "=" not in x) + + +@given(pre=filtered_text().filter(lambda x: "=" not in x), + answer=filtered_text(alphabet="abc123", min_size=1).filter(lambda x: "=" not in x), + description=filtered_text().filter(lambda x: "=" not in x), + ) +def test_cogstub(pre: str, answer: str, description: str): + + original_text = f"{pre} = {answer} # {description}" + filtered_text = f"{pre} = # {description}" + assert strip_text(original_text) == filtered_text + + def test_ex_ipynb(): text = """ @@ -121,6 +144,8 @@ def test_ex_ipynb(): ax.set_xlabel("Frequency [1 / days]") ax.set_yscale("log") # + + z = 1 # compute `z` ``` 3.9 We want to smooth this stock market data. We can do this by "removing" the high-frequency coefficients of its Fourier spectrum. Try zeroing-out the top 90% high-frequency coefficients, and then perform an inverse FFT using these altered coefficients. Plot the "recovered" signal on top of a semi-transparent version of the original data (use the plot parameter `alpha=0.5`). Then repeat this, but with zeroing out the top 98% coefficients. In both of these cases, on what scale are the fluctuations being filtered out? @@ -185,6 +210,8 @@ def test_ex_ipynb(): ```python # STUDENT CODE HERE + + z = # compute `z` ``` 3.9 We want to smooth this stock market data. We can do this by "removing" the high-frequency coefficients of its Fourier spectrum. Try zeroing-out the top 90% high-frequency coefficients, and then perform an inverse FFT using these altered coefficients. Plot the "recovered" signal on top of a semi-transparent version of the original data (use the plot parameter `alpha=0.5`). Then repeat this, but with zeroing out the top 98% coefficients. In both of these cases, on what scale are the fluctuations being filtered out?