accelforge 0.0.1__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 (258) hide show
  1. accelforge/__init__.py +21 -0
  2. accelforge/_accelerated_imports.py +16 -0
  3. accelforge/_deprecate/_simanneal/evalmapping.py +271 -0
  4. accelforge/_deprecate/_simanneal/mapspaceglobals.py +298 -0
  5. accelforge/_deprecate/_simanneal/simanneal.py +666 -0
  6. accelforge/_deprecate/_simanneal/tracking.py +105 -0
  7. accelforge/_deprecate/_simanneal/wrappers.py +218 -0
  8. accelforge/_deprecate/_simanneal2/__init__.py +7 -0
  9. accelforge/_deprecate/_simanneal2/simanneal.py +493 -0
  10. accelforge/_deprecate/_simanneal2/tracking.py +116 -0
  11. accelforge/_deprecate/compatibility_util.py +181 -0
  12. accelforge/_deprecate/layerdeduplication/__init__.py +2 -0
  13. accelforge/_deprecate/layerdeduplication/group_similar_einsums.py +160 -0
  14. accelforge/_deprecate/layerdeduplication/grouped_einsums.py +84 -0
  15. accelforge/_deprecate/mapping_filter_tags/__init__.py +2 -0
  16. accelforge/_deprecate/mapping_filter_tags/ffmt.py +212 -0
  17. accelforge/_deprecate/mapping_filter_tags/onesplit.py +24 -0
  18. accelforge/_deprecate/mapping_filter_tags/util.py +24 -0
  19. accelforge/_deprecate/tags.py +69 -0
  20. accelforge/_deprecate/viz/__init__.py +0 -0
  21. accelforge/_deprecate/viz/interactive.py +159 -0
  22. accelforge/_deprecate/viz/reservationtree.py +307 -0
  23. accelforge/_deprecate/viz/ski_slope.py +88 -0
  24. accelforge/_version.py +15 -0
  25. accelforge/examples.py +39 -0
  26. accelforge/frontend/__init__.py +10 -0
  27. accelforge/frontend/_binding.py +129 -0
  28. accelforge/frontend/_workload_isl/__init__.py +2 -0
  29. accelforge/frontend/_workload_isl/_isl.py +149 -0
  30. accelforge/frontend/_workload_isl/_symbolic.py +141 -0
  31. accelforge/frontend/arch copy.py +1544 -0
  32. accelforge/frontend/arch.py +1642 -0
  33. accelforge/frontend/config.py +63 -0
  34. accelforge/frontend/mapper/__init__.py +5 -0
  35. accelforge/frontend/mapper/ffm.py +126 -0
  36. accelforge/frontend/mapper/mapper.py +7 -0
  37. accelforge/frontend/mapper/metrics.py +30 -0
  38. accelforge/frontend/mapping/__init__.py +1 -0
  39. accelforge/frontend/mapping/mapping.py +1736 -0
  40. accelforge/frontend/model.py +14 -0
  41. accelforge/frontend/renames.py +150 -0
  42. accelforge/frontend/spec copy.py +230 -0
  43. accelforge/frontend/spec.py +301 -0
  44. accelforge/frontend/variables.py +12 -0
  45. accelforge/frontend/workload.py +952 -0
  46. accelforge/mapper/FFM/__init__.py +9 -0
  47. accelforge/mapper/FFM/_join_pmappings/__init__.py +0 -0
  48. accelforge/mapper/FFM/_join_pmappings/compatibility.py +653 -0
  49. accelforge/mapper/FFM/_join_pmappings/compress_pmappings.py +140 -0
  50. accelforge/mapper/FFM/_join_pmappings/join_pmappings.py +703 -0
  51. accelforge/mapper/FFM/_join_pmappings/pmapping_dataframe.py +901 -0
  52. accelforge/mapper/FFM/_join_pmappings/pmapping_group.py +337 -0
  53. accelforge/mapper/FFM/_make_pmappings/contraints/__init__.py +0 -0
  54. accelforge/mapper/FFM/_make_pmappings/contraints/constraints.py +360 -0
  55. accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/__init__.py +1 -0
  56. accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_loops.py +373 -0
  57. accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_pmapping_templates.py +463 -0
  58. accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_reservations.py +95 -0
  59. accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_storage_order.py +382 -0
  60. accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_storages.py +155 -0
  61. accelforge/mapper/FFM/_make_pmappings/make_pmappings.py +411 -0
  62. accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/__init__.py +1 -0
  63. accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/make_pmappings_from_templates.py +407 -0
  64. accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/make_tile_shapes.py +1681 -0
  65. accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/run_model.py +170 -0
  66. accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/symbol_relations.py +174 -0
  67. accelforge/mapper/FFM/_make_pmappings/pmapper_job.py +282 -0
  68. accelforge/mapper/FFM/_pareto_df/df_convention.py +273 -0
  69. accelforge/mapper/FFM/_pareto_df/pareto copy.py +836 -0
  70. accelforge/mapper/FFM/_pareto_df/pareto.py +508 -0
  71. accelforge/mapper/FFM/data.py +61 -0
  72. accelforge/mapper/FFM/main copy.py +236 -0
  73. accelforge/mapper/FFM/main.py +208 -0
  74. accelforge/mapper/FFM/mappings.py +510 -0
  75. accelforge/mapper/FFM/pmappings.py +310 -0
  76. accelforge/mapper/__init__.py +4 -0
  77. accelforge/mapper.py +0 -0
  78. accelforge/model/__init__.py +1 -0
  79. accelforge/model/_looptree/__init__.py +0 -0
  80. accelforge/model/_looptree/accesses.py +335 -0
  81. accelforge/model/_looptree/capacity/__init__.py +1 -0
  82. accelforge/model/_looptree/capacity/aggregators.py +36 -0
  83. accelforge/model/_looptree/capacity/capacity.py +47 -0
  84. accelforge/model/_looptree/energy.py +150 -0
  85. accelforge/model/_looptree/equivalent_ranks.py +29 -0
  86. accelforge/model/_looptree/latency/__init__.py +1 -0
  87. accelforge/model/_looptree/latency/latency.py +98 -0
  88. accelforge/model/_looptree/latency/memory.py +120 -0
  89. accelforge/model/_looptree/latency/processors.py +92 -0
  90. accelforge/model/_looptree/mapping_utilities.py +71 -0
  91. accelforge/model/_looptree/reuse/__init__.py +4 -0
  92. accelforge/model/_looptree/reuse/isl/__init__.py +1 -0
  93. accelforge/model/_looptree/reuse/isl/des.py +59 -0
  94. accelforge/model/_looptree/reuse/isl/isl_functions.py +374 -0
  95. accelforge/model/_looptree/reuse/isl/mapping_to_isl/__init__.py +4 -0
  96. accelforge/model/_looptree/reuse/isl/mapping_to_isl/analyze_mapping.py +297 -0
  97. accelforge/model/_looptree/reuse/isl/mapping_to_isl/skews_from_mapping.py +236 -0
  98. accelforge/model/_looptree/reuse/isl/mapping_to_isl/tiling.py +685 -0
  99. accelforge/model/_looptree/reuse/isl/mapping_to_isl/types.py +188 -0
  100. accelforge/model/_looptree/reuse/isl/spatial.py +260 -0
  101. accelforge/model/_looptree/reuse/isl/temporal.py +182 -0
  102. accelforge/model/_looptree/reuse/symbolic/__init__.py +1 -0
  103. accelforge/model/_looptree/reuse/symbolic/symbolic copy 2.py +1346 -0
  104. accelforge/model/_looptree/reuse/symbolic/symbolic copy.py +1408 -0
  105. accelforge/model/_looptree/reuse/symbolic/symbolic.py +1396 -0
  106. accelforge/model/_looptree/run.py +122 -0
  107. accelforge/model/_looptree/types.py +26 -0
  108. accelforge/model/_looptree/visualization/__init__.py +0 -0
  109. accelforge/model/_looptree/visualization/occupancy.py +11 -0
  110. accelforge/model/main.py +222 -0
  111. accelforge/plotting/__init__.py +2 -0
  112. accelforge/plotting/mappings.py +219 -0
  113. accelforge/plotting/specs.py +57 -0
  114. accelforge/util/__init__.py +4 -0
  115. accelforge/util/_base_analysis_types.py +24 -0
  116. accelforge/util/_basetypes.py +1089 -0
  117. accelforge/util/_frozenset.py +36 -0
  118. accelforge/util/_isl.py +29 -0
  119. accelforge/util/_itertools.py +14 -0
  120. accelforge/util/_mathfuncs.py +57 -0
  121. accelforge/util/_parse_expressions.py +339 -0
  122. accelforge/util/_picklecache.py +32 -0
  123. accelforge/util/_setexpressions.py +268 -0
  124. accelforge/util/_sympy/__init__.py +0 -0
  125. accelforge/util/_sympy/broadcast_max.py +18 -0
  126. accelforge/util/_visualization.py +112 -0
  127. accelforge/util/_yaml.py +579 -0
  128. accelforge/util/parallel.py +193 -0
  129. accelforge-0.0.1.dist-info/METADATA +64 -0
  130. accelforge-0.0.1.dist-info/RECORD +258 -0
  131. accelforge-0.0.1.dist-info/WHEEL +5 -0
  132. accelforge-0.0.1.dist-info/licenses/LICENSE +19 -0
  133. accelforge-0.0.1.dist-info/top_level.txt +5 -0
  134. docs/_build/html/_sources/fastfusion.frontend.mapper.rst.txt +37 -0
  135. docs/_build/html/_sources/fastfusion.frontend.rst.txt +70 -0
  136. docs/_build/html/_sources/fastfusion.frontend.workload.rst.txt +21 -0
  137. docs/_build/html/_sources/fastfusion.mapper.FFM.rst.txt +37 -0
  138. docs/_build/html/_sources/fastfusion.mapper.rst.txt +18 -0
  139. docs/_build/html/_sources/fastfusion.rst.txt +20 -0
  140. docs/_build/html/_sources/fastfusion.util.rst.txt +21 -0
  141. docs/_build/html/_sources/index.rst.txt +87 -0
  142. docs/_build/html/_sources/modules.rst.txt +7 -0
  143. docs/_build/html/_sources/notes/citation.rst.txt +45 -0
  144. docs/_build/html/_sources/notes/definitions.rst.txt +43 -0
  145. docs/_build/html/_sources/notes/faqs.rst.txt +39 -0
  146. docs/_build/html/_sources/notes/modeling/accelerator_energy_latency.rst.txt +72 -0
  147. docs/_build/html/_sources/notes/modeling/component_energy_area.rst.txt +96 -0
  148. docs/_build/html/_sources/notes/modeling/mapping.rst.txt +100 -0
  149. docs/_build/html/_sources/notes/modeling.rst.txt +33 -0
  150. docs/_build/html/_sources/notes/parsing/arithmetic_parsing.rst.txt +136 -0
  151. docs/_build/html/_sources/notes/parsing/setexpressions.rst.txt +63 -0
  152. docs/_build/html/_sources/notes/parsing/yaml_parsing.rst.txt +176 -0
  153. docs/_build/html/_sources/notes/quickstart_and_installation.rst.txt +9 -0
  154. docs/_build/html/_sources/notes/spec/architecture.rst.txt +133 -0
  155. docs/_build/html/_sources/notes/spec/mapping.rst.txt +12 -0
  156. docs/_build/html/_sources/notes/spec/workload.rst.txt +83 -0
  157. docs/_build/html/_sources/notes/spec.rst.txt +36 -0
  158. docs/source/_ext/include_attrs.py +213 -0
  159. docs/source/_ext/include_docstring.py +364 -0
  160. docs/source/_ext/include_functions.py +154 -0
  161. docs/source/_ext/include_notebook.py +131 -0
  162. docs/source/_ext/include_yaml.py +119 -0
  163. docs/source/_ext/inherited_attributes.py +222 -0
  164. docs/source/_ext/paths.py +4 -0
  165. docs/source/conf.py +79 -0
  166. examples/arches/compute_in_memory/_include.yaml +74 -0
  167. examples/arches/compute_in_memory/_include_functions.py +229 -0
  168. examples/arches/compute_in_memory/_load_spec.py +57 -0
  169. examples/arches/compute_in_memory/components/c2c_multiplier.py +181 -0
  170. examples/arches/compute_in_memory/components/dac_c2c_r2r.py +605 -0
  171. examples/arches/compute_in_memory/components/misc.py +195 -0
  172. examples/arches/compute_in_memory/components/util/bit_functions.py +51 -0
  173. examples/arches/compute_in_memory/components/zero_comparator.py +92 -0
  174. examples/arches/compute_in_memory/isaac.yaml +233 -0
  175. examples/arches/compute_in_memory/memory_cells/ecram_demo.yaml +63 -0
  176. examples/arches/compute_in_memory/memory_cells/rram_example.yaml +63 -0
  177. examples/arches/compute_in_memory/memory_cells/rram_isaac_isca_2016.yaml +64 -0
  178. examples/arches/compute_in_memory/memory_cells/rram_neurosim_default.yaml +63 -0
  179. examples/arches/compute_in_memory/memory_cells/rram_raella_isca_2023.yaml +70 -0
  180. examples/arches/compute_in_memory/memory_cells/rram_wan_nature_2022.yaml +63 -0
  181. examples/arches/compute_in_memory/memory_cells/sram_colonnade_jssc_2021.yaml +63 -0
  182. examples/arches/compute_in_memory/memory_cells/sram_example.yaml +63 -0
  183. examples/arches/compute_in_memory/memory_cells/sram_jia_jssc_2020.yaml +63 -0
  184. examples/arches/compute_in_memory/memory_cells/sram_sinangil_jssc_2021.yaml +63 -0
  185. examples/arches/compute_in_memory/memory_cells/sram_wang_vlsi_2022.yaml +63 -0
  186. examples/arches/compute_in_memory/wang_vlsi_2022.yaml +289 -0
  187. examples/arches/eyeriss.yaml +68 -0
  188. examples/arches/fanout_variations/at_glb.yaml +31 -0
  189. examples/arches/fanout_variations/at_glb_with_fanout_node.yaml +34 -0
  190. examples/arches/fanout_variations/at_mac.yaml +31 -0
  191. examples/arches/fanout_variations/at_mac_with_constraints.yaml +38 -0
  192. examples/arches/fanout_variations/at_mac_with_fanout_node.yaml +34 -0
  193. examples/arches/nvdla.yaml +47 -0
  194. examples/arches/simple.yaml +28 -0
  195. examples/arches/tpu_v4i.yaml +67 -0
  196. examples/mappings/unfused_matmuls_to_simple.yaml +33 -0
  197. examples/misc/component_annotated.yaml +33 -0
  198. examples/workloads/gpt3_6.7B.yaml +124 -0
  199. examples/workloads/matmuls.yaml +20 -0
  200. examples/workloads/mobilenet_28.yaml +81 -0
  201. examples/workloads/mobilenet_various_separate.yaml +106 -0
  202. examples/workloads/three_matmuls_annotated.yaml +59 -0
  203. notebooks/.ipynb_checkpoints/fastfusion_arch_study_michael-checkpoint.ipynb +359 -0
  204. notebooks/compute_in_memory/_scripts.py +339 -0
  205. notebooks/compute_in_memory/isaac.guide.ipynb +270 -0
  206. notebooks/compute_in_memory/wang_vlsi_2022.ipynb +602 -0
  207. notebooks/paths.py +4 -0
  208. notebooks/tutorials/.ipynb_checkpoints/1_FFM-checkpoint.ipynb +3110 -0
  209. notebooks/tutorials/FFM.ipynb +3498 -0
  210. notebooks/tutorials/_include.py +48 -0
  211. notebooks/tutorials/component_energy_area.ipynb +363 -0
  212. tests/Q_mapping.yaml +38 -0
  213. tests/__init__.py +0 -0
  214. tests/conv.mapping.yaml +27 -0
  215. tests/conv.workload.yaml +13 -0
  216. tests/conv_sym.mapping.yaml +43 -0
  217. tests/copy.mapping.yaml +35 -0
  218. tests/copy.workload.yaml +15 -0
  219. tests/distribuffers/__init__.py +0 -0
  220. tests/distribuffers/multicast/test_cases.yaml +482 -0
  221. tests/distribuffers/spec/binding/valid_bindings.yaml +97 -0
  222. tests/distribuffers/spec/distributed.yaml +100 -0
  223. tests/distribuffers/spec/logical_arch.yaml +32 -0
  224. tests/distribuffers/spec/physical_arch.yaml +69 -0
  225. tests/distribuffers/test_binding.py +48 -0
  226. tests/frontend/__init__.py +0 -0
  227. tests/frontend/test_mapping_viz.py +52 -0
  228. tests/mapper/__init__.py +0 -0
  229. tests/mapper/configs/conv1d/conv1d.mapping.yaml +31 -0
  230. tests/mapper/configs/conv1d/conv1d.workload.yaml +11 -0
  231. tests/mapper/configs/two_conv1d/two_conv1d.expected.yaml +38 -0
  232. tests/mapper/configs/two_conv1d/two_conv1d.mapping.yaml +54 -0
  233. tests/mapper/configs/two_conv1d/two_conv1d.workload.yaml +19 -0
  234. tests/mapper/test_mapping_to_isl.py +90 -0
  235. tests/mapper/test_spatial_reuse_analysis.py +67 -0
  236. tests/mapper/test_temporal_reuse_analysis.py +56 -0
  237. tests/mapper/util.py +58 -0
  238. tests/matmul.mapping.yaml +29 -0
  239. tests/matmul.workload.yaml +12 -0
  240. tests/matmul_spatial.mapping.yaml +44 -0
  241. tests/mha.renames.yaml +65 -0
  242. tests/mha.workload.yaml +67 -0
  243. tests/mha.yaml +59 -0
  244. tests/mha_full.workload.yaml +67 -0
  245. tests/mobilenet.workload.yaml +35 -0
  246. tests/mobilenet_long.workload.yaml +64 -0
  247. tests/pmappingcache.py +24 -0
  248. tests/processing_stage.arch.yaml +40 -0
  249. tests/snowcat.arch.yaml +36 -0
  250. tests/test_ffm_join_pmappings.py +106 -0
  251. tests/test_ffm_make_pmappings.py +82 -0
  252. tests/test_ffm_make_tile_shapes.py +49 -0
  253. tests/test_mapper.py +100 -0
  254. tests/test_model.py +37 -0
  255. tests/test_plotting.py +72 -0
  256. tests/test_processing_stage.py +46 -0
  257. tests/test_symbolic_model.py +248 -0
  258. tests/test_workload.py +141 -0
