diff --git a/dp_creator_ii/converters.py b/dp_creator_ii/converters.py new file mode 100644 index 0000000..6572f08 --- /dev/null +++ b/dp_creator_ii/converters.py @@ -0,0 +1,23 @@ +from pathlib import Path +from tempfile import TemporaryDirectory +import subprocess + + +def convert_py_to_nb(python_str): + ''' + Given Python code as a string, returns a notebook as a string. + Calls jupytext as a subprocess: + Not ideal, but only the CLI is documented well. + ''' + with TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + py_path = temp_dir_path / 'input.py' + py_path.write_text(python_str) + nb_path = temp_dir_path / 'output.ipynb' + subprocess.run( + ['jupytext', + '--to', 'ipynb', # Target format + '--output', nb_path.absolute(), # Output + py_path.absolute()], # Input + check=True) + return nb_path.read_text() diff --git a/dp_creator_ii/template.py b/dp_creator_ii/template.py index 8bf3eb0..c990e70 100644 --- a/dp_creator_ii/template.py +++ b/dp_creator_ii/template.py @@ -34,7 +34,7 @@ def __str__(self): if unfilled: raise Exception( f'Template {self._path} has unfilled slots: ' - f'{", ".join(unfilled)}\n\n{self._template}') + f'{", ".join(sorted(unfilled))}\n\n{self._template}') return self._template diff --git a/dp_creator_ii/tests/fake.csv b/dp_creator_ii/tests/fixtures/fake.csv similarity index 100% rename from dp_creator_ii/tests/fake.csv rename to dp_creator_ii/tests/fixtures/fake.csv diff --git a/dp_creator_ii/tests/fixtures/fake.ipynb b/dp_creator_ii/tests/fixtures/fake.ipynb new file mode 100644 index 0000000..b2f49b1 --- /dev/null +++ b/dp_creator_ii/tests/fixtures/fake.ipynb @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3cf946cc", + "metadata": {}, + "source": [ + "Introduction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5a25047", + "metadata": {}, + "outputs": [], + "source": [ + "print(2+2)" + ] + }, + { + "cell_type": "markdown", + "id": "08581071", + "metadata": {}, + "source": [ + "Conclusion" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all", + "main_language": "python", + "notebook_metadata_filter": "-all" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dp_creator_ii/tests/fixtures/fake.py b/dp_creator_ii/tests/fixtures/fake.py new file mode 100644 index 0000000..a314dee --- /dev/null +++ b/dp_creator_ii/tests/fixtures/fake.py @@ -0,0 +1,5 @@ +# Introduction + +print(2+2) + +# Conclusion diff --git a/dp_creator_ii/tests/test_converters.py b/dp_creator_ii/tests/test_converters.py new file mode 100644 index 0000000..a4d134b --- /dev/null +++ b/dp_creator_ii/tests/test_converters.py @@ -0,0 +1,18 @@ +import re +from pathlib import Path +from dp_creator_ii.converters import convert_py_to_nb + + +def test_convert_py_to_nb(): + fixtures_path = Path('dp_creator_ii/tests/fixtures') + python_str = (fixtures_path / 'fake.py').read_text() + actual_nb_str = convert_py_to_nb(python_str) + expected_nb_str = (fixtures_path / 'fake.ipynb').read_text() + + def norm_nb(nb_str): + normed_nb_str = re.sub(r'"id": "[^"]+"', '"id": "12345678"', nb_str) + return normed_nb_str + + normed_actual_nb_str = norm_nb(actual_nb_str) + normed_expected_nb_str = norm_nb(expected_nb_str) + assert normed_actual_nb_str == normed_expected_nb_str diff --git a/dp_creator_ii/tests/test_template.py b/dp_creator_ii/tests/test_template.py index 9a89ae9..0b975e5 100644 --- a/dp_creator_ii/tests/test_template.py +++ b/dp_creator_ii/tests/test_template.py @@ -1,10 +1,12 @@ from tempfile import NamedTemporaryFile import subprocess +import re +import pytest import opendp.prelude as dp from dp_creator_ii.template import _Template, make_notebook, make_script -fake_csv = 'dp_creator_ii/tests/fake.csv' +fake_csv = 'dp_creator_ii/tests/fixtures/fake.csv' def test_fill_template(): @@ -18,6 +20,16 @@ def test_fill_template(): assert f"data=pl.scan_csv('{fake_csv}')" in context_block +def test_fill_template_unfilled_slots(): + context_template = _Template('context.py') + with pytest.raises( + Exception, + match=re.escape( + 'context.py has unfilled slots: CSV_PATH, LOSS, UNIT, WEIGHTS') + ): + str(context_template.fill_values()) + + def test_make_notebook(): notebook = make_notebook( csv_path=fake_csv, diff --git a/pyproject.toml b/pyproject.toml index 1e69e4b..2128216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "shiny", "shinywidgets", "opendp[polars]", + "jupytext", ] [project.scripts] diff --git a/requirements-dev.in b/requirements-dev.in index f0b9fa3..6ca0900 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,22 +1,29 @@ # After making changes here, run: -# pip-compile requirements-dev.in +# pip-compile requirements-dev.in && pip install -r requirements-dev.txt +# Developer tools: pip-tools flit autoflake -# opendp -opendp[polars] -# For Python 3.9: -scipy<1.14 - - -# testing: +# Testing: pytest flake8 mypy coverage -# shiny: + + +# Everything below should also be listed in pyproject.toml: + +# OpenDP: +opendp[polars] +# For Python 3.9: +scipy<1.14 + +# Conversion: +jupytext + +# Shiny: shiny shinywidgets \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index c7eb48e..3842683 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,10 @@ asgiref==3.8.1 # via shiny asttokens==2.4.1 # via stack-data +attrs==24.2.0 + # via + # jsonschema + # referencing autoflake==2.3.1 # via -r requirements-dev.in build==1.2.2 @@ -37,6 +41,8 @@ docutils==0.21.2 # via flit executing==2.1.0 # via stack-data +fastjsonschema==2.20.0 + # via nbformat flake8==7.1.1 # via -r requirements-dev.in flit==3.9.0 @@ -61,14 +67,23 @@ jedi==0.19.1 # via ipython joblib==1.4.2 # via scikit-learn +jsonschema==4.23.0 + # via nbformat +jsonschema-specifications==2023.12.1 + # via jsonschema jupyter-core==5.7.2 - # via shinywidgets + # via + # nbformat + # shinywidgets jupyterlab-widgets==3.0.13 # via ipywidgets +jupytext==1.16.4 + # via -r requirements-dev.in linkify-it-py==2.0.3 # via shiny markdown-it-py==3.0.0 # via + # jupytext # mdit-py-plugins # shiny matplotlib-inline==0.1.7 @@ -76,13 +91,17 @@ matplotlib-inline==0.1.7 mccabe==0.7.0 # via flake8 mdit-py-plugins==0.4.2 - # via shiny + # via + # jupytext + # shiny mdurl==0.1.2 # via markdown-it-py mypy==1.11.2 # via -r requirements-dev.in mypy-extensions==1.0.0 # via mypy +nbformat==5.10.4 + # via jupytext numpy==1.26.4 # via # opendp @@ -96,6 +115,7 @@ packaging==24.1 # via # build # htmltools + # jupytext # pytest # shiny parso==0.8.4 @@ -139,12 +159,22 @@ python-dateutil==2.9.0.post0 # via shinywidgets python-multipart==0.0.9 # via shiny +pyyaml==6.0.2 + # via jupytext questionary==2.0.1 # via shiny randomgen==2.0.1 # via opendp +referencing==0.35.1 + # via + # jsonschema + # jsonschema-specifications requests==2.32.3 # via flit +rpds-py==0.20.0 + # via + # jsonschema + # referencing scikit-learn==1.5.2 # via opendp scipy==1.13.1 @@ -178,6 +208,7 @@ traitlets==5.14.3 # ipywidgets # jupyter-core # matplotlib-inline + # nbformat typing-extensions==4.12.2 # via # htmltools