nbsync 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {nbsync-0.1.0 → nbsync-0.1.1}/PKG-INFO +1 -1
  2. nbsync-0.1.1/docs/getting-started/configuration.md +35 -0
  3. {nbsync-0.1.0/docs-gen → nbsync-0.1.1/docs}/getting-started/first-steps.md +57 -62
  4. nbsync-0.1.1/docs/getting-started/installation.md +35 -0
  5. {nbsync-0.1.0 → nbsync-0.1.1}/docs/index.md +35 -17
  6. {nbsync-0.1.0 → nbsync-0.1.1}/mkdocs.yaml +4 -21
  7. nbsync-0.1.1/notebooks/analysis.ipynb +69 -0
  8. {nbsync-0.1.0 → nbsync-0.1.1}/pyproject.toml +2 -1
  9. {nbsync-0.1.0 → nbsync-0.1.1}/scripts/plot.py +1 -1
  10. nbsync-0.1.1/scripts/plotting.py +19 -0
  11. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/cell.py +28 -5
  12. nbsync-0.1.1/tests/__init__.py +0 -0
  13. {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_cell.py +5 -5
  14. nbsync-0.1.0/docs-gen/examples/advanced.md +0 -256
  15. nbsync-0.1.0/docs-gen/examples/basic.md +0 -151
  16. nbsync-0.1.0/docs-gen/examples/tables.md +0 -143
  17. nbsync-0.1.0/docs-gen/features/dynamic.md +0 -127
  18. nbsync-0.1.0/docs-gen/features/images.md +0 -112
  19. nbsync-0.1.0/docs-gen/features/markdown.md +0 -134
  20. nbsync-0.1.0/docs-gen/features/overview.md +0 -62
  21. nbsync-0.1.0/docs-gen/features/python.md +0 -118
  22. nbsync-0.1.0/docs-gen/getting-started/configuration.md +0 -151
  23. nbsync-0.1.0/docs-gen/getting-started/installation.md +0 -99
  24. nbsync-0.1.0/docs-gen/usage/execution.md +0 -211
  25. nbsync-0.1.0/docs-gen/usage/markdown-files.md +0 -203
  26. nbsync-0.1.0/docs-gen/usage/notebook.md +0 -162
  27. nbsync-0.1.0/docs-gen/usage/python-files.md +0 -172
  28. nbsync-0.1.0/docs-gen/usage/tabbed-display.md +0 -247
  29. nbsync-0.1.0/docs-old/examples/format.md +0 -35
  30. nbsync-0.1.0/docs-old/examples/html.md +0 -15
  31. nbsync-0.1.0/docs-old/index.md +0 -122
  32. nbsync-0.1.0/docs-old/usage/class.md +0 -147
  33. nbsync-0.1.0/docs-old/usage/execute.md +0 -100
  34. nbsync-0.1.0/docs-old/usage/notebook.md +0 -159
  35. nbsync-0.1.0/notebooks/class.ipynb +0 -70
  36. nbsync-0.1.0/notebooks/format/html.ipynb +0 -136
  37. nbsync-0.1.0/notebooks/format/pdf.ipynb +0 -71
  38. nbsync-0.1.0/notebooks/format/png.ipynb +0 -59
  39. nbsync-0.1.0/notebooks/format/svg.ipynb +0 -442
  40. nbsync-0.1.0/notebooks/image.ipynb +0 -83
  41. nbsync-0.1.0/notebooks/index.ipynb +0 -59
  42. {nbsync-0.1.0 → nbsync-0.1.1}/.devcontainer/devcontainer.json +0 -0
  43. {nbsync-0.1.0 → nbsync-0.1.1}/.devcontainer/postCreate.sh +0 -0
  44. {nbsync-0.1.0 → nbsync-0.1.1}/.devcontainer/starship.toml +0 -0
  45. {nbsync-0.1.0 → nbsync-0.1.1}/.gitattributes +0 -0
  46. {nbsync-0.1.0 → nbsync-0.1.1}/.github/workflows/ci.yaml +0 -0
  47. {nbsync-0.1.0 → nbsync-0.1.1}/.github/workflows/docs.yaml +0 -0
  48. {nbsync-0.1.0 → nbsync-0.1.1}/.github/workflows/publish.yaml +0 -0
  49. {nbsync-0.1.0 → nbsync-0.1.1}/.gitignore +0 -0
  50. {nbsync-0.1.0 → nbsync-0.1.1}/LICENSE +0 -0
  51. {nbsync-0.1.0 → nbsync-0.1.1}/README.md +0 -0
  52. {nbsync-0.1.0 → nbsync-0.1.1}/scripts/__init__.py +0 -0
  53. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/__init__.py +0 -0
  54. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/logger.py +0 -0
  55. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/markdown.py +0 -0
  56. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/notebook.py +0 -0
  57. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/plugin.py +0 -0
  58. /nbsync-0.1.0/tests/__init__.py → /nbsync-0.1.1/src/nbsync/py.typed +0 -0
  59. {nbsync-0.1.0 → nbsync-0.1.1}/src/nbsync/sync.py +0 -0
  60. {nbsync-0.1.0 → nbsync-0.1.1}/tests/conftest.py +0 -0
  61. {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_markdown.py +0 -0
  62. {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_notebook.py +0 -0
  63. {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_plugin.py +0 -0
  64. {nbsync-0.1.0 → nbsync-0.1.1}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbsync
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: MkDocs plugin treating Jupyter notebooks, Python scripts and Markdown files as first-class citizens for documentation with dynamic execution and real-time synchronization
5
5
  Project-URL: Documentation, https://daizutabi.github.io/nbsync/
6
6
  Project-URL: Source, https://github.com/daizutabi/nbsync
@@ -0,0 +1,35 @@
1
+ # Configuration
2
+
3
+ Configuring nbsync for your MkDocs site is simple but powerful, allowing you to
4
+ customize how notebooks and Python files are integrated with your documentation.
5
+
6
+ ## Basic Configuration
7
+
8
+ To use nbsync with MkDocs, add it to your `mkdocs.yml` file:
9
+
10
+ ```yaml
11
+ plugins:
12
+ - search
13
+ - nbsync
14
+ ```
15
+
16
+ This minimal configuration uses all the default settings.
17
+
18
+ ## Source Directory Configuration
19
+
20
+ Specify where nbsync should look for notebooks and Python files:
21
+
22
+ ```yaml
23
+ plugins:
24
+ - search
25
+ - nbsync:
26
+ src_dir:
27
+ - ../notebooks # Path to notebooks directory
28
+ - ../scripts # Path to Python scripts
29
+ ```
30
+
31
+ The `src_dir` option can be:
32
+
33
+ - A single path as a string
34
+ - A list of paths
35
+ - Relative to your docs directory
@@ -28,34 +28,24 @@ Update your `mkdocs.yml` to include nbsync:
28
28
  ```yaml
29
29
  site_name: My Documentation
30
30
  theme:
31
- name: material
31
+ name: material
32
32
 
33
33
  plugins:
34
- - search
35
- - nbsync:
36
- src_dir:
37
- - ../notebooks
38
- - ../scripts
34
+ - search
35
+ - nbsync:
36
+ src_dir:
37
+ - ../notebooks
38
+ - ../scripts
39
39
  ```
40
40
 
41
41
  ## Creating Your First Integration
42
42
 
43
43
  ### 1. Prepare a Jupyter Notebook
44
44
 
45
- Create or use an existing notebook with visualizations. Tag cells you want to
46
- reference with a comment:
45
+ Create or use an existing notebook with visualizations.
46
+ Tag cells you want to reference with a comment:
47
47
 
48
- ```python
49
- # In your notebook
50
- # #simple-plot
51
- import matplotlib.pyplot as plt
52
- import numpy as np
53
-
54
- x = np.linspace(0, 10, 100)
55
- plt.figure(figsize=(8, 4))
56
- plt.plot(x, np.sin(x))
57
- plt.title("Simple Sine Wave")
58
- ```
48
+ ![](analysis.ipynb){#simple-plot source="only" identifier="1" title="notebooks/analysis.ipynb"}
59
49
 
60
50
  ### 2. Reference in Your Documentation
61
51
 
@@ -66,32 +56,17 @@ In one of your markdown files (e.g., `docs/index.md`), add:
66
56
 
67
57
  Here's a visualization from our analysis:
68
58
 
69
- ![Sine wave plot](../notebooks/analysis.ipynb){#simple-plot}
59
+ ![Sine wave plot](analysis.ipynb){#simple-plot}
70
60
  ```
71
61
 
62
+ ![Sine wave plot](analysis.ipynb){#simple-plot}
63
+
72
64
  ### 3. Create a Python Script
73
65
 
74
66
  Create a file `scripts/plotting.py` with visualization functions:
75
67
 
76
- ```python
77
- # scripts/plotting.py
78
- import matplotlib.pyplot as plt
79
- import numpy as np
80
-
81
- def plot_sine(frequency=1):
82
- """Plot a sine wave with given frequency."""
83
- x = np.linspace(0, 10, 100)
84
- plt.figure(figsize=(6, 3))
85
- plt.plot(x, np.sin(frequency * x))
86
- plt.title(f"Sine Wave (f={frequency})")
87
- plt.ylim(-1.2, 1.2)
88
-
89
- def plot_histogram(bins=20):
90
- """Plot a histogram of random data."""
91
- data = np.random.randn(1000)
92
- plt.figure(figsize=(6, 3))
93
- plt.hist(data, bins=bins)
94
- plt.title(f"Histogram (bins={bins})")
68
+ ```python title="scripts/plotting.py"
69
+ --8<-- "scripts/plotting.py"
95
70
  ```
96
71
 
97
72
  ### 4. Use Functions in Your Documentation
@@ -103,7 +78,7 @@ Create a new file `docs/examples.md`:
103
78
 
104
79
  Let's demonstrate different plots:
105
80
 
106
- ![](../scripts/plotting.py){#.}
81
+ ![](plotting.py){#.}
107
82
 
108
83
  ## Sine Waves
109
84
 
@@ -118,6 +93,16 @@ Let's demonstrate different plots:
118
93
  | ![](){`plot_histogram(20)`} | ![](){`plot_histogram(50)`} |
119
94
  ```
120
95
 
96
+ ![](plotting.py){#.}
97
+
98
+ | Frequency = 1 | Frequency = 2 |
99
+ | :-------------------: | :-------------------: |
100
+ | ![](){`plot_sine(1)`} | ![](){`plot_sine(2)`} |
101
+
102
+ | 20 Bins | 50 Bins |
103
+ | :-------------------------: | :-------------------------: |
104
+ | ![](){`plot_histogram(20)`} | ![](){`plot_histogram(50)`} |
105
+
121
106
  ### 5. Create a Markdown-Based Notebook
122
107
 
123
108
  Create a file `docs/custom.md`:
@@ -127,8 +112,7 @@ Create a file `docs/custom.md`:
127
112
 
128
113
  Here's an analysis created directly in markdown:
129
114
 
130
- ````markdown source="tabbed-nbsync"
131
- ```python .md#data
115
+ ```python .md#_
132
116
  import numpy as np
133
117
  import pandas as pd
134
118
 
@@ -139,35 +123,50 @@ data = pd.DataFrame({
139
123
  'group': np.random.choice(['A', 'B', 'C'], 100)
140
124
  })
141
125
  ```
142
- ````
143
- ````
144
126
 
145
127
  ```python .md#scatter
146
128
  import matplotlib.pyplot as plt
147
129
  import seaborn as sns
148
130
 
149
- plt.figure(figsize=(8, 6))
131
+ plt.figure(figsize=(3, 2))
150
132
  sns.scatterplot(data=data, x='x', y='y', hue='group')
151
133
  plt.title('Scatter Plot by Group')
152
134
  ```
153
135
 
154
- ![Scatter plot](){#scatter}
136
+ ![Scatter plot](.md){#scatter}
137
+ ````
155
138
 
139
+ ```python .md#_
140
+ import numpy as np
141
+ import pandas as pd
142
+
143
+ # Generate sample data
144
+ data = pd.DataFrame({
145
+ 'x': np.random.randn(100),
146
+ 'y': np.random.randn(100),
147
+ 'group': np.random.choice(['A', 'B', 'C'], 100)
148
+ })
156
149
  ```
157
150
 
151
+ ```python .md#scatter
152
+ import matplotlib.pyplot as plt
153
+ import seaborn as sns
154
+
155
+ plt.figure(figsize=(3, 2))
156
+ sns.scatterplot(data=data, x='x', y='y', hue='group')
157
+ plt.title('Scatter Plot by Group')
158
158
  ```
159
159
 
160
+ ![Scatter plot](){#scatter}
161
+
160
162
  ## 6. Run Your Documentation
161
163
 
162
164
  Start the MkDocs development server:
163
165
 
164
166
  ```bash
165
- mkdocs serve
167
+ mkdocs serve --open
166
168
  ```
167
169
 
168
- Navigate to http://localhost:8000 to see your documentation with the integrated
169
- visualizations.
170
-
171
170
  ## Next Steps
172
171
 
173
172
  Now that you have the basics working, you can:
@@ -175,25 +174,21 @@ Now that you have the basics working, you can:
175
174
  1. [Explore advanced notebook features](../usage/notebook.md)
176
175
  2. [Learn about Python file integration](../usage/python-files.md)
177
176
  3. [Discover markdown-based notebooks](../usage/markdown-files.md)
178
- 4. [See real-world examples](../examples/basic.md)
179
177
 
180
178
  ## Troubleshooting
181
179
 
182
180
  ### Common Issues
183
181
 
184
182
  1. **Images Not Showing**:
185
-
186
- - Check paths in your configuration
187
- - Ensure notebooks have correctly tagged cells
188
- - Verify Python dependencies are installed
183
+ - Check paths in your configuration
184
+ - Ensure notebooks have correctly tagged cells
185
+ - Verify Python dependencies are installed
189
186
 
190
187
  2. **Execution Errors**:
191
-
192
- - Check the console output for error messages
193
- - Ensure your environment has all required packages
194
- - Increase timeout if operations are complex
188
+ - Check the console output for error messages
189
+ - Ensure your environment has all required packages
195
190
 
196
191
  3. **Changes Not Reflecting**:
197
- - Hard refresh your browser
198
- - Restart the MkDocs server
199
- - Check file paths and identifiers
192
+ - Hard refresh your browser
193
+ - Restart the MkDocs server
194
+ - Check file paths and identifiers
@@ -0,0 +1,35 @@
1
+ # Installation
2
+
3
+ Installing nbsync is straightforward and can be done using uv or pip,
4
+ the Python package manager.
5
+
6
+ ## Prerequisites
7
+
8
+ Before installing nbsync, ensure you have the following:
9
+
10
+ - Python 3.10 or higher
11
+ - uv or pip (Python package manager)
12
+ - MkDocs 1.6 or higher (documentation generator)
13
+
14
+ ## Basic Installation
15
+
16
+ Install nbsync using uv or pip:
17
+
18
+ ```bash
19
+ uv pip install nbsync
20
+ # or
21
+ pip install nbsync
22
+ ```
23
+
24
+ This command installs the latest stable version of nbsync and its core
25
+ dependencies.
26
+
27
+ ## Installation of nbconvert
28
+
29
+ For dynamic execution functionality, install nbconvert:
30
+
31
+ ```bash
32
+ uv pip install nbconvert
33
+ # or
34
+ pip install nbconvert
35
+ ```
@@ -23,18 +23,43 @@
23
23
 
24
24
  ## What is nbsync?
25
25
 
26
- nbsync is an innovative plugin that seamlessly connects Jupyter notebooks with
27
- MkDocs documentation. Going beyond traditional notebook integration, it provides
28
- functionality to generate and execute notebooks directly from markdown (.md)
29
- files and Python (.py) files.
26
+ nbsync is an innovative MkDocs plugin that treats Jupyter notebooks,
27
+ Python scripts, and Markdown files as first-class citizens for
28
+ documentation. Unlike traditional approaches, nbsync provides equal
29
+ capabilities across all file formats, enabling seamless integration
30
+ and dynamic execution with real-time synchronization.
30
31
 
31
32
  It solves common challenges faced by data scientists, researchers, and technical
32
33
  writers:
33
34
 
34
35
  - **Development happens in notebooks** - ideal for experimentation and visualization
35
36
  - **Documentation lives in markdown** - perfect for narrative and explanation
37
+ - **Code resides in Python files** - organized and version-controlled
36
38
  - **Traditional integration is challenging** - screenshots break, exports get outdated
37
39
 
40
+ ## Inspiration & Comparison
41
+
42
+ nbsync was inspired by and builds upon the excellent work of two MkDocs
43
+ plugins:
44
+
45
+ - [**markdown-exec**](https://pawamoy.github.io/markdown-exec/) - Provides utilities to execute code blocks in Markdown files
46
+ - [**mkdocs-jupyter**](https://mkdocs-jupyter.danielfrg.com/) - Enables embedding Jupyter notebooks in MkDocs
47
+
48
+ While these plugins offer great functionality, nbsync takes a unified
49
+ approach by:
50
+
51
+ 1. **Equal treatment** - Unlike other solutions that prioritize one format, nbsync treats Jupyter notebooks, Python scripts, and Markdown files equally as first-class citizens
52
+ 2. **Real-time synchronization** - Changes to source files are immediately reflected in documentation
53
+ 3. **Seamless integration** - Consistent syntax across all file formats allows for flexible documentation workflows
54
+ 4. **Image syntax code execution** - Unique ability to execute code and embed visualizations anywhere Markdown image syntax (`![alt](url)`) is valid, including tables, lists, and complex layouts
55
+
56
+ ## Acknowledgements
57
+
58
+ The development of nbsync would not have been possible without the
59
+ groundwork laid by markdown-exec and mkdocs-jupyter. We extend our
60
+ sincere gratitude to the developers of these projects for their
61
+ innovative contributions to the documentation ecosystem.
62
+
38
63
  ## Key Features
39
64
 
40
65
  ### Notebooks from Markdown
@@ -44,20 +69,20 @@ documentation. Present code and its output results concisely with tabbed
44
69
  display.
45
70
 
46
71
  ````markdown source="tabbed-nbsync"
47
- ```python .md#plot source="on"
72
+ ```python .md#plot
48
73
  import matplotlib.pyplot as plt
49
74
 
50
75
  fig, ax = plt.subplots(figsize=(2, 1))
51
76
  ax.plot([1, 3, 3, 4])
52
77
  ```
53
78
 
54
- ![Plot result](){#plot source="on"}
79
+ ![Plot result](){#plot source="above"}
55
80
  ````
56
81
 
57
82
  ### Python File Integration
58
83
 
59
- Directly reference external Python files and reuse defined functions or classes.
60
- Avoid code duplication and improve maintainability.
84
+ Directly reference external Python files and reuse defined functions or
85
+ classes. Avoid code duplication and improve maintainability.
61
86
 
62
87
  ```python title="plot.py"
63
88
  --8<-- "scripts/plot.py"
@@ -82,7 +107,8 @@ layouts.
82
107
  ### Dynamic Updates and Execution
83
108
 
84
109
  Automatic synchronization between notebooks and documentation ensures code
85
- changes are reflected in real-time. View changes instantly in MkDocs serve mode.
110
+ changes are reflected in real-time. View changes instantly in MkDocs serve
111
+ mode.
86
112
 
87
113
  ## Getting Started
88
114
 
@@ -91,11 +117,3 @@ Follow these steps to get started with nbsync:
91
117
  1. [Installation](getting-started/installation.md)
92
118
  2. [Configuration](getting-started/configuration.md)
93
119
  3. [First Steps](getting-started/first-steps.md)
94
-
95
- ## Examples
96
-
97
- Explore the possibilities of nbsync through practical examples:
98
-
99
- - [Basic Usage](examples/basic.md)
100
- - [Visualizations in Tables](examples/tables.md)
101
- - [Advanced Examples](examples/advanced.md)
@@ -39,7 +39,6 @@ theme:
39
39
  - navigation.tracking
40
40
  - search.highlight
41
41
  - search.suggest
42
- - toc.follow
43
42
  plugins:
44
43
  - search
45
44
  - nbsync:
@@ -64,23 +63,7 @@ markdown_extensions:
64
63
  permalink: true
65
64
  nav:
66
65
  - Home: index.md
67
- # - Features:
68
- # - features/overview.md
69
- # - features/markdown.md
70
- # - features/python.md
71
- # - features/images.md
72
- # - features/dynamic.md
73
- # - Getting Started:
74
- # - getting-started/installation.md
75
- # - getting-started/configuration.md
76
- # - getting-started/first-steps.md
77
- # - Usage:
78
- # - usage/notebook.md
79
- # - usage/python-files.md
80
- # - usage/markdown-files.md
81
- # - usage/execution.md
82
- # - usage/tabbed-display.md
83
- # - Examples:
84
- # - examples/basic.md
85
- # - examples/tables.md
86
- # - examples/advanced.md
66
+ - Getting Started:
67
+ - getting-started/installation.md
68
+ - getting-started/configuration.md
69
+ - getting-started/first-steps.md
@@ -0,0 +1,69 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 5,
6
+ "metadata": {},
7
+ "outputs": [
8
+ {
9
+ "data": {
10
+ "text/plain": [
11
+ "Text(0.5, 1.0, 'Simple Sine Wave')"
12
+ ]
13
+ },
14
+ "execution_count": 5,
15
+ "metadata": {},
16
+ "output_type": "execute_result"
17
+ },
18
+ {
19
+ "data": {
20
+ "image/png": "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",
21
+ "text/plain": [
22
+ "<Figure size 300x150 with 1 Axes>"
23
+ ]
24
+ },
25
+ "metadata": {},
26
+ "output_type": "display_data"
27
+ }
28
+ ],
29
+ "source": [
30
+ "# #simple-plot\n",
31
+ "import matplotlib.pyplot as plt\n",
32
+ "import numpy as np\n",
33
+ "\n",
34
+ "x = np.linspace(0, 10, 100)\n",
35
+ "plt.figure(figsize=(3, 1.5))\n",
36
+ "plt.plot(x, np.sin(x))\n",
37
+ "plt.title(\"Simple Sine Wave\")"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "metadata": {},
44
+ "outputs": [],
45
+ "source": []
46
+ }
47
+ ],
48
+ "metadata": {
49
+ "kernelspec": {
50
+ "display_name": ".venv",
51
+ "language": "python",
52
+ "name": "python3"
53
+ },
54
+ "language_info": {
55
+ "codemirror_mode": {
56
+ "name": "ipython",
57
+ "version": 3
58
+ },
59
+ "file_extension": ".py",
60
+ "mimetype": "text/x-python",
61
+ "name": "python",
62
+ "nbconvert_exporter": "python",
63
+ "pygments_lexer": "ipython3",
64
+ "version": "3.13.3"
65
+ }
66
+ },
67
+ "nbformat": 4,
68
+ "nbformat_minor": 2
69
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nbsync"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "MkDocs plugin treating Jupyter notebooks, Python scripts and Markdown files as first-class citizens for documentation with dynamic execution and real-time synchronization"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -55,6 +55,7 @@ dev = [
55
55
  "pytest-randomly>=3.16.0",
56
56
  "pytest-xdist>=3.6.1",
57
57
  "ruff>=0.11.4",
58
+ "seaborn>=0.13.2",
58
59
  ]
59
60
 
60
61
  [tool.pytest.ini_options]
@@ -4,7 +4,7 @@ import numpy as np
4
4
 
5
5
  def plot(func):
6
6
  x = np.linspace(0, 360)
7
- y = func(np.deg2rad(x))
7
+ y = func(np.radians(x))
8
8
  fig, ax = plt.subplots(figsize=(2, 1))
9
9
  ax.plot(x, y)
10
10
  ax.set_title(f"Plot {func.__name__}")
@@ -0,0 +1,19 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+
4
+
5
+ def plot_sine(frequency=1):
6
+ """Plot a sine wave with given frequency."""
7
+ x = np.linspace(0, 10, 100)
8
+ plt.figure(figsize=(2, 1.2))
9
+ plt.plot(x, np.sin(frequency * x))
10
+ plt.title(f"Sine Wave (f={frequency})")
11
+ plt.ylim(-1.2, 1.2)
12
+
13
+
14
+ def plot_histogram(bins=20):
15
+ """Plot a histogram of random data."""
16
+ data = np.random.randn(1000)
17
+ plt.figure(figsize=(2, 1.2))
18
+ plt.hist(data, bins=bins)
19
+ plt.title(f"Histogram (bins={bins})")
@@ -30,22 +30,35 @@ class Cell:
30
30
  def convert(self) -> str:
31
31
  kind = self.image.attributes.pop("source", "")
32
32
  tabs = self.image.attributes.pop("tabs", "")
33
+ identifier = self.image.attributes.pop("identifier", "")
33
34
 
34
35
  if "/" not in self.mime or not self.content or kind == "source-only":
35
36
  if self.image.source:
36
- source = get_source(self, include_attrs=True)
37
+ source = get_source(
38
+ self,
39
+ include_attrs=True,
40
+ include_identifier=bool(identifier),
41
+ )
37
42
  kind = "only"
38
43
  else:
39
44
  source = ""
40
45
  result, self.image.url = "", ""
41
46
 
42
47
  elif self.mime.startswith("text/") and isinstance(self.content, str):
43
- source = get_source(self, include_attrs=True)
48
+ source = get_source(
49
+ self,
50
+ include_attrs=True,
51
+ include_identifier=bool(identifier),
52
+ )
44
53
  result, self.image.url = self.content, ""
45
54
  result = result.rstrip()
46
55
 
47
56
  else:
48
- source = get_source(self, include_attrs=False)
57
+ source = get_source(
58
+ self,
59
+ include_attrs=False,
60
+ include_identifier=bool(identifier),
61
+ )
49
62
  result = get_result(self)
50
63
 
51
64
  if markdown := get_markdown(kind, source, result, tabs):
@@ -54,12 +67,22 @@ class Cell:
54
67
  return "" # no cov
55
68
 
56
69
 
57
- def get_source(cell: Cell, *, include_attrs: bool = False) -> str:
70
+ def get_source(
71
+ cell: Cell,
72
+ *,
73
+ include_attrs: bool = False,
74
+ include_identifier: bool = False,
75
+ ) -> str:
58
76
  attrs = [cell.language]
59
77
  if include_attrs:
60
78
  attrs.extend(cell.image.iter_parts())
61
79
  attr = " ".join(attrs)
62
- return f"```{attr}\n{cell.image.source}\n```"
80
+
81
+ source = cell.image.source
82
+ if include_identifier:
83
+ source = f"# #{cell.image.identifier}\n{source}"
84
+
85
+ return f"```{attr}\n{source}\n```"
63
86
 
64
87
 
65
88
  def get_result(cell: Cell) -> str:
File without changes
@@ -54,19 +54,19 @@ def test_empty(convert):
54
54
  def test_image(convert):
55
55
  x = convert("![a](a.ipynb){#fig a b=c}")
56
56
  assert x.startswith("![a]")
57
- assert x.endswith(".png){#fig a b=c}")
57
+ assert x.endswith('.png){#fig a b="c"}')
58
58
 
59
59
 
60
60
  def test_func(convert):
61
- x = convert("![a](a.ipynb){#func a=b c}")
62
- assert x == "```python c a=b\ndef f():\n pass\n```"
61
+ x = convert("![a](a.ipynb){#func a=b c identifier='1'}")
62
+ assert x == '```python c a="b"\n# #func\ndef f():\n pass\n```'
63
63
 
64
64
 
65
65
  @pytest.mark.parametrize("kind", ["above", "on", "1"])
66
66
  def test_above(convert, kind):
67
- x = convert(f"![a](a.ipynb){{#fig source='{kind}' a b=c}}")
67
+ x = convert(f"![a](a.ipynb){{#fig source='{kind}' a b='c'}}")
68
68
  assert x.startswith("```python\n")
69
- assert x.endswith(".png){#fig a b=c}")
69
+ assert x.endswith('.png){#fig a b="c"}')
70
70
 
71
71
 
72
72
  def test_below(convert):