sphinx-mkdocs-migrate 0.0.1.dev0__py3-none-any.whl

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 (39) hide show
  1. sphinx_mkdocs_migrate/__init__.py +8 -0
  2. sphinx_mkdocs_migrate/analyzer/__init__.py +17 -0
  3. sphinx_mkdocs_migrate/analyzer/ci.py +223 -0
  4. sphinx_mkdocs_migrate/analyzer/dependencies.py +134 -0
  5. sphinx_mkdocs_migrate/analyzer/markdown.py +148 -0
  6. sphinx_mkdocs_migrate/analyzer/mkdocs.py +263 -0
  7. sphinx_mkdocs_migrate/analyzer/models.py +348 -0
  8. sphinx_mkdocs_migrate/analyzer/navigation.py +108 -0
  9. sphinx_mkdocs_migrate/analyzer/project.py +511 -0
  10. sphinx_mkdocs_migrate/cli.py +507 -0
  11. sphinx_mkdocs_migrate/parsing/doc_ir.py +533 -0
  12. sphinx_mkdocs_migrate/parsing/flow_extractor.py +457 -0
  13. sphinx_mkdocs_migrate/parsing/html_flow_parser.py +349 -0
  14. sphinx_mkdocs_migrate/parsing/markdown.py +22 -0
  15. sphinx_mkdocs_migrate/parsing/markdown_ir.py +49 -0
  16. sphinx_mkdocs_migrate/parsing/markdown_it_adapter.py +496 -0
  17. sphinx_mkdocs_migrate/parsing/requirements.py +155 -0
  18. sphinx_mkdocs_migrate/planner/accountability.py +111 -0
  19. sphinx_mkdocs_migrate/planner/ci.py +142 -0
  20. sphinx_mkdocs_migrate/planner/conf_builder.py +183 -0
  21. sphinx_mkdocs_migrate/planner/models.py +379 -0
  22. sphinx_mkdocs_migrate/planner/planner.py +1867 -0
  23. sphinx_mkdocs_migrate/planner/policy.py +474 -0
  24. sphinx_mkdocs_migrate/planner/theme_constants.py +70 -0
  25. sphinx_mkdocs_migrate/planner/toctree.py +158 -0
  26. sphinx_mkdocs_migrate/py.typed +1 -0
  27. sphinx_mkdocs_migrate/rules/catalog.py +154 -0
  28. sphinx_mkdocs_migrate/rules/engine.py +94 -0
  29. sphinx_mkdocs_migrate/rules/models.py +176 -0
  30. sphinx_mkdocs_migrate/transformer/engine.py +897 -0
  31. sphinx_mkdocs_migrate/transformer/models.py +59 -0
  32. sphinx_mkdocs_migrate/transformer/myst_transformer.py +393 -0
  33. sphinx_mkdocs_migrate/validator/models.py +40 -0
  34. sphinx_mkdocs_migrate/validator/verifier.py +377 -0
  35. sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/METADATA +199 -0
  36. sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/RECORD +39 -0
  37. sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/WHEEL +4 -0
  38. sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  39. sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,111 @@
