hippogriffe 0.1.3__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hippogriffe
3
- Version: 0.1.3
3
+ Version: 0.2.1
4
4
  Summary: Tweaks for `mkdocstrings[python]`
5
5
  Project-URL: repository, https://github.com/patrick-kidger/hippogriffe
6
6
  Author-email: Patrick Kidger <contact@kidger.site>
@@ -265,9 +265,9 @@ plugins:
265
265
  - hippogriffe:
266
266
  show_bases: true/false
267
267
  show_source_links: all/toplevel/none
268
- extra_public_modules:
269
- - foo
270
- - bar
268
+ extra_public_objects:
269
+ - foo.SomeClass
270
+ - bar.subpackage.some_function
271
271
  ```
272
272
 
273
273
  **show_bases:**
@@ -278,19 +278,21 @@ If `false` then base classes will not be displayed alongside a class. Defaults t
278
278
 
279
279
  Sets which objects will have links to their location in the repository (as configured via the usual MkDocs `repo_url`). If `all` then all objects will have links. If `toplevel` then just `::: somelib.value` will have links, but their members will not. If `none` then no links will be added. Defaults to `toplevel`.
280
280
 
281
- **extra_public_modules:**
281
+ **extra_public_objects:**
282
282
 
283
- A list of module names whose elements should be treated as part of the public API. Pretty-formatting of type annotations is done strictly: every annotation must be part of the known public API, else an error will be raised. The public API is defined as the combination of:
283
+ Pretty-formatting of type annotations is done strictly: every annotation must be part of the known public API, else an error will be raised. The public API is defined as the combination of:
284
284
 
285
285
  - Everything you document using `::: yourlib.Foo`, and all of their members.
286
286
  - Anything from the standard library.
287
- - All objects belonging to any of `extra_public_modules`.
287
+ - All objects belonging to any of `extra_public_objects`.
288
288
 
289
289
  For example,
290
290
  ```yml
291
291
  plugins:
292
292
  - hippogriffe:
293
- extra_public_modules:
294
- - jax
295
- - torch
293
+ extra_public_objects:
294
+ - jax.Array
295
+ - torch.Tensor
296
296
  ```
297
+
298
+ List each object under whatever public path `somelib.Foo` that you would like it to be displayed under (and from which it must be accessible), not whichever private path `somelib._internal.foo.Foo` it is defined at.
@@ -43,9 +43,9 @@ plugins:
43
43
  - hippogriffe:
44
44
  show_bases: true/false
45
45
  show_source_links: all/toplevel/none
46
- extra_public_modules:
47
- - foo
48
- - bar
46
+ extra_public_objects:
47
+ - foo.SomeClass
48
+ - bar.subpackage.some_function
49
49
  ```
50
50
 
51
51
  **show_bases:**
@@ -56,19 +56,21 @@ If `false` then base classes will not be displayed alongside a class. Defaults t
56
56
 
57
57
  Sets which objects will have links to their location in the repository (as configured via the usual MkDocs `repo_url`). If `all` then all objects will have links. If `toplevel` then just `::: somelib.value` will have links, but their members will not. If `none` then no links will be added. Defaults to `toplevel`.
58
58
 
59
- **extra_public_modules:**
59
+ **extra_public_objects:**
60
60
 
61
- A list of module names whose elements should be treated as part of the public API. Pretty-formatting of type annotations is done strictly: every annotation must be part of the known public API, else an error will be raised. The public API is defined as the combination of:
61
+ Pretty-formatting of type annotations is done strictly: every annotation must be part of the known public API, else an error will be raised. The public API is defined as the combination of:
62
62
 
63
63
  - Everything you document using `::: yourlib.Foo`, and all of their members.
64
64
  - Anything from the standard library.
65
- - All objects belonging to any of `extra_public_modules`.
65
+ - All objects belonging to any of `extra_public_objects`.
66
66
 
67
67
  For example,
68
68
  ```yml
69
69
  plugins:
70
70
  - hippogriffe:
71
- extra_public_modules:
72
- - jax
73
- - torch
71
+ extra_public_objects:
72
+ - jax.Array
73
+ - torch.Tensor
74
74
  ```
