nbsync 0.1.4__tar.gz → 0.2.0__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.
- {nbsync-0.1.4 → nbsync-0.2.0}/.github/workflows/docs.yaml +1 -1
- {nbsync-0.1.4 → nbsync-0.2.0}/PKG-INFO +29 -19
- {nbsync-0.1.4 → nbsync-0.2.0}/README.md +25 -14
- nbsync-0.2.0/docs/index.md +1 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/mkdocs.yaml +3 -8
- {nbsync-0.1.4 → nbsync-0.2.0}/pyproject.toml +5 -21
- {nbsync-0.1.4 → nbsync-0.2.0}/src/nbsync/cell.py +1 -2
- nbsync-0.2.0/src/nbsync/logger.py +32 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/src/nbsync/sync.py +1 -2
- nbsync-0.2.0/tests/test_logger.py +64 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/tests/test_sync.py +7 -0
- nbsync-0.1.4/docs/getting-started/configuration.md +0 -35
- nbsync-0.1.4/docs/getting-started/first-steps.md +0 -186
- nbsync-0.1.4/docs/getting-started/installation.md +0 -35
- nbsync-0.1.4/docs/index.md +0 -138
- nbsync-0.1.4/notebooks/analysis.ipynb +0 -69
- nbsync-0.1.4/scripts/plot.py +0 -15
- nbsync-0.1.4/scripts/plotting.py +0 -19
- nbsync-0.1.4/src/nbsync/logger.py +0 -5
- nbsync-0.1.4/src/nbsync/plugin.py +0 -96
- nbsync-0.1.4/tests/__init__.py +0 -0
- nbsync-0.1.4/tests/test_plugin.py +0 -108
- {nbsync-0.1.4 → nbsync-0.2.0}/.devcontainer/devcontainer.json +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/.devcontainer/postCreate.sh +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/.devcontainer/starship.toml +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/.gitattributes +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/.github/workflows/ci.yaml +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/.github/workflows/publish.yaml +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/.gitignore +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/LICENSE +0 -0
- {nbsync-0.1.4/scripts → nbsync-0.2.0/src/nbsync}/__init__.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/src/nbsync/markdown.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/src/nbsync/notebook.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/src/nbsync/py.typed +0 -0
- {nbsync-0.1.4/src/nbsync → nbsync-0.2.0/tests}/__init__.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/tests/conftest.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/tests/test_cell.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/tests/test_markdown.py +0 -0
- {nbsync-0.1.4 → nbsync-0.2.0}/tests/test_notebook.py +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nbsync
|
3
|
-
Version: 0.
|
4
|
-
Summary:
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: A core library to synchronize Jupyter notebooks and Markdown documents, enabling seamless integration and dynamic content execution
|
5
5
|
Project-URL: Documentation, https://daizutabi.github.io/nbsync/
|
6
6
|
Project-URL: Source, https://github.com/daizutabi/nbsync
|
7
7
|
Project-URL: Issues, https://github.com/daizutabi/nbsync/issues
|
@@ -28,7 +28,7 @@ License: MIT License
|
|
28
28
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
29
29
|
SOFTWARE.
|
30
30
|
License-File: LICENSE
|
31
|
-
Keywords: documentation,dynamic-execution,jupyter,markdown,
|
31
|
+
Keywords: documentation,dynamic-execution,jupyter,markdown,notebook,python,real-time-sync,visualization
|
32
32
|
Classifier: Development Status :: 4 - Beta
|
33
33
|
Classifier: Programming Language :: Python
|
34
34
|
Classifier: Programming Language :: Python :: 3.10
|
@@ -39,8 +39,7 @@ Classifier: Topic :: Documentation
|
|
39
39
|
Classifier: Topic :: Software Development :: Documentation
|
40
40
|
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
41
41
|
Requires-Python: >=3.10
|
42
|
-
Requires-Dist:
|
43
|
-
Requires-Dist: nbstore>=0.4.7
|
42
|
+
Requires-Dist: nbstore>=0.4.11
|
44
43
|
Description-Content-Type: text/markdown
|
45
44
|
|
46
45
|
# nbsync
|
@@ -50,11 +49,9 @@ Description-Content-Type: text/markdown
|
|
50
49
|
[![Build Status][GHAction-image]][GHAction-link]
|
51
50
|
[![Coverage Status][codecov-image]][codecov-link]
|
52
51
|
|
53
|
-
<strong>Connect Jupyter notebooks
|
52
|
+
<strong>Connect Jupyter notebooks and Markdown documents</strong>
|
54
53
|
|
55
|
-
nbsync is a
|
56
|
-
visualizations in your documentation, solving the disconnect between
|
57
|
-
code development and documentation.
|
54
|
+
nbsync is a core library that seamlessly bridges Jupyter notebooks and Markdown documents, enabling dynamic content synchronization and execution.
|
58
55
|
|
59
56
|
## Why Use nbsync?
|
60
57
|
|
@@ -68,7 +65,7 @@ Data scientists, researchers, and technical writers face a common dilemma:
|
|
68
65
|
|
69
66
|
### Our Solution
|
70
67
|
|
71
|
-
|
68
|
+
nbsync creates a live bridge between your notebooks and markdown documents by:
|
72
69
|
|
73
70
|
- **Keeping environments separate** - work in the tool best suited for each task
|
74
71
|
- **Maintaining connections** - reference specific figures from notebooks
|
@@ -86,7 +83,7 @@ This plugin creates a live bridge between your notebooks and documentation by:
|
|
86
83
|
|
87
84
|
- **Automatic Updates**:
|
88
85
|
When you modify your notebooks, your documentation updates
|
89
|
-
automatically
|
86
|
+
automatically.
|
90
87
|
|
91
88
|
- **Clean Source Documents**:
|
92
89
|
Your markdown remains readable and focused on content, without
|
@@ -104,14 +101,27 @@ This plugin creates a live bridge between your notebooks and documentation by:
|
|
104
101
|
pip install nbsync
|
105
102
|
```
|
106
103
|
|
107
|
-
### 2.
|
104
|
+
### 2. Basic Usage
|
108
105
|
|
109
|
-
|
106
|
+
```python
|
107
|
+
from nbsync.sync import Synchronizer
|
108
|
+
from nbstore import Store
|
109
|
+
|
110
|
+
# Initialize with a notebook store
|
111
|
+
store = Store("path/to/notebooks")
|
112
|
+
sync = Synchronizer(store)
|
113
|
+
|
114
|
+
# Process markdown with notebook references
|
115
|
+
markdown_text = """
|
116
|
+
# My Document
|
117
|
+
|
118
|
+
{#my-figure}
|
119
|
+
"""
|
110
120
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
121
|
+
# Convert markdown with notebook references to final output
|
122
|
+
for element in sync.convert(markdown_text):
|
123
|
+
# Process each element (string or Cell objects)
|
124
|
+
print(element)
|
115
125
|
```
|
116
126
|
|
117
127
|
### 3. Mark Figures in Your Notebook
|
@@ -140,14 +150,14 @@ Creating documentation and developing visualizations involve different
|
|
140
150
|
workflows and timeframes. When building visualizations in Jupyter notebooks,
|
141
151
|
you need rapid cycles of execution, verification, and modification.
|
142
152
|
|
143
|
-
|
153
|
+
nbsync is designed specifically to address these separation of
|
144
154
|
concerns, allowing you to:
|
145
155
|
|
146
156
|
- **Focus on code** in notebooks without documentation distractions
|
147
157
|
- **Focus on narrative** in markdown without code interruptions
|
148
158
|
- **Maintain powerful connections** between both environments
|
149
159
|
|
150
|
-
Each environment is optimized for its purpose, while
|
160
|
+
Each environment is optimized for its purpose, while nbsync
|
151
161
|
handles the integration automatically.
|
152
162
|
|
153
163
|
## Contributing
|
@@ -5,11 +5,9 @@
|
|
5
5
|
[![Build Status][GHAction-image]][GHAction-link]
|
6
6
|
[![Coverage Status][codecov-image]][codecov-link]
|
7
7
|
|
8
|
-
<strong>Connect Jupyter notebooks
|
8
|
+
<strong>Connect Jupyter notebooks and Markdown documents</strong>
|
9
9
|
|
10
|
-
nbsync is a
|
11
|
-
visualizations in your documentation, solving the disconnect between
|
12
|
-
code development and documentation.
|
10
|
+
nbsync is a core library that seamlessly bridges Jupyter notebooks and Markdown documents, enabling dynamic content synchronization and execution.
|
13
11
|
|
14
12
|
## Why Use nbsync?
|
15
13
|
|
@@ -23,7 +21,7 @@ Data scientists, researchers, and technical writers face a common dilemma:
|
|
23
21
|
|
24
22
|
### Our Solution
|
25
23
|
|
26
|
-
|
24
|
+
nbsync creates a live bridge between your notebooks and markdown documents by:
|
27
25
|
|
28
26
|
- **Keeping environments separate** - work in the tool best suited for each task
|
29
27
|
- **Maintaining connections** - reference specific figures from notebooks
|
@@ -41,7 +39,7 @@ This plugin creates a live bridge between your notebooks and documentation by:
|
|
41
39
|
|
42
40
|
- **Automatic Updates**:
|
43
41
|
When you modify your notebooks, your documentation updates
|
44
|
-
automatically
|
42
|
+
automatically.
|
45
43
|
|
46
44
|
- **Clean Source Documents**:
|
47
45
|
Your markdown remains readable and focused on content, without
|
@@ -59,14 +57,27 @@ This plugin creates a live bridge between your notebooks and documentation by:
|
|
59
57
|
pip install nbsync
|
60
58
|
```
|
61
59
|
|
62
|
-
### 2.
|
60
|
+
### 2. Basic Usage
|
63
61
|
|
64
|
-
|
62
|
+
```python
|
63
|
+
from nbsync.sync import Synchronizer
|
64
|
+
from nbstore import Store
|
65
|
+
|
66
|
+
# Initialize with a notebook store
|
67
|
+
store = Store("path/to/notebooks")
|
68
|
+
sync = Synchronizer(store)
|
69
|
+
|
70
|
+
# Process markdown with notebook references
|
71
|
+
markdown_text = """
|
72
|
+
# My Document
|
73
|
+
|
74
|
+
{#my-figure}
|
75
|
+
"""
|
65
76
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
77
|
+
# Convert markdown with notebook references to final output
|
78
|
+
for element in sync.convert(markdown_text):
|
79
|
+
# Process each element (string or Cell objects)
|
80
|
+
print(element)
|
70
81
|
```
|
71
82
|
|
72
83
|
### 3. Mark Figures in Your Notebook
|
@@ -95,14 +106,14 @@ Creating documentation and developing visualizations involve different
|
|
95
106
|
workflows and timeframes. When building visualizations in Jupyter notebooks,
|
96
107
|
you need rapid cycles of execution, verification, and modification.
|
97
108
|
|
98
|
-
|
109
|
+
nbsync is designed specifically to address these separation of
|
99
110
|
concerns, allowing you to:
|
100
111
|
|
101
112
|
- **Focus on code** in notebooks without documentation distractions
|
102
113
|
- **Focus on narrative** in markdown without code interruptions
|
103
114
|
- **Maintain powerful connections** between both environments
|
104
115
|
|
105
|
-
Each environment is optimized for its purpose, while
|
116
|
+
Each environment is optimized for its purpose, while nbsync
|
106
117
|
handles the integration automatically.
|
107
118
|
|
108
119
|
## Contributing
|
@@ -0,0 +1 @@
|
|
1
|
+
# nbsync
|
@@ -41,10 +41,8 @@ theme:
|
|
41
41
|
- search.suggest
|
42
42
|
plugins:
|
43
43
|
- search
|
44
|
-
-
|
45
|
-
|
46
|
-
- ../notebooks
|
47
|
-
- ../scripts
|
44
|
+
- mkapi:
|
45
|
+
debug: true
|
48
46
|
markdown_extensions:
|
49
47
|
- admonition
|
50
48
|
- pymdownx.emoji:
|
@@ -60,7 +58,4 @@ markdown_extensions:
|
|
60
58
|
alternate_style: true
|
61
59
|
nav:
|
62
60
|
- Home: index.md
|
63
|
-
-
|
64
|
-
- getting-started/installation.md
|
65
|
-
- getting-started/configuration.md
|
66
|
-
- getting-started/first-steps.md
|
61
|
+
- API: $api/nbsync.***
|
@@ -4,15 +4,14 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "nbsync"
|
7
|
-
version = "0.
|
8
|
-
description = "
|
7
|
+
version = "0.2.0"
|
8
|
+
description = "A core library to synchronize Jupyter notebooks and Markdown documents, enabling seamless integration and dynamic content execution"
|
9
9
|
readme = "README.md"
|
10
10
|
license = { file = "LICENSE" }
|
11
11
|
authors = [{ name = "daizutabi", email = "daizutabi@gmail.com" }]
|
12
12
|
keywords = [
|
13
13
|
"jupyter",
|
14
14
|
"notebook",
|
15
|
-
"mkdocs",
|
16
15
|
"documentation",
|
17
16
|
"markdown",
|
18
17
|
"python",
|
@@ -32,31 +31,23 @@ classifiers = [
|
|
32
31
|
"Topic :: Text Processing :: Markup :: Markdown",
|
33
32
|
]
|
34
33
|
requires-python = ">=3.10"
|
35
|
-
dependencies = ["
|
34
|
+
dependencies = ["nbstore>=0.4.11"]
|
36
35
|
|
37
36
|
[project.urls]
|
38
37
|
Documentation = "https://daizutabi.github.io/nbsync/"
|
39
38
|
Source = "https://github.com/daizutabi/nbsync"
|
40
39
|
Issues = "https://github.com/daizutabi/nbsync/issues"
|
41
40
|
|
42
|
-
[project.entry-points."mkdocs.plugins"]
|
43
|
-
nbsync = "nbsync.plugin:Plugin"
|
44
|
-
|
45
41
|
[dependency-groups]
|
46
42
|
dev = [
|
47
43
|
"ipykernel>=6.29.5",
|
48
44
|
"matplotlib>=3.10.1",
|
49
|
-
"mkdocs-material>=9.6.11",
|
50
45
|
"nbconvert>=7.16.6",
|
51
|
-
"pandas>=2.2",
|
52
|
-
"polars>=1.26",
|
53
|
-
"pytest-clarity>=1.0.1",
|
54
46
|
"pytest-cov>=6.1.0",
|
55
47
|
"pytest-randomly>=3.16.0",
|
56
|
-
"pytest-xdist>=3.6.1",
|
57
48
|
"ruff>=0.11.4",
|
58
|
-
"seaborn>=0.13.2",
|
59
49
|
]
|
50
|
+
docs = ["mkapi", "mkdocs-material"]
|
60
51
|
|
61
52
|
[tool.pytest.ini_options]
|
62
53
|
testpaths = ["src", "tests"]
|
@@ -81,11 +72,4 @@ unfixable = ["F401"]
|
|
81
72
|
ignore = ["ANN401", "ARG001", "ARG002", "C901", "D", "PLR0911", "S105"]
|
82
73
|
|
83
74
|
[tool.ruff.lint.per-file-ignores]
|
84
|
-
"**/tests/*" = ["ANN", "ARG", "D", "FBT", "PLR", "RUF", "S", "
|
85
|
-
"**/{notebooks,scripts}/*" = ["ANN", "ERA001", "NPY", "T201"]
|
86
|
-
|
87
|
-
[tool.pyright]
|
88
|
-
include = ["src", "tests"]
|
89
|
-
strictDictionaryInference = true
|
90
|
-
strictListInference = true
|
91
|
-
strictSetInference = true
|
75
|
+
"**/tests/*" = ["ANN", "ARG", "D", "FBT", "PGH003", "PLR", "RUF", "S", "SLF"]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from logging import Logger
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
_logger = logging.getLogger("nbsync")
|
8
|
+
|
9
|
+
|
10
|
+
def configure(logger: Logger | None = None) -> Logger:
|
11
|
+
global _logger # noqa: PLW0603
|
12
|
+
|
13
|
+
if logger:
|
14
|
+
_logger = logger
|
15
|
+
|
16
|
+
return _logger
|
17
|
+
|
18
|
+
|
19
|
+
def debug(msg: str, *args: Any, **kwargs: Any) -> None:
|
20
|
+
_logger.debug(msg, *args, **kwargs)
|
21
|
+
|
22
|
+
|
23
|
+
def info(msg: str, *args: Any, **kwargs: Any) -> None:
|
24
|
+
_logger.info(msg, *args, **kwargs)
|
25
|
+
|
26
|
+
|
27
|
+
def warning(msg: str, *args: Any, **kwargs: Any) -> None:
|
28
|
+
_logger.warning(msg, *args, **kwargs)
|
29
|
+
|
30
|
+
|
31
|
+
def error(msg: str, *args: Any, **kwargs: Any) -> None:
|
32
|
+
_logger.error(msg, *args, **kwargs)
|
@@ -10,12 +10,11 @@ from nbstore.markdown import CodeBlock, Image
|
|
10
10
|
from nbstore.notebook import get_language, get_mime_content, get_source
|
11
11
|
|
12
12
|
import nbsync.markdown
|
13
|
+
from nbsync import logger
|
13
14
|
from nbsync.cell import Cell
|
14
15
|
from nbsync.markdown import is_truelike
|
15
16
|
from nbsync.notebook import Notebook
|
16
17
|
|
17
|
-
from .logger import logger
|
18
|
-
|
19
18
|
if TYPE_CHECKING:
|
20
19
|
from collections.abc import Iterator
|
21
20
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import logging
|
2
|
+
from logging import Logger
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from nbsync import logger
|
7
|
+
|
8
|
+
|
9
|
+
@pytest.fixture
|
10
|
+
def reset_logger():
|
11
|
+
original_logger = logger._logger
|
12
|
+
|
13
|
+
yield
|
14
|
+
|
15
|
+
logger._logger = original_logger
|
16
|
+
|
17
|
+
|
18
|
+
def test_default_logger():
|
19
|
+
assert isinstance(logger._logger, Logger)
|
20
|
+
assert logger._logger.name == "nbsync"
|
21
|
+
|
22
|
+
|
23
|
+
def test_configure(reset_logger):
|
24
|
+
custom_logger = logging.getLogger("custom_logger")
|
25
|
+
|
26
|
+
result = logger.configure(custom_logger)
|
27
|
+
|
28
|
+
assert result is custom_logger
|
29
|
+
assert logger._logger is custom_logger
|
30
|
+
assert logger._logger.name == "custom_logger"
|
31
|
+
|
32
|
+
|
33
|
+
def test_configure_no_args(reset_logger):
|
34
|
+
custom_logger = logging.getLogger("custom_logger")
|
35
|
+
logger._logger = custom_logger
|
36
|
+
|
37
|
+
result = logger.configure()
|
38
|
+
|
39
|
+
assert result is custom_logger
|
40
|
+
assert logger._logger is custom_logger
|
41
|
+
|
42
|
+
|
43
|
+
def test_logging_methods(reset_logger, caplog):
|
44
|
+
caplog.set_level(logging.DEBUG, logger="nbsync")
|
45
|
+
|
46
|
+
debug_msg = "Debug message"
|
47
|
+
info_msg = "Info message"
|
48
|
+
warning_msg = "Warning message"
|
49
|
+
error_msg = "Error message"
|
50
|
+
|
51
|
+
logger.debug(debug_msg)
|
52
|
+
logger.info(info_msg)
|
53
|
+
logger.warning(warning_msg)
|
54
|
+
logger.error(error_msg)
|
55
|
+
|
56
|
+
assert debug_msg in caplog.text
|
57
|
+
assert info_msg in caplog.text
|
58
|
+
assert warning_msg in caplog.text
|
59
|
+
assert error_msg in caplog.text
|
60
|
+
|
61
|
+
assert (logger._logger.name, logging.DEBUG, debug_msg) in caplog.record_tuples
|
62
|
+
assert (logger._logger.name, logging.INFO, info_msg) in caplog.record_tuples
|
63
|
+
assert (logger._logger.name, logging.WARNING, warning_msg) in caplog.record_tuples
|
64
|
+
assert (logger._logger.name, logging.ERROR, error_msg) in caplog.record_tuples
|
@@ -167,3 +167,10 @@ def test_sync_notebook_not_found():
|
|
167
167
|
|
168
168
|
image = Image("abc", "id", [], {}, "", "a.ipynb")
|
169
169
|
assert convert(image, {}) == ""
|
170
|
+
|
171
|
+
|
172
|
+
def test_convert_no_id():
|
173
|
+
from nbsync.sync import convert
|
174
|
+
|
175
|
+
image = Image("abc", ".", [], {}, "", "a.ipynb")
|
176
|
+
assert convert(image, {}) == ""
|
@@ -1,35 +0,0 @@
|
|
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
|
@@ -1,186 +0,0 @@
|
|
1
|
-
# First Steps
|
2
|
-
|
3
|
-
Let's walk through a quick example of using nbsync to integrate notebooks with
|
4
|
-
your MkDocs documentation.
|
5
|
-
|
6
|
-
## Setting Up Your Project
|
7
|
-
|
8
|
-
Start with a typical MkDocs project structure:
|
9
|
-
|
10
|
-
```
|
11
|
-
my-project/
|
12
|
-
├── docs/
|
13
|
-
│ ├── index.md
|
14
|
-
│ └── ...
|
15
|
-
├── notebooks/
|
16
|
-
│ ├── analysis.ipynb
|
17
|
-
│ └── ...
|
18
|
-
├── scripts/
|
19
|
-
│ ├── plotting.py
|
20
|
-
│ └── ...
|
21
|
-
└── mkdocs.yml
|
22
|
-
```
|
23
|
-
|
24
|
-
## Configure MkDocs
|
25
|
-
|
26
|
-
Update your `mkdocs.yml` to include nbsync:
|
27
|
-
|
28
|
-
```yaml
|
29
|
-
site_name: My Documentation
|
30
|
-
theme:
|
31
|
-
name: material
|
32
|
-
|
33
|
-
plugins:
|
34
|
-
- search
|
35
|
-
- nbsync:
|
36
|
-
src_dir:
|
37
|
-
- ../notebooks
|
38
|
-
- ../scripts
|
39
|
-
```
|
40
|
-
|
41
|
-
## Creating Your First Integration
|
42
|
-
|
43
|
-
### 1. Prepare a Jupyter Notebook
|
44
|
-
|
45
|
-
Create or use an existing notebook with visualizations.
|
46
|
-
Tag cells you want to reference with a comment:
|
47
|
-
|
48
|
-
{#simple-plot source="only" identifier="1" title="notebooks/analysis.ipynb"}
|
49
|
-
|
50
|
-
### 2. Reference in Your Documentation
|
51
|
-
|
52
|
-
In one of your markdown files (e.g., `docs/index.md`), add:
|
53
|
-
|
54
|
-
```markdown
|
55
|
-
# My Project Documentation
|
56
|
-
|
57
|
-
Here's a visualization from our analysis:
|
58
|
-
|
59
|
-
{#simple-plot}
|
60
|
-
```
|
61
|
-
|
62
|
-
{#simple-plot}
|
63
|
-
|
64
|
-
### 3. Create a Python Script
|
65
|
-
|
66
|
-
Create a file `scripts/plotting.py` with visualization functions:
|
67
|
-
|
68
|
-
```python title="scripts/plotting.py"
|
69
|
-
--8<-- "scripts/plotting.py"
|
70
|
-
```
|
71
|
-
|
72
|
-
### 4. Use Functions in Your Documentation
|
73
|
-
|
74
|
-
Create a new file `docs/examples.md`:
|
75
|
-
|
76
|
-
```markdown
|
77
|
-
# Examples
|
78
|
-
|
79
|
-
Let's demonstrate different plots:
|
80
|
-
|
81
|
-
{#.}
|
82
|
-
|
83
|
-
## Sine Waves
|
84
|
-
|
85
|
-
| Frequency = 1 | Frequency = 2 |
|
86
|
-
| :-------------------: | :-------------------: |
|
87
|
-
| ![](){`plot_sine(1)`} | ![](){`plot_sine(2)`} |
|
88
|
-
|
89
|
-
## Histogram Examples
|
90
|
-
|
91
|
-
| 20 Bins | 50 Bins |
|
92
|
-
| :-------------------------: | :-------------------------: |
|
93
|
-
| ![](){`plot_histogram(20)`} | ![](){`plot_histogram(50)`} |
|
94
|
-
```
|
95
|
-
|
96
|
-
{#.}
|
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
|
-
|
106
|
-
### 5. Create a Markdown-Based Notebook
|
107
|
-
|
108
|
-
Create a file `docs/custom.md`:
|
109
|
-
|
110
|
-
````markdown
|
111
|
-
# Custom Analysis
|
112
|
-
|
113
|
-
Here's an analysis created directly in markdown:
|
114
|
-
|
115
|
-
```python .md#_
|
116
|
-
import numpy as np
|
117
|
-
import pandas as pd
|
118
|
-
|
119
|
-
# Generate sample data
|
120
|
-
data = pd.DataFrame({
|
121
|
-
'x': np.random.randn(100),
|
122
|
-
'y': np.random.randn(100),
|
123
|
-
'group': np.random.choice(['A', 'B', 'C'], 100)
|
124
|
-
})
|
125
|
-
```
|
126
|
-
|
127
|
-
```python .md#scatter
|
128
|
-
import matplotlib.pyplot as plt
|
129
|
-
import seaborn as sns
|
130
|
-
|
131
|
-
plt.figure(figsize=(3, 2))
|
132
|
-
sns.scatterplot(data=data, x='x', y='y', hue='group')
|
133
|
-
plt.title('Scatter Plot by Group')
|
134
|
-
```
|
135
|
-
|
136
|
-
{#scatter}
|
137
|
-
````
|
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
|
-
})
|
149
|
-
```
|
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
|
-
```
|
159
|
-
|
160
|
-
![Scatter plot](){#scatter}
|
161
|
-
|
162
|
-
### 7. Run Your Documentation
|
163
|
-
|
164
|
-
Start the MkDocs development server:
|
165
|
-
|
166
|
-
```bash
|
167
|
-
mkdocs serve --open
|
168
|
-
```
|
169
|
-
|
170
|
-
## Troubleshooting
|
171
|
-
|
172
|
-
### Common Issues
|
173
|
-
|
174
|
-
1. **Images Not Showing**:
|
175
|
-
- Check paths in your configuration
|
176
|
-
- Ensure notebooks have correctly tagged cells
|
177
|
-
- Verify Python dependencies are installed
|
178
|
-
|
179
|
-
2. **Execution Errors**:
|
180
|
-
- Check the console output for error messages
|
181
|
-
- Ensure your environment has all required packages
|
182
|
-
|
183
|
-
3. **Changes Not Reflecting**:
|
184
|
-
- Hard refresh your browser
|
185
|
-
- Restart the MkDocs server
|
186
|
-
- Check file paths and identifiers
|
@@ -1,35 +0,0 @@
|
|
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
|
-
```
|
nbsync-0.1.4/docs/index.md
DELETED
@@ -1,138 +0,0 @@
|
|
1
|
-
# nbsync
|
2
|
-
|
3
|
-
<div class="grid cards" markdown>
|
4
|
-
|
5
|
-
- :material-notebook-edit: **Notebooks from Markdown**
|
6
|
-
Extend standard markdown syntax to automatically generate notebooks from
|
7
|
-
documentation
|
8
|
-
[:octicons-arrow-right-24: Markdown Features](#notebooks-from-markdown)
|
9
|
-
|
10
|
-
- :material-language-python: **Python File Integration**
|
11
|
-
Directly reference external Python files and reuse functions or classes
|
12
|
-
[:octicons-arrow-right-24: Python Integration](#python-file-integration)
|
13
|
-
|
14
|
-
- :material-image-edit: **Code Execution in Images**
|
15
|
-
Execute code within image notation for dynamic visualizations
|
16
|
-
[:octicons-arrow-right-24: Dynamic Visualization](#code-execution-in-images)
|
17
|
-
|
18
|
-
- :material-refresh-auto: **Dynamic Updates**
|
19
|
-
Real-time synchronization between notebooks and documentation
|
20
|
-
[:octicons-arrow-right-24: Dynamic Updates](#dynamic-updates-and-execution)
|
21
|
-
|
22
|
-
</div>
|
23
|
-
|
24
|
-
## What is nbsync?
|
25
|
-
|
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.
|
31
|
-
|
32
|
-
It solves common challenges faced by data scientists, researchers, and technical
|
33
|
-
writers:
|
34
|
-
|
35
|
-
- **Development happens in notebooks** - ideal for experimentation and visualization
|
36
|
-
- **Documentation lives in markdown** - perfect for narrative and explanation
|
37
|
-
- **Code resides in Python files** - organized and version-controlled
|
38
|
-
- **Traditional integration is challenging** - screenshots break, exports get outdated
|
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 (``) 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
|
-
|
63
|
-
## Key Features
|
64
|
-
|
65
|
-
### Notebooks from Markdown
|
66
|
-
|
67
|
-
Extend standard markdown syntax to define notebook cells within your
|
68
|
-
documentation. Present code and its output results concisely with tabbed
|
69
|
-
display.
|
70
|
-
|
71
|
-
````markdown source="tabbed-nbsync"
|
72
|
-
```python .md#plot
|
73
|
-
import matplotlib.pyplot as plt
|
74
|
-
|
75
|
-
fig, ax = plt.subplots(figsize=(2, 1))
|
76
|
-
ax.plot([1, 3, 3, 4])
|
77
|
-
```
|
78
|
-
|
79
|
-
![Plot result](){#plot source="above"}
|
80
|
-
````
|
81
|
-
|
82
|
-
### Python File Integration
|
83
|
-
|
84
|
-
Directly reference external Python files and reuse defined functions or
|
85
|
-
classes. Avoid code duplication and improve maintainability.
|
86
|
-
|
87
|
-
```python title="plot.py"
|
88
|
-
--8<-- "scripts/plot.py"
|
89
|
-
```
|
90
|
-
|
91
|
-
```markdown source="tabbed-nbsync"
|
92
|
-
{#sqrt source="on"}
|
93
|
-
```
|
94
|
-
|
95
|
-
### Code Execution in Images
|
96
|
-
|
97
|
-
Execute Python code directly within image notation and display the results.
|
98
|
-
This enables easy placement of dynamic visualizations in tables or complex
|
99
|
-
layouts.
|
100
|
-
|
101
|
-
```markdown source="tabbed-nbsync"
|
102
|
-
| Sine | Cosine |
|
103
|
-
| :-------------------: | :-------------------: |
|
104
|
-
| ![](){`plot(np.sin)`} | ![](){`plot(np.cos)`} |
|
105
|
-
```
|
106
|
-
|
107
|
-
### Code Execution with markdown-exec Style Syntax
|
108
|
-
|
109
|
-
nbsync supports the markdown-exec style code blocks with the `exec="1"`
|
110
|
-
attribute as a compatible approach to code execution. While this syntax
|
111
|
-
is familiar to markdown-exec users, nbsync executes it through the
|
112
|
-
Jupyter Notebook engine instead, providing the ability to render diverse
|
113
|
-
MIME content types (HTML, SVG, images, etc.) directly in your
|
114
|
-
documentation. This enables richer and more complex outputs than
|
115
|
-
traditional execution methods.
|
116
|
-
|
117
|
-
````markdown source="tabbed-nbsync"
|
118
|
-
```python exec="1" source="tabbed-left"
|
119
|
-
import numpy as np
|
120
|
-
from PIL import Image
|
121
|
-
x = np.random.randint(0, 255, (50, 200), dtype=np.uint8)
|
122
|
-
Image.fromarray(x)
|
123
|
-
```
|
124
|
-
````
|
125
|
-
|
126
|
-
### Dynamic Updates and Execution
|
127
|
-
|
128
|
-
Automatic synchronization between notebooks and documentation ensures code
|
129
|
-
changes are reflected in real-time. View changes instantly in MkDocs serve
|
130
|
-
mode.
|
131
|
-
|
132
|
-
## Getting Started
|
133
|
-
|
134
|
-
Follow these steps to get started with nbsync:
|
135
|
-
|
136
|
-
1. [Installation](getting-started/installation.md)
|
137
|
-
2. [Configuration](getting-started/configuration.md)
|
138
|
-
3. [First Steps](getting-started/first-steps.md)
|
@@ -1,69 +0,0 @@
|
|
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
|
-
}
|
nbsync-0.1.4/scripts/plot.py
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
import matplotlib.pyplot as plt
|
2
|
-
import numpy as np
|
3
|
-
|
4
|
-
|
5
|
-
def plot(func):
|
6
|
-
x = np.linspace(0, 360)
|
7
|
-
y = func(np.radians(x))
|
8
|
-
fig, ax = plt.subplots(figsize=(2, 1))
|
9
|
-
ax.plot(x, y)
|
10
|
-
ax.set_title(f"Plot {func.__name__}")
|
11
|
-
|
12
|
-
|
13
|
-
if __name__ == "__main__":
|
14
|
-
# %% #sqrt
|
15
|
-
plot(np.sqrt)
|
nbsync-0.1.4/scripts/plotting.py
DELETED
@@ -1,19 +0,0 @@
|
|
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})")
|
@@ -1,96 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from pathlib import Path
|
4
|
-
from typing import TYPE_CHECKING, ClassVar
|
5
|
-
|
6
|
-
from mkdocs.config import Config as BaseConfig
|
7
|
-
from mkdocs.config import config_options
|
8
|
-
from mkdocs.plugins import BasePlugin
|
9
|
-
from mkdocs.structure.files import File
|
10
|
-
from nbstore.store import Store
|
11
|
-
|
12
|
-
from .logger import logger
|
13
|
-
from .sync import Synchronizer
|
14
|
-
|
15
|
-
if TYPE_CHECKING:
|
16
|
-
from typing import Any
|
17
|
-
|
18
|
-
from mkdocs.config.defaults import MkDocsConfig
|
19
|
-
from mkdocs.structure.files import Files
|
20
|
-
from mkdocs.structure.pages import Page
|
21
|
-
|
22
|
-
from .cell import Cell
|
23
|
-
|
24
|
-
|
25
|
-
class Config(BaseConfig):
|
26
|
-
"""Configuration for Nbstore plugin."""
|
27
|
-
|
28
|
-
src_dir = config_options.Type((str, list), default=".")
|
29
|
-
|
30
|
-
|
31
|
-
class Plugin(BasePlugin[Config]):
|
32
|
-
store: ClassVar[Store | None] = None
|
33
|
-
syncs: ClassVar[dict[str, Synchronizer]] = {}
|
34
|
-
files: Files
|
35
|
-
|
36
|
-
def on_config(self, config: MkDocsConfig, **kwargs: Any) -> MkDocsConfig:
|
37
|
-
if isinstance(self.config.src_dir, str):
|
38
|
-
src_dirs = [self.config.src_dir]
|
39
|
-
else:
|
40
|
-
src_dirs = self.config.src_dir
|
41
|
-
|
42
|
-
src_dirs = [(Path(config.docs_dir) / s).resolve() for s in src_dirs]
|
43
|
-
|
44
|
-
store = self.__class__.store
|
45
|
-
|
46
|
-
if store is None or store.src_dirs != src_dirs:
|
47
|
-
self.__class__.store = Store(src_dirs)
|
48
|
-
config.watch.extend(x.as_posix() for x in src_dirs)
|
49
|
-
|
50
|
-
for name in ["attr_list", "md_in_html"]:
|
51
|
-
if name not in config.markdown_extensions:
|
52
|
-
config.markdown_extensions.append(name)
|
53
|
-
|
54
|
-
return config
|
55
|
-
|
56
|
-
def on_files(self, files: Files, config: MkDocsConfig, **kwargs: Any) -> Files:
|
57
|
-
self.files = files
|
58
|
-
return files
|
59
|
-
|
60
|
-
def on_page_markdown(
|
61
|
-
self,
|
62
|
-
markdown: str,
|
63
|
-
page: Page,
|
64
|
-
config: MkDocsConfig,
|
65
|
-
**kwargs: Any,
|
66
|
-
) -> str:
|
67
|
-
if self.__class__.store is None:
|
68
|
-
msg = "Store must be initialized before processing markdown"
|
69
|
-
logger.error(msg)
|
70
|
-
return markdown
|
71
|
-
|
72
|
-
src_uri = page.file.src_uri
|
73
|
-
syncs = self.__class__.syncs
|
74
|
-
|
75
|
-
if src_uri not in syncs:
|
76
|
-
syncs[src_uri] = Synchronizer(self.__class__.store)
|
77
|
-
|
78
|
-
markdowns = []
|
79
|
-
|
80
|
-
for elem in syncs[src_uri].convert(markdown):
|
81
|
-
if isinstance(elem, str):
|
82
|
-
markdowns.append(elem)
|
83
|
-
|
84
|
-
elif markdown := elem.convert():
|
85
|
-
markdowns.append(markdown)
|
86
|
-
|
87
|
-
if elem.image.url and elem.content:
|
88
|
-
file = generate_file(elem, src_uri, config)
|
89
|
-
self.files.append(file)
|
90
|
-
|
91
|
-
return "".join(markdowns)
|
92
|
-
|
93
|
-
|
94
|
-
def generate_file(cell: Cell, page_uri: str, config: MkDocsConfig) -> File:
|
95
|
-
src_uri = (Path(page_uri).parent / cell.image.url).as_posix()
|
96
|
-
return File.generated(config, src_uri, content=cell.content)
|
nbsync-0.1.4/tests/__init__.py
DELETED
File without changes
|
@@ -1,108 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import shutil
|
3
|
-
import sys
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
import pytest
|
7
|
-
from mkdocs.commands.build import build
|
8
|
-
from mkdocs.config import load_config
|
9
|
-
from mkdocs.config.defaults import MkDocsConfig
|
10
|
-
|
11
|
-
from nbsync.plugin import Config, Plugin
|
12
|
-
|
13
|
-
|
14
|
-
@pytest.fixture(scope="module")
|
15
|
-
def config_file():
|
16
|
-
return Path(__file__).parent.parent / "mkdocs.yaml"
|
17
|
-
|
18
|
-
|
19
|
-
def test_config_file_exists(config_file: Path):
|
20
|
-
assert config_file.exists()
|
21
|
-
|
22
|
-
|
23
|
-
@pytest.fixture(scope="module")
|
24
|
-
def mkdocs_config(config_file: Path):
|
25
|
-
return load_config(str(config_file))
|
26
|
-
|
27
|
-
|
28
|
-
@pytest.fixture(scope="module")
|
29
|
-
def nbstore_plugin(mkdocs_config: MkDocsConfig):
|
30
|
-
return mkdocs_config.plugins["nbsync"]
|
31
|
-
|
32
|
-
|
33
|
-
def test_nbstore_plugin(nbstore_plugin: Plugin):
|
34
|
-
assert isinstance(nbstore_plugin, Plugin)
|
35
|
-
assert isinstance(nbstore_plugin.config, Config)
|
36
|
-
|
37
|
-
|
38
|
-
@pytest.fixture(scope="module")
|
39
|
-
def nbstore_config(nbstore_plugin: Plugin):
|
40
|
-
return nbstore_plugin.config
|
41
|
-
|
42
|
-
|
43
|
-
def test_nbstore_config(nbstore_config: Config):
|
44
|
-
config = nbstore_config
|
45
|
-
assert config.src_dir == ["../notebooks", "../scripts"]
|
46
|
-
|
47
|
-
|
48
|
-
@pytest.fixture
|
49
|
-
def config_plugin(tmp_path):
|
50
|
-
dest = Path(tmp_path)
|
51
|
-
root = Path(__file__).parent.parent
|
52
|
-
config_file = root / "mkdocs.yaml"
|
53
|
-
shutil.copy(config_file, dest)
|
54
|
-
for src in ["docs", "notebooks", "scripts", "src", "tests"]:
|
55
|
-
src_dir = root / src
|
56
|
-
shutil.copytree(src_dir, dest / src)
|
57
|
-
curdir = Path(os.curdir).absolute()
|
58
|
-
os.chdir(dest)
|
59
|
-
sys.path.insert(0, ".")
|
60
|
-
config = load_config("mkdocs.yaml")
|
61
|
-
plugin = config.plugins["nbsync"]
|
62
|
-
assert isinstance(plugin, Plugin)
|
63
|
-
plugin.__init__()
|
64
|
-
|
65
|
-
yield config, plugin
|
66
|
-
|
67
|
-
config.plugins.on_shutdown()
|
68
|
-
sys.path.pop(0)
|
69
|
-
os.chdir(curdir)
|
70
|
-
|
71
|
-
|
72
|
-
def test_on_config(config_plugin: tuple[MkDocsConfig, Plugin]):
|
73
|
-
config, plugin = config_plugin
|
74
|
-
plugin.on_config(config)
|
75
|
-
assert plugin.store is not None
|
76
|
-
|
77
|
-
|
78
|
-
@pytest.fixture
|
79
|
-
def config(config_plugin):
|
80
|
-
return config_plugin[0]
|
81
|
-
|
82
|
-
|
83
|
-
def test_build(config: MkDocsConfig):
|
84
|
-
config.plugins.on_startup(command="build", dirty=False)
|
85
|
-
plugin = config.plugins["nbsync"]
|
86
|
-
assert isinstance(plugin, Plugin)
|
87
|
-
|
88
|
-
build(config, dirty=False)
|
89
|
-
|
90
|
-
|
91
|
-
def test_on_page_markdown_fallback():
|
92
|
-
class FakePlugin(Plugin):
|
93
|
-
pass
|
94
|
-
|
95
|
-
plugin = FakePlugin()
|
96
|
-
plugin.__class__.store = None
|
97
|
-
assert plugin.on_page_markdown("abc", None, None) == "abc" # type: ignore
|
98
|
-
|
99
|
-
|
100
|
-
def test_src_dir_list(config_plugin: tuple[MkDocsConfig, Plugin]):
|
101
|
-
config, plugin = config_plugin
|
102
|
-
src_dir = plugin.config.src_dir
|
103
|
-
plugin.config.src_dir = ["a", "b"]
|
104
|
-
plugin.on_config(config)
|
105
|
-
assert plugin.store
|
106
|
-
assert plugin.store.src_dirs[0].name == "a"
|
107
|
-
assert plugin.store.src_dirs[1].name == "b"
|
108
|
-
plugin.config.src_dir = src_dir
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|