1
+ """Data models and inventory structures for Construct Accountability (Milestone 3.5 / 0.2.0).
2
+
3
+ Orthogonal Dimensions:
4
+ 1. Source Occurrence: kind, file, span (start_line, end_line), occurrence text
5
+ 2. Disposition: PRESERVE | TRANSFORM | MANUAL | UNSUPPORTED
6
+ 3. Target Strategy: strategy name, emitted target directive/representation, target reference
7
+ 4. Verification State: UNVERIFIED | ACCOUNTED | VERIFIED
8
+ 5. Manual Action: required (bool), instruction, rationale
9
+ """
10
+
11
+ from enum import Enum
12
+ from typing import List, Dict, Optional, Tuple
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class ConstructDisposition(str, Enum):
17
+ """What should happen to the source construct during migration."""
18
+
19
+ PRESERVE = "PRESERVE" # Preserve source representation/content through migration
20
+ TRANSFORM = "TRANSFORM" # Automatically transform to target Sphinx/MyST construct
21
+ MANUAL = "MANUAL" # Requires human decision or manual migration action
22
+ UNSUPPORTED = "UNSUPPORTED" # Known limitation; not automatically supported
23
+
24
+
25
+ class ConstructVerification(str, Enum):
26
+ """Has the construct outcome been verified?"""
27
+
28
+ UNVERIFIED = "UNVERIFIED" # Discovered in source but not yet accounted for in plan
29
+ ACCOUNTED = "ACCOUNTED" # Cataloged with explicit disposition and strategy in plan
30
+ VERIFIED = "VERIFIED" # Proven valid via post-migration parse, build, or human confirmation
31
+
32
+
33
+ class SourceOccurrence(BaseModel):
34
+ """Exact source provenance and location of an observed construct."""
35
+
36
+ kind: (
37
+ str # e.g., "admonition", "code_fence", "tab_set", "heading", "link", "toctree"
38
+ )
39
+ file: str # Relative file path (e.g. "docs/index.md", "mkdocs.yml")
40
+ span: Tuple[int, int] # (start_line, end_line)
41
+ occurrence: Optional[str] = None # Representative snippet or identifier
42
+
43
+
44
+ class TargetStrategy(BaseModel):
45
+ """Planned target representation and emission strategy in Sphinx/MyST."""
46
+
47
+ strategy: str # e.g., "myst_admonition", "direct_fence", "sphinx_design_tab", "root_toctree"
48
+ emitted: Optional[str] = None # Exact generated directive / snippet
49
+ target_reference: Optional[str] = (
50
+ None # Directive name, anchor ID, or conf variable
51
+ )
52
+
53
+
54
+ class ManualActionSpec(BaseModel):
55
+ """Specification of human action requirements."""
56
+
57
+ required: bool = False
58
+ instruction: Optional[str] = None
59
+ rationale: Optional[str] = None
60
+
61
+
62
+ class ConstructRecord(BaseModel):
63
+ """An individual accountable construct tracked across the full migration lifecycle."""
64
+
65
+ record_id: str
66
+ source: SourceOccurrence
67
+ disposition: ConstructDisposition
68
+ target: TargetStrategy
69
+ verification: ConstructVerification = ConstructVerification.ACCOUNTED
70
+ manual_action: ManualActionSpec = Field(default_factory=ManualActionSpec)
71
+
72
+
73
+ class AccountabilityInventory(BaseModel):
74
+ """Repository-wide construct accountability matrix."""
75
+
76
+ records: List[ConstructRecord] = Field(default_factory=list)
77
+
78
+ @property
79
+ def total_count(self) -> int:
80
+ return len(self.records)
81
+
82
+ @property
83
+ def accounted_count(self) -> int:
84
+ return sum(
85
+ 1
86
+ for r in self.records
87
+ if r.verification
88
+ in (ConstructVerification.ACCOUNTED, ConstructVerification.VERIFIED)
89
+ )
90
+
91
+ @property
92
+ def verified_count(self) -> int:
93
+ return sum(
94
+ 1 for r in self.records if r.verification == ConstructVerification.VERIFIED
95
+ )
96
+
97
+ @property
98
+ def manual_count(self) -> int:
99
+ return sum(1 for r in self.records if r.manual_action.required)
100
+
101
+ def by_disposition(self) -> Dict[str, int]:
102
+ counts: Dict[str, int] = {}
103
+ for r in self.records:
104
+ counts[r.disposition.value] = counts.get(r.disposition.value, 0) + 1
105
+ return counts
106
+
107
+ def by_kind(self) -> Dict[str, int]:
108
+ counts: Dict[str, int] = {}
109
+ for r in self.records:
110
+ counts[r.source.kind] = counts.get(r.source.kind, 0) + 1
111
+ return counts
@@ -0,0 +1,142 @@
1
+ """CI/CD and test runner migration planner."""
2
+
3
+ from __future__ import annotations
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from pydantic import BaseModel
8
+ from ..analyzer.ci import (
9
+ DEFAULT_CHECKOUT_TAG,
10
+ DEFAULT_CHECKOUT_SHA,
11
+ DEFAULT_SETUP_UV_TAG,
12
+ DEFAULT_SETUP_UV_SHA,
13
+ )
14
+ from ..analyzer.models import CIAnalysis, DependencyAnalysis
15
+
16
+
17
+ class CIWorkflowPlan(BaseModel):
18
+ """Planned CI workflow updates for building Sphinx documentation."""
19
+
20
+ tox_docs_env: Optional[str] = None
21
+ github_docs_job: Optional[str] = None
22
+ checkout_pinned_ref: Optional[str] = None
23
+ setup_uv_pinned_ref: Optional[str] = None
24
+
25
+
26
+ def determine_tox_dependency_line(
27
+ dep_analysis: Optional[DependencyAnalysis],
28
+ project_root: Optional[Path] = None,
29
+ ) -> str:
30
+ """Determine the dependency configuration line for tox [testenv:docs]."""
31
+ if (
32
+ dep_analysis
33
+ and dep_analysis.source_group_type
34
+ and dep_analysis.source_group_name
35
+ ):
36
+ if dep_analysis.source_group_type == "dependency-groups":
37
+ return f"dependency_groups = {dep_analysis.source_group_name}"
38
+ elif dep_analysis.source_group_type == "optional-dependencies":
39
+ return f"extras = {dep_analysis.source_group_name}"
40
+ elif dep_analysis.source_group_type == "requirements":
41
+ return f"deps = -r {dep_analysis.source_group_name}"
42
+
43
+ if project_root is not None:
44
+ pyproject_path = project_root / "pyproject.toml"
45
+ if pyproject_path.exists():
46
+ try:
47
+ py_text = pyproject_path.read_text(encoding="utf-8")
48
+ if "[dependency-groups]" in py_text:
49
+ if re.search(r"^\s*docs\s*=", py_text, re.MULTILINE):
50
+ return "dependency_groups = docs"
51
+ elif re.search(r"^\s*documentation\s*=", py_text, re.MULTILINE):
52
+ return "dependency_groups = documentation"
53
+ elif re.search(r"^\s*dev\s*=", py_text, re.MULTILINE):
54
+ return "dependency_groups = dev"
55
+ elif "[project.optional-dependencies]" in py_text:
56
+ if re.search(r"^\s*docs\s*=", py_text, re.MULTILINE):
57
+ return "extras = docs"
58
+ elif re.search(r"^\s*doc\s*=", py_text, re.MULTILINE):
59
+ return "extras = doc"
60
+ elif re.search(r"^\s*dev\s*=", py_text, re.MULTILINE):
61
+ return "extras = dev"
62
+ except Exception:
63
+ pass
64
+ if (project_root / "docs" / "requirements.txt").exists():
65
+ return "deps = -r docs/requirements.txt"
66
+ elif (project_root / "requirements.txt").exists():
67
+ return "deps = -r requirements.txt"
68
+
69
+ return "dependency_groups = dev"
70
+
71
+
72
+ def build_tox_docs_env(
73
+ dep_config_line: str,
74
+ build_command: str = "sphinx-build -b html docs site/_build/html",
75
+ ) -> str:
76
+ """Build the [testenv:docs] section string for tox.ini."""
77
+ return f"""
78
+ [testenv:docs]
79
+ description = build documentation
80
+ {dep_config_line}
81
+ commands =
82
+ {build_command}
83
+ """
84
+
85
+
86
+ def build_github_docs_job(
87
+ checkout_ref: str,
88
+ uv_ref: str,
89
+ tox_env: str = "docs",
90
+ ) -> str:
91
+ """Build the GitHub Actions documentation job block."""
92
+ return f"""
93
+ docs:
94
+ name: "Python Docs"
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@{checkout_ref}
98
+
99
+ - uses: astral-sh/setup-uv@{uv_ref}
100
+
101
+ - name: Build docs with tox
102
+ run: uvx tox -e {tox_env}
103
+ """
104
+
105
+
106
+ def plan_ci_workflow(
107
+ ci_analysis: Optional[CIAnalysis],
108
+ dep_analysis: Optional[DependencyAnalysis],
109
+ project_root: Optional[Path] = None,
110
+ ) -> CIWorkflowPlan:
111
+ """Plan concrete CI changes based on observed repository evidence."""
112
+ dep_line = determine_tox_dependency_line(dep_analysis, project_root)
113
+ tox_env = build_tox_docs_env(dep_line)
114
+
115
+ default_chk = (
116
+ f"{DEFAULT_CHECKOUT_SHA} # {DEFAULT_CHECKOUT_TAG}"
117
+ if DEFAULT_CHECKOUT_SHA
118
+ else DEFAULT_CHECKOUT_TAG
119
+ )
120
+ default_uv = (
121
+ f"{DEFAULT_SETUP_UV_SHA} # {DEFAULT_SETUP_UV_TAG}"
122
+ if DEFAULT_SETUP_UV_SHA
123
+ else DEFAULT_SETUP_UV_TAG
124
+ )
125
+ ch_ref = (
126
+ ci_analysis.checkout_pinned_ref
127
+ if (ci_analysis and ci_analysis.checkout_pinned_ref)
128
+ else default_chk
129
+ )
130
+ uv_ref = (
131
+ ci_analysis.setup_uv_pinned_ref
132
+ if (ci_analysis and ci_analysis.setup_uv_pinned_ref)
133
+ else default_uv
134
+ )
135
+ gh_job = build_github_docs_job(checkout_ref=ch_ref, uv_ref=uv_ref)
136
+
137
+ return CIWorkflowPlan(
138
+ tox_docs_env=tox_env,
139
+ github_docs_job=gh_job,
140
+ checkout_pinned_ref=ch_ref,
141
+ setup_uv_pinned_ref=uv_ref,
142
+ )
@@ -0,0 +1,183 @@
1
+ """Sphinx conf.py configuration builder and code synthesizer."""
2
+
3
+ from __future__ import annotations
4
+ import pprint
5
+ from pathlib import Path
6
+ from typing import Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .models import MigrationPlan, ConfigMigrationProposal
10
+
11
+
12
+ def build_conf_py(
13
+ plan: Optional[MigrationPlan] = None,
14
+ cfg: Optional[ConfigMigrationProposal] = None,
15
+ project_root: Optional[Path] = None,
16
+ plan_hash: Optional[str] = None,
17
+ has_generated_docs: bool = False,
18
+ ) -> str:
19
+ """Generates a clean Sphinx conf.py based strictly on MigrationPlan requirements."""
20
+ if plan is not None:
21
+ cfg = cfg or plan.proposed_sphinx_config
22
+ project_root = project_root or Path(plan.project_root)
23
+ plan_hash = plan_hash or plan.canonical_hash()[:12]
24
+ has_generated_docs = bool(plan.generated_documents)
25
+
26
+ project_name = cfg.project_name if cfg else "Documentation"
27
+ theme = (
28
+ cfg.theme.target_theme
29
+ if cfg and cfg.theme and cfg.theme.target_theme
30
+ else "sphinx_rtd_theme"
31
+ )
32
+ extensions = list(cfg.extensions_to_add) if cfg else ["myst_parser"]
33
+ myst_exts = list(cfg.myst_enable_extensions) if cfg else ["colon_fence"]
34
+ custom_opts = cfg.custom_options if cfg else {}
35
+
36
+ copyright_val = custom_opts.get("copyright", "Documentation Authors")
37
+ author_val = custom_opts.get("author", "Documentation Authors")
38
+
39
+ hash_str = plan_hash if plan_hash else "synthetic"
40
+ lines = [
41
+ "# Configuration file for Sphinx documentation generator.",
42
+ f"# Generated automatically by sphinx-mkdocs-migrate from plan: {hash_str}",
43
+ "import os",
44
+ "import sys",
45
+ ]
46
+
47
+ has_autodoc = any("autodoc" in ext for ext in extensions) or has_generated_docs
48
+ if has_autodoc:
49
+ if project_root is not None:
50
+ src_candidate = project_root / "src"
51
+ if src_candidate.exists() and src_candidate.is_dir():
52
+ lines.append("sys.path.insert(0, os.path.abspath('../src'))")
53
+ else:
54
+ lines.append("sys.path.insert(0, os.path.abspath('../src'))")
55
+ lines.append("sys.path.insert(0, os.path.abspath('..'))")
56
+
57
+ lines.extend(
58
+ [
59
+ "",
60
+ f"project = {repr(project_name)}",
61
+ f"copyright = {repr(copyright_val)}",
62
+ f"author = {repr(author_val)}",
63
+ "",
64
+ "extensions = [",
65
+ ]
66
+ )
67
+ for ext in sorted(extensions):
68
+ lines.append(f" {repr(ext)},")
69
+ lines.append("]")
70
+ lines.extend(
71
+ [
72
+ "",
73
+ "source_suffix = {",
74
+ " '.md': 'markdown',",
75
+ "}",
76
+ "",
77
+ f"html_theme = {repr(theme)}",
78
+ ]
79
+ )
80
+
81
+ # Standard scalar and list Sphinx configuration keys
82
+ standard_settings = [
83
+ "html_logo",
84
+ "html_favicon",
85
+ "language",
86
+ "html_css_files",
87
+ "html_js_files",
88
+ "html_title",
89
+ "html_baseurl",
90
+ "version",
91
+ "release",
92
+ "autosummary_generate",
93
+ "add_module_names",
94
+ "autoclass_content",
95
+ "autodoc_mock_imports",
96
+ ]
97
+ for key in standard_settings:
98
+ if key in custom_opts and custom_opts[key] is not None:
99
+ lines.append(f"{key} = {repr(custom_opts[key])}")
100
+
101
+ if "html_theme_options" in custom_opts:
102
+ formatted_opts = pprint.pformat(custom_opts["html_theme_options"], indent=4)
103
+ lines.extend(
104
+ [
105
+ "",
106
+ f"html_theme_options = {formatted_opts}",
107
+ ]
108
+ )
109
+ elif theme == "sphinx_immaterial":
110
+ lines.extend(
111
+ [
112
+ "",
113
+ "html_theme_options = {",
114
+ " 'font': False,",
115
+ " 'globaltoc_collapse': False,",
116
+ "}",
117
+ ]
118
+ )
119
+
120
+ if "object_description_options" in custom_opts:
121
+ formatted_obj_opts = pprint.pformat(
122
+ custom_opts["object_description_options"], indent=4
123
+ )
124
+ lines.extend(
125
+ [
126
+ "",
127
+ f"object_description_options = {formatted_obj_opts}",
128
+ ]
129
+ )
130
+ elif theme == "sphinx_immaterial":
131
+ lines.extend(
132
+ [
133
+ "",
134
+ "object_description_options = [",
135
+ " ('py:.*', dict(include_fields_in_toc=False)),",
136
+ " ('py:parameter', dict(include_in_toc=False)),",
137
+ "]",
138
+ ]
139
+ )
140
+
141
+ lines.extend(
142
+ [
143
+ "",
144
+ "myst_enable_extensions = [",
145
+ ]
146
+ )
147
+ for m_ext in sorted(myst_exts):
148
+ lines.append(f" {repr(m_ext)},")
149
+ lines.extend(["]", "", "myst_heading_anchors = 3", ""])
150
+
151
+ if cfg and "sphinx.ext.autodoc" in cfg.extensions_to_add:
152
+ lines.extend(
153
+ [
154
+ "",
155
+ "import re",
156
+ "_re_md_link = re.compile(r'(?<![!])\\[(?P<text>[^\\]\\n]+?)\\]\\((?P<url>[^\\)\\s]+)\\)')",
157
+ "_re_cross_ref = re.compile(r'(?<![!])\\[(?P<text>[^\\]\\n]+?)\\]\\[(?P<target>[a-zA-Z_0-9\\.]+)\\]')",
158
+ "_re_empty_cross_ref = re.compile(r'(?<![!])\\[(?P<target>[a-zA-Z_0-9\\.]+)\\]\\[\\]')",
159
+ "_re_md_code = re.compile(r'(?<![:`])`(?P<code>[^`\\n]+?)`(?!_|\\`)')",
160
+ "",
161
+ "def process_docstrings(app, what, name, obj, options, lines):",
162
+ " if what == 'module' and getattr(options, 'members', None):",
163
+ " lines.clear()",
164
+ " return",
165
+ " for i in range(len(lines)):",
166
+ " if '[' in lines[i] and '][' in lines[i]:",
167
+ " lines[i] = _re_empty_cross_ref.sub(r':py:obj:`\\g<target>`', lines[i])",
168
+ " lines[i] = _re_cross_ref.sub(r':py:obj:`\\g<text> <\\g<target>>`', lines[i])",
169
+ " if '[' in lines[i] and '](' in lines[i]:",
170
+ " def repl(m):",
171
+ " clean_text = m.group('text').replace('`', '').strip()",
172
+ " return f'`{clean_text} <{m.group(\"url\")}>`_'",
173
+ " lines[i] = _re_md_link.sub(repl, lines[i])",
174
+ " if '`' in lines[i]:",
175
+ " lines[i] = _re_md_code.sub(r'``\\g<code>``', lines[i])",
176
+ "",
177
+ "def setup(app):",
178
+ " app.connect('autodoc-process-docstring', process_docstrings)",
179
+ "",
180
+ ]
181
+ )
182
+
183
+ return "\n".join(lines)