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.
- accelforge/__init__.py +21 -0
- accelforge/_accelerated_imports.py +16 -0
- accelforge/_deprecate/_simanneal/evalmapping.py +271 -0
- accelforge/_deprecate/_simanneal/mapspaceglobals.py +298 -0
- accelforge/_deprecate/_simanneal/simanneal.py +666 -0
- accelforge/_deprecate/_simanneal/tracking.py +105 -0
- accelforge/_deprecate/_simanneal/wrappers.py +218 -0
- accelforge/_deprecate/_simanneal2/__init__.py +7 -0
- accelforge/_deprecate/_simanneal2/simanneal.py +493 -0
- accelforge/_deprecate/_simanneal2/tracking.py +116 -0
- accelforge/_deprecate/compatibility_util.py +181 -0
- accelforge/_deprecate/layerdeduplication/__init__.py +2 -0
- accelforge/_deprecate/layerdeduplication/group_similar_einsums.py +160 -0
- accelforge/_deprecate/layerdeduplication/grouped_einsums.py +84 -0
- accelforge/_deprecate/mapping_filter_tags/__init__.py +2 -0
- accelforge/_deprecate/mapping_filter_tags/ffmt.py +212 -0
- accelforge/_deprecate/mapping_filter_tags/onesplit.py +24 -0
- accelforge/_deprecate/mapping_filter_tags/util.py +24 -0
- accelforge/_deprecate/tags.py +69 -0
- accelforge/_deprecate/viz/__init__.py +0 -0
- accelforge/_deprecate/viz/interactive.py +159 -0
- accelforge/_deprecate/viz/reservationtree.py +307 -0
- accelforge/_deprecate/viz/ski_slope.py +88 -0
- accelforge/_version.py +15 -0
- accelforge/examples.py +39 -0
- accelforge/frontend/__init__.py +10 -0
- accelforge/frontend/_binding.py +129 -0
- accelforge/frontend/_workload_isl/__init__.py +2 -0
- accelforge/frontend/_workload_isl/_isl.py +149 -0
- accelforge/frontend/_workload_isl/_symbolic.py +141 -0
- accelforge/frontend/arch copy.py +1544 -0
- accelforge/frontend/arch.py +1642 -0
- accelforge/frontend/config.py +63 -0
- accelforge/frontend/mapper/__init__.py +5 -0
- accelforge/frontend/mapper/ffm.py +126 -0
- accelforge/frontend/mapper/mapper.py +7 -0
- accelforge/frontend/mapper/metrics.py +30 -0
- accelforge/frontend/mapping/__init__.py +1 -0
- accelforge/frontend/mapping/mapping.py +1736 -0
- accelforge/frontend/model.py +14 -0
- accelforge/frontend/renames.py +150 -0
- accelforge/frontend/spec copy.py +230 -0
- accelforge/frontend/spec.py +301 -0
- accelforge/frontend/variables.py +12 -0
- accelforge/frontend/workload.py +952 -0
- accelforge/mapper/FFM/__init__.py +9 -0
- accelforge/mapper/FFM/_join_pmappings/__init__.py +0 -0
- accelforge/mapper/FFM/_join_pmappings/compatibility.py +653 -0
- accelforge/mapper/FFM/_join_pmappings/compress_pmappings.py +140 -0
- accelforge/mapper/FFM/_join_pmappings/join_pmappings.py +703 -0
- accelforge/mapper/FFM/_join_pmappings/pmapping_dataframe.py +901 -0
- accelforge/mapper/FFM/_join_pmappings/pmapping_group.py +337 -0
- accelforge/mapper/FFM/_make_pmappings/contraints/__init__.py +0 -0
- accelforge/mapper/FFM/_make_pmappings/contraints/constraints.py +360 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/__init__.py +1 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_loops.py +373 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_pmapping_templates.py +463 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_reservations.py +95 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_storage_order.py +382 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmapping_templates/make_storages.py +155 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmappings.py +411 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/__init__.py +1 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/make_pmappings_from_templates.py +407 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/make_tile_shapes.py +1681 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/run_model.py +170 -0
- accelforge/mapper/FFM/_make_pmappings/make_pmappings_from_templates/symbol_relations.py +174 -0
- accelforge/mapper/FFM/_make_pmappings/pmapper_job.py +282 -0
- accelforge/mapper/FFM/_pareto_df/df_convention.py +273 -0
- accelforge/mapper/FFM/_pareto_df/pareto copy.py +836 -0
- accelforge/mapper/FFM/_pareto_df/pareto.py +508 -0
- accelforge/mapper/FFM/data.py +61 -0
- accelforge/mapper/FFM/main copy.py +236 -0
- accelforge/mapper/FFM/main.py +208 -0
- accelforge/mapper/FFM/mappings.py +510 -0
- accelforge/mapper/FFM/pmappings.py +310 -0
- accelforge/mapper/__init__.py +4 -0
- accelforge/mapper.py +0 -0
- accelforge/model/__init__.py +1 -0
- accelforge/model/_looptree/__init__.py +0 -0
- accelforge/model/_looptree/accesses.py +335 -0
- accelforge/model/_looptree/capacity/__init__.py +1 -0
- accelforge/model/_looptree/capacity/aggregators.py +36 -0
- accelforge/model/_looptree/capacity/capacity.py +47 -0
- accelforge/model/_looptree/energy.py +150 -0
- accelforge/model/_looptree/equivalent_ranks.py +29 -0
- accelforge/model/_looptree/latency/__init__.py +1 -0
- accelforge/model/_looptree/latency/latency.py +98 -0
- accelforge/model/_looptree/latency/memory.py +120 -0
- accelforge/model/_looptree/latency/processors.py +92 -0
- accelforge/model/_looptree/mapping_utilities.py +71 -0
- accelforge/model/_looptree/reuse/__init__.py +4 -0
- accelforge/model/_looptree/reuse/isl/__init__.py +1 -0
- accelforge/model/_looptree/reuse/isl/des.py +59 -0
- accelforge/model/_looptree/reuse/isl/isl_functions.py +374 -0
- accelforge/model/_looptree/reuse/isl/mapping_to_isl/__init__.py +4 -0
- accelforge/model/_looptree/reuse/isl/mapping_to_isl/analyze_mapping.py +297 -0
- accelforge/model/_looptree/reuse/isl/mapping_to_isl/skews_from_mapping.py +236 -0
- accelforge/model/_looptree/reuse/isl/mapping_to_isl/tiling.py +685 -0
- accelforge/model/_looptree/reuse/isl/mapping_to_isl/types.py +188 -0
- accelforge/model/_looptree/reuse/isl/spatial.py +260 -0
- accelforge/model/_looptree/reuse/isl/temporal.py +182 -0
- accelforge/model/_looptree/reuse/symbolic/__init__.py +1 -0
- accelforge/model/_looptree/reuse/symbolic/symbolic copy 2.py +1346 -0
- accelforge/model/_looptree/reuse/symbolic/symbolic copy.py +1408 -0
- accelforge/model/_looptree/reuse/symbolic/symbolic.py +1396 -0
- accelforge/model/_looptree/run.py +122 -0
- accelforge/model/_looptree/types.py +26 -0
- accelforge/model/_looptree/visualization/__init__.py +0 -0
- accelforge/model/_looptree/visualization/occupancy.py +11 -0
- accelforge/model/main.py +222 -0
- accelforge/plotting/__init__.py +2 -0
- accelforge/plotting/mappings.py +219 -0
- accelforge/plotting/specs.py +57 -0
- accelforge/util/__init__.py +4 -0
- accelforge/util/_base_analysis_types.py +24 -0
- accelforge/util/_basetypes.py +1089 -0
- accelforge/util/_frozenset.py +36 -0
- accelforge/util/_isl.py +29 -0
- accelforge/util/_itertools.py +14 -0
- accelforge/util/_mathfuncs.py +57 -0
- accelforge/util/_parse_expressions.py +339 -0
- accelforge/util/_picklecache.py +32 -0
- accelforge/util/_setexpressions.py +268 -0
- accelforge/util/_sympy/__init__.py +0 -0
- accelforge/util/_sympy/broadcast_max.py +18 -0
- accelforge/util/_visualization.py +112 -0
- accelforge/util/_yaml.py +579 -0
- accelforge/util/parallel.py +193 -0
- accelforge-0.0.1.dist-info/METADATA +64 -0
- accelforge-0.0.1.dist-info/RECORD +258 -0
- accelforge-0.0.1.dist-info/WHEEL +5 -0
- accelforge-0.0.1.dist-info/licenses/LICENSE +19 -0
- accelforge-0.0.1.dist-info/top_level.txt +5 -0
- docs/_build/html/_sources/fastfusion.frontend.mapper.rst.txt +37 -0
- docs/_build/html/_sources/fastfusion.frontend.rst.txt +70 -0
- docs/_build/html/_sources/fastfusion.frontend.workload.rst.txt +21 -0
- docs/_build/html/_sources/fastfusion.mapper.FFM.rst.txt +37 -0
- docs/_build/html/_sources/fastfusion.mapper.rst.txt +18 -0
- docs/_build/html/_sources/fastfusion.rst.txt +20 -0
- docs/_build/html/_sources/fastfusion.util.rst.txt +21 -0
- docs/_build/html/_sources/index.rst.txt +87 -0
- docs/_build/html/_sources/modules.rst.txt +7 -0
- docs/_build/html/_sources/notes/citation.rst.txt +45 -0
- docs/_build/html/_sources/notes/definitions.rst.txt +43 -0
- docs/_build/html/_sources/notes/faqs.rst.txt +39 -0
- docs/_build/html/_sources/notes/modeling/accelerator_energy_latency.rst.txt +72 -0
- docs/_build/html/_sources/notes/modeling/component_energy_area.rst.txt +96 -0
- docs/_build/html/_sources/notes/modeling/mapping.rst.txt +100 -0
- docs/_build/html/_sources/notes/modeling.rst.txt +33 -0
- docs/_build/html/_sources/notes/parsing/arithmetic_parsing.rst.txt +136 -0
- docs/_build/html/_sources/notes/parsing/setexpressions.rst.txt +63 -0
- docs/_build/html/_sources/notes/parsing/yaml_parsing.rst.txt +176 -0
- docs/_build/html/_sources/notes/quickstart_and_installation.rst.txt +9 -0
- docs/_build/html/_sources/notes/spec/architecture.rst.txt +133 -0
- docs/_build/html/_sources/notes/spec/mapping.rst.txt +12 -0
- docs/_build/html/_sources/notes/spec/workload.rst.txt +83 -0
- docs/_build/html/_sources/notes/spec.rst.txt +36 -0
- docs/source/_ext/include_attrs.py +213 -0
- docs/source/_ext/include_docstring.py +364 -0
- docs/source/_ext/include_functions.py +154 -0
- docs/source/_ext/include_notebook.py +131 -0
- docs/source/_ext/include_yaml.py +119 -0
- docs/source/_ext/inherited_attributes.py +222 -0
- docs/source/_ext/paths.py +4 -0
- docs/source/conf.py +79 -0
- examples/arches/compute_in_memory/_include.yaml +74 -0
- examples/arches/compute_in_memory/_include_functions.py +229 -0
- examples/arches/compute_in_memory/_load_spec.py +57 -0
- examples/arches/compute_in_memory/components/c2c_multiplier.py +181 -0
- examples/arches/compute_in_memory/components/dac_c2c_r2r.py +605 -0
- examples/arches/compute_in_memory/components/misc.py +195 -0
- examples/arches/compute_in_memory/components/util/bit_functions.py +51 -0
- examples/arches/compute_in_memory/components/zero_comparator.py +92 -0
- examples/arches/compute_in_memory/isaac.yaml +233 -0
- examples/arches/compute_in_memory/memory_cells/ecram_demo.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/rram_example.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/rram_isaac_isca_2016.yaml +64 -0
- examples/arches/compute_in_memory/memory_cells/rram_neurosim_default.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/rram_raella_isca_2023.yaml +70 -0
- examples/arches/compute_in_memory/memory_cells/rram_wan_nature_2022.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/sram_colonnade_jssc_2021.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/sram_example.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/sram_jia_jssc_2020.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/sram_sinangil_jssc_2021.yaml +63 -0
- examples/arches/compute_in_memory/memory_cells/sram_wang_vlsi_2022.yaml +63 -0
- examples/arches/compute_in_memory/wang_vlsi_2022.yaml +289 -0
- examples/arches/eyeriss.yaml +68 -0
- examples/arches/fanout_variations/at_glb.yaml +31 -0
- examples/arches/fanout_variations/at_glb_with_fanout_node.yaml +34 -0
- examples/arches/fanout_variations/at_mac.yaml +31 -0
- examples/arches/fanout_variations/at_mac_with_constraints.yaml +38 -0
- examples/arches/fanout_variations/at_mac_with_fanout_node.yaml +34 -0
- examples/arches/nvdla.yaml +47 -0
- examples/arches/simple.yaml +28 -0
- examples/arches/tpu_v4i.yaml +67 -0
- examples/mappings/unfused_matmuls_to_simple.yaml +33 -0
- examples/misc/component_annotated.yaml +33 -0
- examples/workloads/gpt3_6.7B.yaml +124 -0
- examples/workloads/matmuls.yaml +20 -0
- examples/workloads/mobilenet_28.yaml +81 -0
- examples/workloads/mobilenet_various_separate.yaml +106 -0
- examples/workloads/three_matmuls_annotated.yaml +59 -0
- notebooks/.ipynb_checkpoints/fastfusion_arch_study_michael-checkpoint.ipynb +359 -0
- notebooks/compute_in_memory/_scripts.py +339 -0
- notebooks/compute_in_memory/isaac.guide.ipynb +270 -0
- notebooks/compute_in_memory/wang_vlsi_2022.ipynb +602 -0
- notebooks/paths.py +4 -0
- notebooks/tutorials/.ipynb_checkpoints/1_FFM-checkpoint.ipynb +3110 -0
- notebooks/tutorials/FFM.ipynb +3498 -0
- notebooks/tutorials/_include.py +48 -0
- notebooks/tutorials/component_energy_area.ipynb +363 -0
- tests/Q_mapping.yaml +38 -0
- tests/__init__.py +0 -0
- tests/conv.mapping.yaml +27 -0
- tests/conv.workload.yaml +13 -0
- tests/conv_sym.mapping.yaml +43 -0
- tests/copy.mapping.yaml +35 -0
- tests/copy.workload.yaml +15 -0
- tests/distribuffers/__init__.py +0 -0
- tests/distribuffers/multicast/test_cases.yaml +482 -0
- tests/distribuffers/spec/binding/valid_bindings.yaml +97 -0
- tests/distribuffers/spec/distributed.yaml +100 -0
- tests/distribuffers/spec/logical_arch.yaml +32 -0
- tests/distribuffers/spec/physical_arch.yaml +69 -0
- tests/distribuffers/test_binding.py +48 -0
- tests/frontend/__init__.py +0 -0
- tests/frontend/test_mapping_viz.py +52 -0
- tests/mapper/__init__.py +0 -0
- tests/mapper/configs/conv1d/conv1d.mapping.yaml +31 -0
- tests/mapper/configs/conv1d/conv1d.workload.yaml +11 -0
- tests/mapper/configs/two_conv1d/two_conv1d.expected.yaml +38 -0
- tests/mapper/configs/two_conv1d/two_conv1d.mapping.yaml +54 -0
- tests/mapper/configs/two_conv1d/two_conv1d.workload.yaml +19 -0
- tests/mapper/test_mapping_to_isl.py +90 -0
- tests/mapper/test_spatial_reuse_analysis.py +67 -0
- tests/mapper/test_temporal_reuse_analysis.py +56 -0
- tests/mapper/util.py +58 -0
- tests/matmul.mapping.yaml +29 -0
- tests/matmul.workload.yaml +12 -0
- tests/matmul_spatial.mapping.yaml +44 -0
- tests/mha.renames.yaml +65 -0
- tests/mha.workload.yaml +67 -0
- tests/mha.yaml +59 -0
- tests/mha_full.workload.yaml +67 -0
- tests/mobilenet.workload.yaml +35 -0
- tests/mobilenet_long.workload.yaml +64 -0
- tests/pmappingcache.py +24 -0
- tests/processing_stage.arch.yaml +40 -0
- tests/snowcat.arch.yaml +36 -0
- tests/test_ffm_join_pmappings.py +106 -0
- tests/test_ffm_make_pmappings.py +82 -0
- tests/test_ffm_make_tile_shapes.py +49 -0
- tests/test_mapper.py +100 -0
- tests/test_model.py +37 -0
- tests/test_plotting.py +72 -0
- tests/test_processing_stage.py +46 -0
- tests/test_symbolic_model.py +248 -0
- 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)
|