75
+
76
+ List each object under whatever public path `somelib.Foo` that you would like it to be displayed under (and from which it must be accessible), not whichever private path `somelib._internal.foo.Foo` it is defined at.
@@ -2,6 +2,7 @@ import ast
2
2
  import builtins
3
3
  import contextlib
4
4
  import functools as ft
5
+ import importlib
5
6
  import inspect
6
7
  import pathlib
7
8
  import re
@@ -33,23 +34,38 @@ class _PublicApi:
33
34
  pkg: griffe.Module,
34
35
  top_level_public_api: set[str],
35
36
  builtin_modules: list[str],
36
- stdlib_modules: list[str],
37
- extra_public_modules: list[str],
37
+ extra_public_objects: list[str],
38
38
  ):
39
39
  self._objects: set[griffe.Object] = set()
40
40
  self._toplevel_objects: set[griffe.Object] = set()
41
41
  self._data: dict[str, list[str]] = {}
42
42
  self._builtin_modules = builtin_modules
43
- self._public_modules = stdlib_modules + extra_public_modules
43
+ for object_path in extra_public_objects:
44
+ object_pieces = object_path.split(".")
45
+ for i in reversed(range(1, len(object_pieces))):
46
+ module_name = "".join(object_pieces[:i])
47
+ object_name = object_pieces[i:]
48
+ try:
49
+ object = importlib.import_module(module_name)
50
+ except Exception:
51
+ continue
52
+ for object_piece in object_name:
53
+ object = getattr(object, object_piece)
54
+ private_path = f"{object.__module__}.{object.__qualname__}"
55
+ try:
56
+ paths = self._data[private_path]
57
+ except KeyError:
58
+ paths = self._data[private_path] = []
59
+ paths.append(object_path)
44
60
  # Don't infinite loop on cycles. We only store Objects, and not Aliases, as in
45
61
  # cycles then the aliases with be distinct: `X.Y.X.Y` is not `X.Y`, though the
46
62
  # underlying object is the same.
47
63
 
48
- agenda: list[tuple[griffe.Object, bool]] = [(pkg, False)]
64
+ agenda: list[tuple[griffe.Object, str, bool]] = [(pkg, pkg.path, False)]
49
65
  seen: set[griffe.Object] = {pkg}
50
66
  while len(agenda) > 0:
51
- item, force_public = agenda.pop()
52
- toplevel_public = item.path in top_level_public_api
67
+ item, public_path, force_public = agenda.pop()
68
+ toplevel_public = public_path in top_level_public_api
53
69
  if force_public or toplevel_public:
54
70
  # If we're in the public API, then we consider all of our children to be
55
71
  # in it as well... (this saves us from having to parse out `filters` and
@@ -58,7 +74,7 @@ class _PublicApi:
58
74
  paths = self._data[item.path]
59
75
  except KeyError:
60
76
  paths = self._data[item.path] = []
61
- paths.append(item.path)
77
+ paths.append(public_path)
62
78
  self._objects.add(item)
63
79
  if toplevel_public:
64
80
  self._toplevel_objects.add(item)
@@ -70,13 +86,13 @@ class _PublicApi:
70
86
  for member in item.all_members.values():
71
87
  # Skip private elements
72
88
  if member.name.startswith("_") and not (
73
- member.name.startswith("__") and item.name.endswith("__")
89
+ member.name.startswith("__") and member.name.endswith("__")
74
90
  ):
75
91
  continue
76
92
  if isinstance(member, griffe.Alias):
77
93
  try:
78
94
  final_member = member.final_target
79
- except griffe.AliasResolutionError:
95
+ except (griffe.AliasResolutionError, griffe.CyclicAliasError):
80
96
  continue
81
97
  if member.name != final_member.name:
82
98
  # Renaming during import counts as private.
@@ -87,7 +103,7 @@ class _PublicApi:
87
103
  final_member = member
88
104
  if final_member in seen:
89
105
  continue
90
- agenda.append((final_member, sub_force_public))
106
+ agenda.append((final_member, member.path, sub_force_public))
91
107
  seen.add(final_member)
92
108
 
93
109
  def toplevel(self) -> Iterable[griffe.Object]:
@@ -103,16 +119,17 @@ class _PublicApi:
103
119
  for m in self._builtin_modules:
104
120
  if key.startswith(m + "."):
105
121
  return key.removeprefix(m + "."), False
106
- for m in self._public_modules:
122
+ for m in sys.stdlib_module_names:
107
123
  if key.startswith(m + "."):
108
124
  return key, False
109
- # Not using `KeyError` because that displays its message with `repr`.
125
+ # Note that this message must not have any newlines in it, to display
126
+ # correctly.
110
127
  raise _NotInPublicApiException(
111
- f"Tried and failed to find {key} in the public API. Commons reasons "
112
- "for this error are:\n"
113
- "- If it is from outside this package, then that package is not listed "
114
- "under the `hippogriffe.extra_public_modules`\n"
115
- "- If it is from inside this package, then it may have been written "
128
+ f"Tried and failed to find `{key}` in the public API. Commons reasons "
129
+ "for this error are (1) if it is from outside this package, then this "
130
+ "object is not listed (under whatever public path it should be "
131
+ "displayed as) in `hippogriffe.extra_public_objects`; (2) if it is "
132
+ "from inside this package, then it may have been written "
116
133
  "`::: somelib.Foo:` with a trailing colon, when just `:::somelib.Foo` "
117
134
  "is correct."
118
135
  ) from e
@@ -211,20 +228,17 @@ def _resolved_bases(cls: griffe.Class) -> list[str | griffe.Object]:
211
228
  return resolved_bases
212
229
 
213
230
 
214
- def _collect_bases(
215
- cls: griffe.Class, public_api: _PublicApi, public_modules: set[str]
216
- ) -> dict[str, bool]:
231
+ def _collect_bases(cls: griffe.Class, public_api: _PublicApi) -> dict[str, bool]:
217
232
  bases: dict[str, bool] = {}
218
233
  for base in _resolved_bases(cls):
219
234
  if isinstance(base, str):
220
235
  # builtins case above
221
- if "builtins" in public_modules:
222
- bases[base] = False
236
+ bases[base] = False
223
237
  elif isinstance(base, griffe.Class):
224
238
  try:
225
239
  base, autoref = public_api[base.path]
226
240
  except _NotInPublicApiException:
227
- bases.update(_collect_bases(base, public_api, public_modules))
241
+ bases.update(_collect_bases(base, public_api))
228
242
  else:
229
243
  bases[base] = autoref
230
244
  return bases
@@ -254,11 +268,23 @@ def _get_repo_url(repo_url: None | str) -> tuple[pathlib.Path, str]:
254
268
  else:
255
269
  toplevel = pathlib.Path(git_toplevel.stdout.decode().strip())
256
270
  commit_hash = git_head.stdout.decode().strip()
257
- raw_url = repo_url.removeprefix("https://").removeprefix("https://")
258
- if raw_url.startswith("github.com") or raw_url.startswith("gitlab.com"):
271
+ if "://" in repo_url:
272
+ protocol, repo_url = repo_url.split("://", 1)
273
+ protocol = f"{protocol}://"
274
+ else:
275
+ protocol = ""
276
+ supported_site = False
277
+ if repo_url.startswith("github.com"):
278
+ supported_site = True
279
+ fragment = "L{start}-L{end}"
280
+ elif repo_url.startswith("gitlab.com"):
281
+ supported_site = True
282
+ fragment = "L{start}-{end}"
283
+ if supported_site:
284
+ # Expect url in the form `https://github.com/org/repo`, strip any trailing paths
285
+ repo_url = "/".join(repo_url.split("/")[:3])
259
286
  repo_url = (
260
- f"{repo_url.removesuffix('/')}/blob/{commit_hash}/{{path}}"
261
- "#L{start}-{end}"
287
+ f"{protocol}{repo_url}/blob/{commit_hash}/{{path}}#{fragment}"
262
288
  )
263
289
  else:
264
290
  # We need to format the `repo_url` to what the repo expects, so we have to
@@ -324,8 +350,7 @@ class HippogriffeExtension(griffe.Extension):
324
350
  pkg,