@@ -0,0 +1,213 @@
1
+ from docutils import nodes
2
+ from docutils.parsers.rst import Directive
3
+ import importlib
4
+ import inspect
5
+ import ast
6
+ import typing
7
+
8
+
9
+ class IncludeAttrs(Directive):
10
+ required_arguments = 1 # fully-qualified name
11
+
12
+ def run(self):
13
+ fqname = self.arguments[0]
14
+ parts = fqname.split(".")
15
+
16
+ # --- progressively import the longest valid module ---
17
+ module = None
18
+ for i in range(len(parts), 0, -1):
19
+ try:
20
+ module = importlib.import_module(".".join(parts[:i]))
21
+ rest = parts[i:]
22
+ break
23
+ except ImportError:
24
+ continue
25
+
26
+ if module is None:
27
+ return []
28
+
29
+ obj = module
30
+ for part in rest:
31
+ if hasattr(obj, part):
32
+ obj = getattr(obj, part)
33
+ else:
34
+ return []
35
+
36
+ # --- Collect all attributes with their metadata ---
37
+ attrs = {} # {attr_name: {'type': ..., 'default': ..., 'doc': ...}}
38
+
39
+ # Check if obj is a class
40
+ if not inspect.isclass(obj):
41
+ return []
42
+
43
+ # --- Get type annotations ---
44
+ annotations = getattr(obj, "__annotations__", {})
45
+
46
+ # --- Extract inline docstrings and defaults using AST ---
47
+ try:
48
+ source = inspect.getsource(obj)
49
+ tree = ast.parse(source)
50
+
51
+ for node in ast.walk(tree):
52
+ if isinstance(node, ast.ClassDef):
53
+ for i, item in enumerate(node.body):
54
+ # Look for annotated assignment (attribute with type hint)
55
+ if isinstance(item, ast.AnnAssign):
56
+ if isinstance(item.target, ast.Name):
57
+ attr_name = item.target.id
58
+
59
+ # Skip underscore-prefixed and excluded attributes
60
+ if self._should_skip_attr(attr_name):
61
+ continue
62
+
63
+ if attr_name not in attrs:
64
+ attrs[attr_name] = {'type': None, 'default': None, 'doc': None}
65
+
66
+ # Get type annotation
67
+ if attr_name in annotations:
68
+ attrs[attr_name]['type'] = annotations[attr_name]
69
+
70
+ # Get default value
71
+ if item.value is not None:
72
+ try:
73
+ attrs[attr_name]['default'] = ast.unparse(item.value)
74
+ except:
75
+ attrs[attr_name]['default'] = repr(item.value)
76
+
77
+ # Check if next item is a string (docstring)
78
+ if i + 1 < len(node.body):
79
+ next_item = node.body[i + 1]
80
+ if isinstance(next_item, ast.Expr) and isinstance(next_item.value, ast.Constant):
81
+ if isinstance(next_item.value.value, str):
82
+ attrs[attr_name]['doc'] = next_item.value.value.strip()
83
+ except (OSError, TypeError, SyntaxError):
84
+ pass
85
+
86
+ # --- Pydantic v2 fields ---
87
+ if hasattr(obj, "model_fields"):
88
+ for field_name, field in obj.model_fields.items():
89
+ # Skip underscore-prefixed and excluded attributes
90
+ if self._should_skip_attr(field_name):
91
+ continue
92
+
93
+ if field_name not in attrs:
94
+ attrs[field_name] = {'type': None, 'default': None, 'doc': None}
95
+
96
+ # Get type
97
+ if hasattr(field, "annotation"):
98
+ attrs[field_name]['type'] = field.annotation
99
+
100
+ # Get default
101
+ if hasattr(field, "default") and field.default is not None:
102
+ attrs[field_name]['default'] = repr(field.default)
103
+ elif hasattr(field, "default_factory") and field.default_factory is not None:
104
+ attrs[field_name]['default'] = f"{field.default_factory.__name__}()"
105
+
106
+ # Get docstring
107
+ doc = field.description or (field.json_schema_extra or {}).get("description")
108
+ if doc:
109
+ attrs[field_name]['doc'] = doc
110
+
111
+ # --- Pydantic v1 fields ---
112
+ if hasattr(obj, "__fields__"):
113
+ for field_name, field in obj.__fields__.items():
114
+ # Skip underscore-prefixed and excluded attributes
115
+ if self._should_skip_attr(field_name):
116
+ continue
117
+
118
+ if field_name not in attrs:
119
+ attrs[field_name] = {'type': None, 'default': None, 'doc': None}
120
+
121
+ # Get type
122
+ if hasattr(field, "outer_type_"):
123
+ attrs[field_name]['type'] = field.outer_type_
124
+
125
+ # Get default
126
+ if hasattr(field, "default") and field.default is not None:
127
+ attrs[field_name]['default'] = repr(field.default)
128
+ elif hasattr(field, "default_factory") and field.default_factory is not None:
129
+ attrs[field_name]['default'] = f"{field.default_factory.__name__}()"
130
+
131
+ # Get docstring - field is already a FieldInfo object
132
+ if hasattr(field, "description"):
133
+ doc = field.description
134
+ if doc:
135
+ attrs[field_name]['doc'] = doc
136
+
137
+ # --- Build bullet list ---
138
+ if not attrs:
139
+ return []
140
+
141
+ bullet_list = nodes.bullet_list()
142
+ for attr_name in sorted(attrs.keys()):
143
+ attr_info = attrs[attr_name]
144
+ list_item = nodes.list_item()
145
+ para = nodes.paragraph()
146
+
147
+ # Attribute name as :py:attr: role for clickable links
148
+ from sphinx.addnodes import pending_xref
149
+ refnode = pending_xref(
150
+ '',
151
+ refdomain='py',
152
+ reftype='attr',
153
+ reftarget=fqname + '.' + attr_name,
154
+ refwarn=True
155
+ )
156
+ refnode += nodes.literal('', attr_name, classes=['xref', 'py', 'py-attr'])
157
+ para += refnode
158
+
159
+ # # Type
160
+ # if attr_info['type'] is not None:
161
+ # type_str = self._format_type(attr_info['type'])
162
+ # para += nodes.Text(f" ({type_str})")
163
+
164
+ # # Default
165
+ # if attr_info['default'] is not None:
166
+ # para += nodes.Text(f", default: {attr_info['default']}")
167
+
168
+ # Docstring
169
+ if attr_info['doc']:
170
+ para += nodes.Text(f": {attr_info['doc']}")
171
+
172
+ list_item += para
173
+ bullet_list += list_item
174
+
175
+ return [bullet_list]
176
+
177
+ def _should_skip_attr(self, attr_name):
178
+ """Check if an attribute should be skipped."""
179
+ return (
180
+ attr_name.startswith('_') or
181
+ attr_name in ('type', 'version')
182
+ )
183
+
184
+ def _format_type(self, type_hint):
185
+ """Format a type hint into a readable string."""
186
+ if type_hint is None:
187
+ return "Any"
188
+
189
+ # Handle string annotations
190
+ if isinstance(type_hint, str):
191
+ return type_hint
192
+
193
+ # Get the type name
194
+ if hasattr(type_hint, "__name__"):
195
+ return type_hint.__name__
196
+
197
+ # Handle typing module types
198
+ if hasattr(type_hint, "__origin__"):
199
+ origin = type_hint.__origin__
200
+ args = getattr(type_hint, "__args__", ())
201
+
202
+ origin_name = getattr(origin, "__name__", str(origin))
203
+
204
+ if args:
205
+ args_str = ", ".join(self._format_type(arg) for arg in args)
206
+ return f"{origin_name}[{args_str}]"
207
+ return origin_name
208
+
209
+ return str(type_hint)
210
+
211
+
212
+ def setup(app):
213
+ app.add_directive("include-attrs", IncludeAttrs)
@@ -0,0 +1,364 @@
1
+ from docutils import nodes
2
+ from docutils.parsers.rst import Directive, roles
3
+ from docutils.statemachine import ViewList
4
+ from sphinx.util.nodes import nested_parse_with_titles
5
+ import importlib
6
+ import inspect
7
+ import ast
8
+ import re
9
+
10
+
11
+ class IncludeDocstring(Directive):
12
+ required_arguments = 1 # fully-qualified name
13
+ option_spec = {
14
+ 'decapitalize': lambda x: True, # Flag option, presence means True
15
+ 'inline': lambda x: True # Flag option for inline rendering
16
+ }
17
+
18
+ def run(self):
19
+ fqname = self.arguments[0]
20
+ parts = fqname.split(".")
21
+
22
+ # --- progressively import the longest valid module ---
23
+ module = None
24
+ for i in range(len(parts), 0, -1):
25
+ try:
26
+ module = importlib.import_module(".".join(parts[:i]))
27
+ rest = parts[i:]
28
+ break
29
+ except ImportError:
30
+ continue
31
+
32
+ if module is None:
33
+ return []
34
+
35
+ obj = module
36
+ for idx, part in enumerate(rest):
37
+ # Check if we're at the last part and current obj might have the part as a field
38
+ is_last = (idx == len(rest) - 1)
39
+
40
+ # Try to get docstring from Pydantic/annotated field BEFORE moving to next attribute
41
+ docstring = self._try_get_field_docstring(obj, part)
42
+ if docstring:
43
+ if is_last:
44
+ return self._parse_docstring(docstring)
45
+ # If not last, we found the field but need to continue traversing
46
+ # Get the field's type and continue with that
47
+ field_type = self._get_field_type(obj, part)
48
+ if field_type:
49
+ obj = field_type
50
+ continue
51
+
52
+ # Normal attribute access - move to next level
53
+ if hasattr(obj, part):
54
+ next_obj = getattr(obj, part)
55
+ # If we got an instance, use its class for field lookups
56
+ if not inspect.isclass(next_obj) and not inspect.ismodule(next_obj):
57
+ obj = type(next_obj)
58
+ else:
59
+ obj = next_obj
60
+ continue
61
+
62
+ # If we couldn't find it as an attribute and already checked fields, give up
63
+ return []
64
+
65
+ # Fallback: normal __doc__ for the final object
66
+ doc = getattr(obj, "__doc__", None)
67
+ return self._parse_docstring(doc) if doc else []
68
+
69
+ def _try_get_field_docstring(self, obj, field_name):
70
+ """Try to extract docstring from a field in obj."""
71
+ # --- Check if obj is a class with annotations ---
72
+ if inspect.isclass(obj) and hasattr(obj, "__annotations__") and field_name in obj.__annotations__:
73
+ # Try to extract inline docstring using AST
74
+ try:
75
+ source = inspect.getsource(obj)
76
+ tree = ast.parse(source)
77
+
78
+ for node in ast.walk(tree):
79
+ if isinstance(node, ast.ClassDef):
80
+ for i, item in enumerate(node.body):
81
+ # Look for annotated assignment (attribute with type hint)
82
+ if isinstance(item, ast.AnnAssign):
83
+ if isinstance(item.target, ast.Name) and item.target.id == field_name:
84
+ # Check if next item is a string (docstring)
85
+ if i + 1 < len(node.body):
86
+ next_item = node.body[i + 1]
87
+ if isinstance(next_item, ast.Expr) and isinstance(next_item.value, ast.Constant):
88
+ if isinstance(next_item.value.value, str):
89
+ return next_item.value.value
90
+ except (OSError, TypeError, SyntaxError):
91
+ pass
92
+
93
+ # --- Pydantic v2 field ---
94
+ if hasattr(obj, "model_fields") and field_name in obj.model_fields:
95
+ field = obj.model_fields[field_name]
96
+ return (
97
+ field.description
98
+ or (field.json_schema_extra or {}).get("description")
99
+ )
100
+
101
+ # --- Pydantic v1 field ---
102
+ if hasattr(obj, "__fields__") and field_name in obj.__fields__:
103
+ field = obj.__fields__[field_name]
104
+ if hasattr(field, "description"):
105
+ return field.description
106
+
107
+ return None
108
+
109
+ def _get_field_type(self, obj, field_name):
110
+ """Get the type/class of a field so we can continue traversing."""
111
+ # Pydantic v2
112
+ if hasattr(obj, "model_fields") and field_name in obj.model_fields:
113
+ field = obj.model_fields[field_name]
114
+ if hasattr(field, "annotation"):
115
+ return field.annotation
116
+
117
+ # Pydantic v1
118
+ if hasattr(obj, "__fields__") and field_name in obj.__fields__:
119
+ field = obj.__fields__[field_name]
120
+ if hasattr(field, "outer_type_"):
121
+ return field.outer_type_
122
+ elif hasattr(field, "type_"):
123
+ return field.type_
124
+
125
+ # Regular annotations
126
+ if inspect.isclass(obj) and hasattr(obj, "__annotations__") and field_name in obj.__annotations__:
127
+ return obj.__annotations__[field_name]
128
+
129
+ return None
130
+
131
+ def _parse_docstring(self, docstring):
132
+ """Parse a docstring as reStructuredText."""
133
+ if not docstring:
134
+ return []
135
+
136
+ # Decapitalize first letter if option is set
137
+ if 'decapitalize' in self.options:
138
+ docstring = self._decapitalize_first_letter(docstring)
139
+
140
+ # If inline mode, return just the text without block parsing
141
+ if 'inline' in self.options:
142
+ # Strip whitespace and collapse to single line
143
+ text = ' '.join(docstring.split())
144
+ # Parse as inline RST to handle inline markup like ``code``
145
+ result = ViewList()
146
+ result.append(text, '<include-docstring>', 0)
147
+
148
+ # Use a paragraph node for inline parsing
149
+ para = nodes.paragraph()
150
+ para.document = self.state.document
151
+ self.state.nested_parse(result, 0, para)
152
+
153
+ # Return the inline contents of the paragraph
154
+ return para.children
155
+
156
+ # Dedent the docstring while preserving blank lines and relative indentation
157
+ lines = docstring.splitlines()
158
+
159
+ # Find the minimum indentation (excluding blank lines)
160
+ min_indent = float('inf')
161
+ for line in lines:
162
+ stripped = line.lstrip()
163
+ if stripped: # Only consider non-blank lines
164
+ indent = len(line) - len(stripped)
165
+ min_indent = min(min_indent, indent)
166
+
167
+ # Remove the common indentation
168
+ if min_indent < float('inf'):
169
+ dedented_lines = []
170
+ for line in lines:
171
+ if line.strip(): # Non-blank line
172
+ dedented_lines.append(line[min_indent:])
173
+ else: # Blank line
174
+ dedented_lines.append('')
175
+ else:
176
+ dedented_lines = lines
177
+
178
+ # Parse the docstring as reStructuredText
179
+ result = ViewList()
180
+
181
+ for i, line in enumerate(dedented_lines):
182
+ result.append(line, '<include-docstring>', i)
183
+
184
+ node = nodes.section()
185
+ node.document = self.state.document
186
+ nested_parse_with_titles(self.state, result, node)
187
+
188
+ return node.children
189
+
190
+ def _decapitalize_first_letter(self, text):
191
+ """Decapitalize the first letter of the text."""
192
+ if not text:
193
+ return text
194
+
195
+ # Find the first letter (skip whitespace)
196
+ for i, char in enumerate(text):
197
+ if char.isalpha():
198
+ return text[:i] + char.lower() + text[i+1:]
199
+
200
+ return text
201
+
202
+
203
+ def docstring_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
204
+ """
205
+ Inline role to include docstrings.
206
+ Usage: :docstring:`module.Class.attribute` or :docstring-lower:`module.Class.attribute`
207
+ """
208
+ fqname = text.strip()
209
+ decapitalize = name == 'docstring-lower'
210
+
211
+ # Get the docstring
212
+ docstring = _get_docstring(fqname)
213
+
214
+ if not docstring:
215
+ msg = inliner.reporter.warning(
216
+ f'Could not find docstring for {fqname}',
217
+ line=lineno)
218
+ prb = inliner.problematic(rawtext, rawtext, msg)
219
+ return [prb], [msg]
220
+
221
+ # Decapitalize if requested
222
+ if decapitalize:
223
+ docstring = _decapitalize_first_letter(docstring)
224
+
225
+ # Collapse to single line
226
+ processed_text = ' '.join(docstring.split())
227
+
228
+ # Parse the text as inline RST
229
+ # The inliner.parse() method handles inline markup
230
+ nodes_list, messages = inliner.parse(processed_text, lineno, inliner, inliner.parent)
231
+
232
+ return nodes_list, messages
233
+
234
+
235
+ def _get_docstring(fqname):
236
+ """Get docstring from a fully qualified name."""
237
+ parts = fqname.split(".")
238
+
239
+ # Import the module
240
+ module = None
241
+ for i in range(len(parts), 0, -1):
242
+ try:
243
+ module = importlib.import_module(".".join(parts[:i]))
244
+ rest = parts[i:]
245
+ break
246
+ except ImportError:
247
+ continue
248
+
249
+ if module is None:
250
+ return None
251
+
252
+ obj = module
253
+ for idx, part in enumerate(rest):
254
+ # Check if we're at the last part
255
+ is_last = (idx == len(rest) - 1)
256
+
257
+ # Try to get docstring from Pydantic/annotated field BEFORE moving to next attribute
258
+ docstring = _try_get_field_docstring(obj, part)
259
+ if docstring:
260
+ if is_last:
261
+ return docstring
262
+ # If not last, we found the field but need to continue traversing
263
+ # Don't return yet, but also don't try normal attribute access for this field
264
+ # Instead, get the field's type and continue with that
265
+ field_type = _get_field_type(obj, part)
266
+ if field_type:
267
+ obj = field_type
268
+ continue
269
+
270
+ # Normal attribute access - move to next level
271
+ if hasattr(obj, part):
272
+ next_obj = getattr(obj, part)
273
+ # If we got an instance, use its class for field lookups
274
+ if not inspect.isclass(next_obj) and not inspect.ismodule(next_obj):
275
+ obj = type(next_obj)
276
+ else:
277
+ obj = next_obj
278
+ continue
279
+
280
+ # If we couldn't find it as an attribute and already checked fields, give up
281
+ return None
282
+
283
+ # Fallback: normal __doc__ for the final object
284
+ return getattr(obj, "__doc__", None)
285
+
286
+
287
+ def _try_get_field_docstring(obj, field_name):
288
+ """Try to extract docstring from a field in obj."""
289
+ # --- Check if obj is a class with annotations ---
290
+ if inspect.isclass(obj) and hasattr(obj, "__annotations__") and field_name in obj.__annotations__:
291
+ # Try to extract inline docstring using AST
292
+ try:
293
+ source = inspect.getsource(obj)
294
+ tree = ast.parse(source)
295
+
296
+ for node in ast.walk(tree):
297
+ if isinstance(node, ast.ClassDef):
298
+ for i, item in enumerate(node.body):
299
+ if isinstance(item, ast.AnnAssign):
300
+ if isinstance(item.target, ast.Name) and item.target.id == field_name:
301
+ if i + 1 < len(node.body):
302
+ next_item = node.body[i + 1]
303
+ if isinstance(next_item, ast.Expr) and isinstance(next_item.value, ast.Constant):
304
+ if isinstance(next_item.value.value, str):
305
+ return next_item.value.value
306
+ except (OSError, TypeError, SyntaxError):
307
+ pass
308
+
309
+ # --- Pydantic v2 field ---
310
+ if hasattr(obj, "model_fields") and field_name in obj.model_fields:
311
+ field = obj.model_fields[field_name]
312
+ return (
313
+ field.description
314
+ or (field.json_schema_extra or {}).get("description")
315
+ )
316
+
317
+ # --- Pydantic v1 field ---
318
+ if hasattr(obj, "__fields__") and field_name in obj.__fields__:
319
+ field = obj.__fields__[field_name]
320
+ if hasattr(field, "description"):
321
+ return field.description
322
+
323
+ return None
324
+
325
+
326
+ def _get_field_type(obj, field_name):
327
+ """Get the type/class of a field so we can continue traversing."""
328
+ # Pydantic v2
329
+ if hasattr(obj, "model_fields") and field_name in obj.model_fields:
330
+ field = obj.model_fields[field_name]
331
+ if hasattr(field, "annotation"):
332
+ return field.annotation
333
+
334
+ # Pydantic v1
335
+ if hasattr(obj, "__fields__") and field_name in obj.__fields__:
336
+ field = obj.__fields__[field_name]
337
+ if hasattr(field, "outer_type_"):
338
+ return field.outer_type_
339
+ elif hasattr(field, "type_"):
340
+ return field.type_
341
+
342
+ # Regular annotations
343
+ if inspect.isclass(obj) and hasattr(obj, "__annotations__") and field_name in obj.__annotations__:
344
+ return obj.__annotations__[field_name]
345
+
346
+ return None
347
+
348
+
349
+ def _decapitalize_first_letter(text):
350
+ """Decapitalize the first letter of the text."""
351
+ if not text:
352
+ return text
353
+
354
+ for i, char in enumerate(text):
355
+ if char.isalpha():
356
+ return text[:i] + char.lower() + text[i+1:]
357
+
358
+ return text
359
+
360
+
361
+ def setup(app):
362
+ app.add_directive("include-docstring", IncludeDocstring)
363
+ app.add_role("docstring", docstring_role)
364
+ app.add_role("docstring-lower", docstring_role)