mkdocstrings-python-xref 1.16.1__tar.gz → 1.16.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,3 +37,7 @@ conda-meta-data.json
37
37
 
38
38
 
39
39
 
40
+
41
+ # pixi environments
42
+ .pixi
43
+ *.egg-info
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2022-2023 Analog Devices, Inc.
189
+ Copyright 2022-2025 Analog Devices, Inc.
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocstrings-python-xref
3
- Version: 1.16.1
3
+ Version: 1.16.3
4
4
  Summary: Enhanced mkdocstrings python handler
5
5
  Project-URL: Repository, https://github.com/analog-garage/mkdocstrings-python-xref
6
6
  Project-URL: Documentation, https://analog-garage.github.io/mkdocstrings-python-xref/
7
7
  Author-email: Christopher Barber <Christopher.Barber@analog.com>
8
8
  License-File: LICENSE.md
9
9
  Keywords: documentation-tool,mkdocstrings,mkdocstrings-handler,python
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
@@ -18,6 +18,20 @@ Classifier: Topic :: Software Development :: Documentation
18
18
  Requires-Python: >=3.9
19
19
  Requires-Dist: griffe>=1.0
20
20
  Requires-Dist: mkdocstrings-python<2.0,>=1.16.6
21
+ Provides-Extra: dev
22
+ Requires-Dist: beautifulsoup4>=4.12; extra == 'dev'
23
+ Requires-Dist: black>=23.12; extra == 'dev'
24
+ Requires-Dist: build>=1.0.0; extra == 'dev'
25
+ Requires-Dist: coverage>=7.4.0; extra == 'dev'
26
+ Requires-Dist: hatchling>=1.21; extra == 'dev'
27
+ Requires-Dist: linkchecker>=10.4; extra == 'dev'
28
+ Requires-Dist: mike>=1.1; extra == 'dev'
29
+ Requires-Dist: mkdocs-material>=9.5.4; extra == 'dev'
30
+ Requires-Dist: mkdocs<2.0,>=1.5.3; extra == 'dev'
31
+ Requires-Dist: mypy>=1.10; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.2; extra == 'dev'
34
+ Requires-Dist: ruff>=0.4.10; extra == 'dev'
21
35
  Description-Content-Type: text/markdown
22
36
 
23
37
  # mkdocstrings-python-xref
@@ -10,7 +10,7 @@ authors = [
10
10
  {name = "Christopher Barber", email="Christopher.Barber@analog.com" },
11
11
  ]