325
351
  top_level_public_api=self.top_level_public_api,
326
352
  builtin_modules=self.config.builtin_modules,
327
- stdlib_modules=self.config.stdlib_modules,
328
- extra_public_modules=self.config.extra_public_modules,
353
+ extra_public_objects=self.config.extra_public_objects,
329
354
  )
330
355
 
331
356
  def use_public_name(context: None | dict, obj: Any) -> None | wl.AbstractDoc:
@@ -342,21 +367,34 @@ class HippogriffeExtension(griffe.Extension):
342
367
  new_path, _ = public_api[f"{obj.__module__}.{obj.__qualname__}"]
343
368
  return wl.TextDoc(new_path)
344
369
 
345
- public_modules = set(self.config.extra_public_modules) | set(
346
- self.config.stdlib_modules
347
- )
348
370
  for obj in public_api:
349
371
  if obj.is_function:
350
372
  assert type(obj) is griffe.Function
351
- obj.extra["mkdocstrings"]["template"] = "hippogriffe/fn.html.jinja"
352
- _pretty_fn(obj, use_public_name)
373
+ try:
374
+ _pretty_fn(obj, use_public_name)
375
+ except _NotInPublicApiException as e:
376
+ # Defer error until later -- right now our `public_api` is an
377
+ # overestimation of the 'true' public API as for a public class
378
+ # `Foo` then we actually include all of its attributes in the public
379
+ # API here, even if those aren't documented.
380
+ # It's fairly common to have nonpublic annotations in nonpublic
381
+ # methods, and we shouldn't die on those now -- if the method is
382
+ # never documented then we don't need to worry. Putting this here is
383
+ # totally sneaky, it's letting jinja think this is a template and
384
+ # having it raise a TemplateNotFound error when it tries to format
385
+ # this object.
386
+ obj.extra["mkdocstrings"]["template"] = (
387
+ f"{e} This arose whilst pretty-printing `{obj.path}`. You may "
388
+ "ignore the rest of this error message, which comes from jinja."
389
+ " "
390
+ )
391
+ else:
392
+ obj.extra["mkdocstrings"]["template"] = "hippogriffe/fn.html.jinja"
353
393
  elif obj.is_class:
354
394
  assert type(obj) is griffe.Class
355
395
  obj.extra["mkdocstrings"]["template"] = "hippogriffe/class.html.jinja"
356
396
  if self.config.show_bases:
357
- public_bases = list(
358
- _collect_bases(obj, public_api, public_modules).items()
359
- )
397
+ public_bases = list(_collect_bases(obj, public_api).items())
360
398
  obj.extra["hippogriffe"]["public_bases"] = public_bases
361
399
 
362
400
  if self.config.show_source_links == "none":
@@ -1,6 +1,5 @@
1
1
  import pathlib
2
2
  import re
3
- import sys
4
3
  import typing
5
4
 
6
5
  from mkdocs.config import Config
@@ -25,13 +24,8 @@ class PluginConfig(Config):
25
24
  show_source_links = Choice(["all", "toplevel", "none"], default="toplevel")
26
25
  """Whether to include [source] links to the repo."""
27
26
 
28
- extra_public_modules = Type(list, default=[])
29
- """Any third-party modules whose objects are allowed in the public API."""
30
-
31
- stdlib_modules = Type(list, default=list(sys.stdlib_module_names))
32
- """A list of stdlib modules. This is concatenated with `extra_public_modules` to
33
- form the list of external modules that are allowed in the public API
34
- """
27
+ extra_public_objects = Type(list, default=[])
28
+ """Any third-party objects which are allowed in the public API."""
35
29
 
36
30
  builtin_modules = Type(
37
31
  list, default=["builtins", "collections.abc", "typing", "typing_extensions"]
@@ -25,7 +25,7 @@ name = "hippogriffe"
25
25
  readme = "README.md"
26
26
  requires-python = ">=3.10"
27
27
  urls = {repository = "https://github.com/patrick-kidger/hippogriffe"}
28
- version = "0.1.3"
28
+ version = "0.2.1"
29
29
 
30
30
  [project.optional-dependencies]
31
31
  dev = ["pre-commit"]
File without changes
File without changes