|
139 | 139 |
|
140 | 140 |
|
141 | 141 | def autodoc_skip_member(app, what, name, obj, skip, options): |
142 | | - """Skip members (functions, classes, modules) without docstrings.""" |
| 142 | + """Skip members without docstrings or from undocumented modules.""" |
143 | 143 | if not getattr(obj, 'docstring', None): |
144 | 144 | return True |
145 | 145 | elif what in ('class', 'function', 'attribute'): |
146 | | - # Check if the module of the class has a docstring |
147 | 146 | module_name = '.'.join(name.split('.')[:-1]) |
148 | 147 | try: |
149 | | - module = importlib.import_module(module_name) |
150 | | - return not getattr(module, '__doc__', None) |
| 148 | + mod = importlib.import_module(module_name) |
| 149 | + if not getattr(mod, '__doc__', None): |
| 150 | + return True # Module has no docstring, skip its members |
151 | 151 | except ModuleNotFoundError: |
| 152 | + # Import failed (e.g. attribute path like Class.attr, or missing dep). |
| 153 | + # Defer to AutoAPI default. |
152 | 154 | return None |
| 155 | + # For private names, defer to AutoAPI default (which skips them). |
| 156 | + # For public names, force-include (overrides the imported-members default). |
| 157 | + short_name = name.split('.')[-1] |
| 158 | + if short_name.startswith('_') and not short_name.startswith('__'): |
| 159 | + return None |
| 160 | + return False |
153 | 161 | return skip |
154 | 162 |
|
155 | 163 |
|
156 | 164 | def linkcode_resolve(domain, info): |
157 | 165 | if domain != 'py': |
158 | 166 | return None |
159 | 167 |
|
| 168 | + fullname = info['fullname'] |
160 | 169 | try: |
161 | | - obj = eval(info['fullname'], module.__dict__) |
162 | | - file, start, end = get_line_numbers(obj) |
163 | | - relpath = os.path.relpath(file, os.path.dirname(module.__file__)) |
164 | | - return f'{repo_url}/blob/v{release}/src/{main_module_name}/{relpath}#L{start}-L{end}' |
165 | | - except Exception: |
| 170 | + file, start, end = get_line_numbers(eval(fullname)) |
| 171 | + except AttributeError: |
| 172 | + # Instance attribute (dataclass field or self.x = ... in __init__) |
| 173 | + parts = fullname.rsplit('.', 1) |
| 174 | + if len(parts) != 2: |
| 175 | + return None |
| 176 | + try: |
| 177 | + file, start, end = get_attr_line_numbers(eval(parts[0]), parts[1]) |
| 178 | + except Exception: |
| 179 | + return None |
| 180 | + except Exception as e: |
| 181 | + print(f'linkcode_resolve failed: {info} — {e}') |
166 | 182 | return None |
167 | 183 |
|
| 184 | + relpath = os.path.relpath(file, os.path.dirname(module.__file__)) |
| 185 | + return f'{repo_url}/blob/v{release}/src/{main_module_name}/{relpath}#L{start}-L{end}' |
| 186 | + |
168 | 187 |
|
169 | 188 | def get_line_numbers(obj): |
170 | 189 | if isinstance(obj, property): |
@@ -196,6 +215,22 @@ def get_enum_member_line_numbers(obj): |
196 | 215 | raise ValueError(f'Enum member {obj.name} not found in {class_}') |
197 | 216 |
|
198 | 217 |
|
| 218 | +def get_attr_line_numbers(class_, attr_name): |
| 219 | + with module_restored(class_): |
| 220 | + source_lines, start_line = inspect.getsourcelines(class_) |
| 221 | + for i, line in enumerate(source_lines): |
| 222 | + stripped = line.strip() |
| 223 | + if ( |
| 224 | + stripped.startswith(f'{attr_name}:') |
| 225 | + or stripped.startswith(f'{attr_name} :') |
| 226 | + or f'self.{attr_name} =' in stripped |
| 227 | + or f'self.{attr_name}=' in stripped |
| 228 | + ): |
| 229 | + return inspect.getsourcefile(class_), start_line + i, start_line + i |
| 230 | + else: |
| 231 | + raise ValueError(f'Attribute {attr_name} not found in {class_}') |
| 232 | + |
| 233 | + |
199 | 234 | def get_member_line_numbers(obj: types.MemberDescriptorType): |
200 | 235 | class_ = obj.__objclass__ |
201 | 236 | with module_restored(class_): |
|
0 commit comments