12
12
  classifiers = [
13
- "Development Status :: 3 - Alpha",
13
+ "Development Status :: 5 - Production/Stable",
14
14
  "Intended Audience :: Developers",
15
15
  "Topic :: Software Development :: Documentation",
16
16
  "Programming Language :: Python :: 3.9",
@@ -33,6 +33,51 @@ dependencies = [
33
33
  Repository = "https://github.com/analog-garage/mkdocstrings-python-xref"
34
34
  Documentation = "https://analog-garage.github.io/mkdocstrings-python-xref/"
35
35
 
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "build >=1.0.0", # python-build on conda
39
+ "hatchling >=1.21",
40
+ "coverage >=7.4.0",
41
+ "pytest >=8.2",
42
+ "pytest-cov >=5.0",
43
+ "mypy >=1.10",
44
+ "ruff >=0.4.10",
45
+ "beautifulsoup4 >=4.12",
46
+ "black >=23.12",
47
+ "mike >=1.1",
48
+ "mkdocs >=1.5.3,<2.0",
49
+ "mkdocs-material >=9.5.4",
50
+ "linkchecker >=10.4"
51
+ ]
52
+
53
+ [tool.pixi.workspace]
54
+ name = "mkxref-dev"
55
+ channels = ["conda-forge"]
56
+ platforms = ["osx-arm64", "linux-64", "win-64"]
57
+
58
+ [tool.pixi.dependencies]
59
+ # Use conda for these in pixi
60
+ mkdocstrings-python ="*"
61
+ griffe ="*"
62
+ hatchling = "*"
63
+ python-build = "*"
64
+ coverage ="*"
65
+ pytest ="*"
66
+ pytest-cov ="*"
67
+ mypy ="*"
68
+ ruff = "*"
69
+ black = "*"
70
+ mike = "*"
71
+ mkdocs = "*"
72
+ mkdocs-material = "*"
73
+ linkchecker = "*"
74
+
75
+ [tool.pixi.pypi-dependencies]
76
+ mkdocstrings-python-xref = { path = ".", editable = true }
77
+
78
+ [tool.pixi.environments]
79
+ default = {features = ["dev"]}
80
+
36
81
  [tool.hatch.version]
37
82
  path = "src/mkdocstrings_handlers/python_xref/VERSION"
38
83
  pattern = "\\s*(?P<version>[\\w.]*)"
@@ -174,3 +219,53 @@ disable = [
174
219
  "wrong-spelling-in-comment",
175
220
  "wrong-spelling-in-docstring",
176
221
  ]
222
+
223
+ [tool.pixi.tasks]
224
+ # linting tasks
225
+ mypy = "mypy"
226
+ ruff = "ruff check src/mkdocstrings_handlers tests"
227
+ lint = {depends-on = ["ruff", "mypy"]}
228
+
229
+ # testing tasks
230
+ pytest = "pytest -sv -ra tests"
231
+ test = {depends-on = ["pytest", "lint"]}
232
+ coverage = "pytest -ra --cov --cov-report=html --cov-report=term -- tests"
233
+ coverage-show = "python -m webbrowser file://$PIXI_PROJECT_ROOT/htmlcov/index.html"
234
+
235
+ # doc tasks
236
+ docs = {depends-on = ["doc"]}
237
+ show-doc = "mkdocs serve -f mkdocs.yml"
238
+ show-docs = {depends-on = ["show-doc"]}
239
+
240
+ # cleanup tasks
241
+ clean-build = "rm -rf build dist"
242
+ clean-coverage = "rm -rf .coverage .coverage.* htmlcov"
243
+ clean-docs = "rm -rf site"
244
+ clean-test = "rm -rf .pytest_cache .mypy_cache .ruff_cache"
245
+ clean = {depends-on = ["clean-build", "clean-coverage", "clean-test"]}
246
+
247
+ # build tasks
248
+ build = {depends-on = ["build-wheel", "build-sdist", "build-conda"]}
249
+
250
+ [tool.pixi.tasks.build-wheel]
251
+ env = {VERSION = "$(cat src/mkdocstrings_handlers/python_xref/VERSION)"}
252
+ cmd = "pip wheel . --no-deps --no-build-isolation -w dist"
253
+ inputs = ["pyproject.toml", "LICENSE.md", "src/**/*"]
254
+ outputs = ["dist/mkdocstrings_python_xref-$VERSION-py3-none-any.whl"]
255
+
256
+ [tool.pixi.tasks.build-sdist]
257
+ env = {VERSION = "$(cat src/mkdocstrings_handlers/python_xref/VERSION)"}
258
+ cmd = "python -m build --sdist --no-isolation --outdir dist"
259
+ inputs = ["pyproject.toml", "LICENSE.md", "src/**/*"]
260
+ outputs = ["dist/mkdocstrings_pixipython_xref-$VERSION.tar.gz"]
261
+
262
+ [tool.pixi.tasks.build-conda]
263
+ #env = {VERSION = "$(cat src/mkdocstrings_handlers/python_xref/VERSION)"}
264
+ cmd = "whl2conda convert dist/*.whl -w dist --overwrite"
265
+ depends-on = ["build-wheel"]
266
+ inputs = ["dist/mkdocstrings_python_xref-$VERSION-py3-none-any.whl"]
267
+
268
+ [tool.pixi.tasks.doc]
269
+ cmd = "mkdocs build -f mkdocs.yml"
270
+ inputs = ["docs/*.md", "docs/*.svg", "mkdocs.yml"]
271
+ outputs = ["site/*.html"]
@@ -15,8 +15,10 @@
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import ast
18
19
  import re
19
- from typing import Callable, List, Optional, cast
20
+ import sys
21
+ from typing import Any, Callable, List, Optional, cast
20
22
 
21
23
  from griffe import Docstring, Object
22
24
  from mkdocstrings import get_logger
@@ -303,14 +305,12 @@ class _RelativeCrossrefProcessor:
303
305
  # We include the file:// prefix because it helps IDEs such as PyCharm
304
306
  # recognize that this is a navigable location it can highlight.
305
307
  prefix = f"file://{parent.filepath}:"
306
- line = doc.lineno
307
- if line is not None: # pragma: no branch
308
- # Add line offset to match in docstring. This can still be
309
- # short if the doc string has leading newlines.
310
- line += doc.value.count("\n", 0, self._cur_offset)
308
+ line, col = doc_value_offset_to_location(doc, self._cur_offset)
309
+ if line >= 0:
311
310
  prefix += f"{line}:"
312
- # It would be nice to add the column as well, but we cannot determine
313
- # that without knowing how much the doc string was unindented.
311
+ if col >= 0:
312
+ prefix += f"{col}:"
313
+
314
314
  prefix += " \n"
315
315
 
316
316
  logger.warning(prefix + msg)
@@ -334,3 +334,68 @@ def substitute_relative_crossrefs(obj: Object, checkref: Optional[Callable[[str]
334
334
  for member in obj.members.values():
335
335
  if isinstance(member, Object): # pragma: no branch
336
336
  substitute_relative_crossrefs(member, checkref=checkref)
337
+
338
+ def doc_value_offset_to_location(doc: Docstring, offset: int) -> tuple[int,int]:
339
+ """
340
+ Converts offset into doc.value to line and column in source file.
341
+
342
+ Returns:
343
+ line and column or else (-1,-1) if it cannot be computed
344
+ """
345
+ linenum = -1
346
+ colnum = -2
347
+
348
+ if doc.lineno is not None:
349
+ linenum = doc.lineno # start of the docstring source
350
+ # line offset with respect to start of cleaned up docstring
351
+ lineoffset = clean_lineoffset = doc.value.count("\n", 0, offset)
352
+
353
+ # look at original doc source, if available
354
+ try:
355
+ source = doc.source
356
+ # compute docstring without cleaning up spaces and indentation
357
+ rawvalue = str(safe_eval(source))
358
+
359
+ # adjust line offset by number of lines removed from front of docstring
360
+ lineoffset += leading_space(rawvalue).count("\n")
361
+
362
+ if lineoffset == 0 and (m := re.match(r"(\s*['\"]{1,3}\s*)\S", source)):
363
+ # is on the same line as opening quote
364
+ colnum = offset + len(m.group(1))
365
+ else:
366
+ # indentation of first non-empty line in raw and cleaned up strings
367
+ raw_line = rawvalue.splitlines()[lineoffset]
368
+ clean_line = doc.value.splitlines()[clean_lineoffset]
369
+ raw_indent = len(leading_space(raw_line))
370
+ clean_indent = len(leading_space(clean_line))
371
+ try:
372
+ linestart = doc.value.rindex("\n", 0, offset) + 1
373
+ except ValueError: # pragma: no cover
374
+ linestart = 0 # paranoid check, should not really happen
375
+ colnum = offset - linestart + raw_indent - clean_indent
376
+
377
+ except Exception:
378
+ # Don't expect to get here, but just in case, it is better to
379
+ # not fix up the line/column than to die.
380
+ pass
381
+
382
+ linenum += lineoffset
383
+
384
+ return linenum, colnum + 1
385
+
386
+
387
+ def leading_space(s: str) -> str:
388
+ """Returns whitespace at the front of string."""
389
+ if m := re.match(r"\s*", s):
390
+ return m[0]
391
+ return "" # pragma: no cover
392
+
393
+ if sys.version_info < (3, 10) or True:
394
+ # TODO: remove when 3.9 support is dropped
395
+ # In 3.9, literal_eval cannot handle comments in input
396
+ def safe_eval(s: str) -> Any:
397
+ """Safely evaluate a string expression."""
398
+ return eval(s) #eval(s, dict(__builtins__={}), {})
399
+ else:
400
+ save_eval = ast.literal_eval
401
+
@@ -17,8 +17,10 @@ Implementation of python_xref handler
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import re
20
21
  import sys
21
- from dataclasses import dataclass, fields
22
+ from dataclasses import dataclass, field, fields
23
+ from functools import partial
22
24
  from pathlib import Path
23
25
  from typing import Any, ClassVar, Mapping, MutableMapping, Optional
24
26
  from warnings import warn
@@ -43,6 +45,7 @@ if sys.version_info >= (3, 10):
43
45
  @dataclass(**_dataclass_options)
44
46
  class PythonRelXRefOptions(PythonOptions):
45
47
  check_crossrefs: bool = True
48
+ check_crossrefs_exclude: list[str | re.Pattern] = field(default_factory=list)
46
49
 
47
50
  class PythonRelXRefHandler(PythonHandler):
48
51
  """Extended version of mkdocstrings Python handler
@@ -62,26 +65,30 @@ class PythonRelXRefHandler(PythonHandler):
62
65
  base_dir: The base directory of the project.
63
66
  **kwargs: Arguments passed to the parent constructor.
64
67
  """
65
- check_crossrefs = config.options.pop('check_crossrefs', None) # Remove
68
+ self.check_crossrefs = config.options.pop('check_crossrefs', True)
69
+ exclude = config.options.pop('check_crossrefs_exclude', [])
70
+ self.check_crossrefs_exclude = [re.compile(p) for p in exclude]
66
71
  super().__init__(config, base_dir, **kwargs)
67
- if check_crossrefs is not None:
68
- self.global_options["check_crossrefs"] = check_crossrefs
69
72
 
70
73
  def get_options(self, local_options: Mapping[str, Any]) -> PythonRelXRefOptions:
71
74
  local_options = dict(local_options)
72
- check_crossrefs = local_options.pop('check_crossrefs', None)
75
+ check_crossrefs = local_options.pop(
76
+ 'check_crossrefs', self.check_crossrefs)
77
+ check_crossrefs_exclude = local_options.pop(
78
+ 'check_crossrefs_exclude', self.check_crossrefs_exclude)
73
79
  _opts = super().get_options(local_options)
74
80
  opts = PythonRelXRefOptions(
81
+ check_crossrefs=check_crossrefs,
82
+ check_crossrefs_exclude=check_crossrefs_exclude,
75
83
  **{field.name: getattr(_opts, field.name) for field in fields(_opts)}
76
84
  )
77
- if check_crossrefs is not None:
78
- opts.check_crossrefs = bool(check_crossrefs)
79
85
  return opts
80
86
 
81
87
  def render(self, data: CollectorItem, options: PythonOptions) -> str:
82
88
  if options.relative_crossrefs:
83
- if isinstance(options, PythonRelXRefOptions):
84
- checkref = self._check_ref if options.check_crossrefs else None
89
+ if isinstance(options, PythonRelXRefOptions) and options.check_crossrefs:
90
+ checkref = partial(
91
+ self._check_ref, exclude=options.check_crossrefs_exclude)
85
92
  else:
86
93
  checkref = None
87
94
  substitute_relative_crossrefs(data, checkref=checkref)
@@ -98,8 +105,11 @@ class PythonRelXRefHandler(PythonHandler):
98
105
  handler = 'python'
99
106
  return super().get_templates_dir(handler)
100
107
 
101
- def _check_ref(self, ref:str) -> bool:
108
+ def _check_ref(self, ref : str, exclude: list[str | re.Pattern] = []) -> bool:
102
109
  """Check for existence of reference"""
110
+ for ex in exclude:
111
+ if re.match(ex, ref):
112
+ return True
103
113
  try:
104
114
  self.collect(ref, PythonOptions())
105
115
  return True