Skip to content

Commit 2a8d824

Browse files
committed
feat: Expose Git info in objects, allowing to compute a new source_link property
This change also deprecates the previously exposed Git utilities. There are other third-party libraries that do that better than Griffe, so these utilities are now internal and not meant for user consumption. Issue-361: #361 Issue-mkdocstrings-python-253: mkdocstrings/python#253
1 parent bff6f61 commit 2a8d824

18 files changed

Lines changed: 626 additions & 67 deletions

File tree

docs/guide/users.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,20 @@ These how-tos will show you how to achieve specific things with Griffe.
112112

113113
[:octicons-arrow-right-24: See how to selectively inspect objects](users/how-to/selectively-inspect.md)
114114

115-
- :material-select:{ .lg .middle } **Set objects' docstring style**
115+
- :material-bow-tie:{ .lg .middle } **Set objects' docstring style**
116116

117117
---
118118

119119
Sometimes the wrong docstring styles are attached to objects. You can fix this with a few different methods.
120120

121121
[:octicons-arrow-right-24: See how to set the correct docstring styles on objects](users/how-to/set-docstring-styles.md)
122122

123+
- :simple-git:{ .lg .middle } **Set Git source info on objects**
124+
125+
---
126+
127+
Griffe tries to find the right Git remote URL to provide source links to loaded objects. In some cases you might want to override the Git information or the source link directly.
128+
129+
[:octicons-arrow-right-24: See how to set the correct Git information or source link on objects](users/how-to/set-git-info.md)
130+
123131
</div>
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Set Git information and source link on objects
2+
3+
Griffe tries to set [source information][source-information] on each package it loads. Sometimes it won't be able to find such information, or to find the correct information. In this case, you can programmatically set the right information with a Griffe extension. This will let you fix or customize the source links for many objects at once or for specific objects.
4+
5+
## Git information on whole packages
6+
7+
In this example we see how to set the Git information for whole packages. This will affect every object in these packages, and therefore the source link for each object.
8+
9+
Start by creating an extensions module (a simple Python file) somewhere in your repository, if you don't already have one. Within it, create an extension class:
10+
11+
```python
12+
import griffe
13+
14+
15+
class GitInfo(griffe.Extension):
16+
"""An extension to set the right Git information."""
17+
```
18+
19+
Next we hook onto the `on_package` event to override the `git_info` attribute of the packages we are interested into.
20+
21+
```python
22+
from pathlib import Path
23+
from typing import Any
24+
25+
import griffe
26+
27+
28+
class GitInfo(griffe.Extension):
29+
"""An extension to set the right Git information."""
30+
31+
def on_package(self, *, pkg: griffe.Module, **kwargs: Any) -> None:
32+
if pkg.name == "my_package_name":
33+
pkg.git_info = griffe.GitInfo(
34+
repository=Path("/path/to/this/package/local/repository"),
35+
service="forgejo",
36+
remote_url="https://myhostedforge.mydomain.com/myaccount/myproject",
37+
commit_hash="77f928aeab857cb45564462a4f849c2df2cca99a",
38+
)
39+
```
40+
41+
Here we hardcode the commit hash, but ideally we would obtain it by running a Git command in a subprocess, or any other way that gives a relevant commit hash.
42+
43+
```python
44+
import subprocess
45+
46+
process = subprocess.run(["git", "-C", repo, "rev-parse", "HEAD"], text=True, capture_output=True)
47+
commit_hash = process.stdout.strip()
48+
```
49+
50+
We could also reuse properties that Griffe found:
51+
52+
```python
53+
# Here we reuse `repository` and `commit_hash` while overriding only `service` and `remote_url`.
54+
pkg.git_info = griffe.GitInfo(
55+
repository=pkg.git_info.repository,
56+
service="forgejo",
57+
remote_url="https://myhostedforge.mydomain.com/myaccount/myproject",
58+
commit_hash=pkg.git_info.commit_hash,
59+
)
60+
61+
# We could also mutate the original `GitInfo` object:
62+
pkg.git_info.service = "forgejo"
63+
```
64+
65+
Now, with this extension enabled (see [Using extensions][using-extensions]), every object source link in our `my_package_name` package will be based on this Git information. For example, the source link for `my_package_name.my_function` would be something like `https://myhostedforge.mydomain.com/myaccount/myproject/src/commit/77f928aeab857cb45564462a4f849c2df2cca99a/src/my_package_name/__init__.py#L35-L48`.
66+
67+
## Source links on specific objects
68+
69+
Let say you expose Python objects in your API that are compiled from other sources (C extension, Pyo3 code, etc.). Let say you also know the filepath and line numbers for each of these compiled objects. With this information, you could fix the source link for these compiled objects so that they point to the actual sources, and not to the final modules, where the line numbers would be incorrect (or to nowhere since we wouldn't have line numbers in the first place).
70+
71+
Start by creating an extensions module (a simple Python file) somewhere in your repository, if you don't already have one. Within it, create an extension class:
72+
73+
```python
74+
import griffe
75+
76+
77+
class SourceLinks(griffe.Extension):
78+
"""An extension to set the right source links."""
79+
```
80+
81+
Next we hook onto the `on_object` event to override the `source_link` attribute of the objects we are interested into.
82+
83+
```python
84+
from pathlib import Path
85+
from typing import Any
86+
87+
import griffe
88+
89+
90+
class SourceLinks(griffe.Extension):
91+
"""An extension to set the right source links."""
92+
93+
def on_object(self, *, obj: griffe.Object, **kwargs: Any) -> None:
94+
if obj.path == "my_package_name.my_function":
95+
obj.source_link = "https://myhostedforge.mydomain.com/myaccount/myproject/src/commit/77f928aeab857cb45564462a4f849c2df2cca99a/src/lib.rs#L35-L48"
96+
# Handle any other object you want.
97+
elif ...:
98+
...
99+
```
100+
101+
Here we hardcode the link, but we can also reuse the Git information of the package to just correct the filepath and line numbers:
102+
103+
```python
104+
from pathlib import Path
105+
from typing import Any
106+
107+
import griffe
108+
109+
110+
class SourceLinks(griffe.Extension):
111+
"""An extension to set the right source links."""
112+
113+
def on_object(self, *, obj: griffe.Object, **kwargs: Any) -> None:
114+
if obj.path == "my_package_name.my_function":
115+
obj.source_link = obj.git_info.get_source_link(
116+
filepath="src/lib.rs",
117+
lineno=35,
118+
endlineno=48,
119+
)
120+
# Handle any other object you want.
121+
elif ...:
122+
...
123+
```

docs/guide/users/loading.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ griffe.load("itertools", allow_inspection=False)
133133

134134
## Alias resolution
135135

136-
>? QUESTION: **What's that?**
136+
>? QUESTION: **What's that?**
137137
> In Griffe, indirections to objects are called *aliases*. These indirections, or aliases, represent two kinds of objects: imported objects and inherited objects. Indeed, an imported object is "aliased" in the module that imports it, while its true location is in the module it was imported from. Similarly, a method inherited from a parent class is "aliased" in the subclass, while its true location is in the parent class.
138138
>
139139
> The name "alias" comes from the fact that imported objects can be aliased under a different name: `from X import A as B`. In the case of inherited members, this doesn't really apply, but we reuse the concept for conciseness.
@@ -325,6 +325,37 @@ While automatically resolving aliases pointing at external packages can be conve
325325

326326
One special case that we must mention is that Griffe will by default automatically load *private sibling packages*. For example, when resolving aliases for the `ast` module, Griffe will automatically try and load `_ast` too (if dynamic analysis is allowed, since this is a builtin module), even without `resolve_external=True`. If you want to prevent this behavior, you can pass `resolve_external=False` (it is `None` by default).
327327

328+
## Source information
329+
330+
By default, Griffe runs some Git commands to find the following information about a package:
331+
332+
- the repository local path
333+
- the Git remote URL
334+
- what service it corresponds to (GitHub, etc.)
335+
- the current commit hash
336+
337+
It then assigns this information to each package it loads, in the [`git_info`][griffe.Object.git_info] attribute. This attribute can be reassigned on any object, if necessary. Each object who has it set to `None` will look into its parents.
338+
339+
In the following cases, the information will not be set:
340+
341+
- Griffe couldn't find the source for an object, or line numbers in the source
342+
- the source of a package is not tracked within the identified repository
343+
- Griffe cannot identify a known, supported service from the remote URL
344+
- any Git command failed
345+
346+
Griffe supports the services listed in the [`KnownGitService`][griffe.KnownGitService] symbol. Please open a feature request if you would like to add other support for other services.
347+
348+
Thanks to this source information, Griffe can then compute source links for each objects, by combining the information with the object's filepath and line numbers.
349+
350+
You can globally change how Griffe obtains the source information with the following environment variables:
351+
352+
- `GRIFFE_GIT_REMOTE_URL`: It is the repository remote URL, as an HTTPS link that readers of your documentation can access to see the repository online, on the service it is hosted on. Example: `GRIFFE_GIT_REMOTE_URL=https://app.radicle.at/nodes/seed.radicle.at/rad:z4M5XTPDD4Wh1sm8iPCenF85J3z8Z`.
353+
- `GRIFFE_GIT_REMOTE`: You can also let Griffe obtain the remote URL by getting it from the Git local configuration. The Git remote defaults to `origin`. This environment variable lets you change it to something else. Example: `GRIFFE_GIT_REMOTE=upstream`.
354+
- `GRIFFE_GIT_SERVICE`: Griffe infers the service by looking at the remote URL. If the remote URL contains a [known service name][griffe.KnownGitService], Griffe will use it as service. You can otherwise explicitly set the service using this environment variable. Example: `GRIFFE_GIT_SERVICE=codeberg`.
355+
- `GRIFFE_GIT_COMMIT_HASH`: Griffe gets the commit hash by running a Git command. If you prefer using another commit hash, you can set it using this environment variable. Example: `GRIFFE_GIT_COMMIT_HASH=77f928aeab857cb45564462a4f849c2df2cca99a`.
356+
357+
For more complex cases, see [How to programmatically set the correct Git information or source link on objects](how-to/set-git-info.md).
358+
328359
## Next steps
329360

330361
Now that the API is loaded, you can start [navigating it](navigating.md), [serializing it](serializing.md) or [checking for API breaking changes](checking.md). If you find out that the API data is incorrect or incomplete, you might want to learn how to [extend it](extending.md).

docs/reference/api/git.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Git utilities
22

3+
<!-- YORE: Bump 2: Remove file. -->
4+
5+
DANGER: **Deprecated utilities.** We have decided to stop exposing Git-related utilities as it's not a core part of the library's functionality. The functions documented on this page will become unavailable in the next major version.
6+
37
::: griffe.assert_git_repo
48

59
::: griffe.get_latest_tag

docs/reference/api/models.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The 6 models:
2121

2222
::: griffe.Kind
2323

24-
## **Models base classes**
24+
## **Model base classes**
2525

2626
::: griffe.GetMembersMixin
2727

@@ -35,10 +35,16 @@ The 6 models:
3535

3636
::: griffe.Object
3737

38-
## **Models type parameter**
38+
## **Type parameters**
3939

4040
::: griffe.TypeParameters
4141

4242
::: griffe.TypeParameter
4343

4444
::: griffe.TypeParameterKind
45+
46+
## **Git information**
47+
48+
::: griffe.KnownGitService
49+
50+
::: griffe.GitInfo

docs/schema.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@
153153
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.relative_package_filepath",
154154
"type": "string"
155155
},
156+
"git_info": {
157+
"title": "The Git information associated to this object's package.",
158+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Object.git_info",
159+
"$ref": "#/$defs/GitInfo"
160+
},
161+
"source_link": {
162+
"title": "The source link of the object.",
163+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.source_link",
164+
"type": [
165+
"string",
166+
"null"
167+
]
168+
},
156169
"public": {
157170
"title": "Whether the object was explicitly marked as public.",
158171
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.public",
@@ -579,6 +592,58 @@
579592
"annotation",
580593
"default"
581594
]
595+
},
596+
"GitInfo": {
597+
"title": "Git information.",
598+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.GitInfo",
599+
"type": "object",
600+
"properties": {
601+
"repository": {
602+
"title": "The repository local path.",
603+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.GitInfo.repository",
604+
"type": [
605+
"string"
606+
]
607+
},
608+
"service": {
609+
"title": "The Git service (e.g., 'github', 'gitlab', 'bitbucket').",
610+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.GitInfo.service",
611+
"type": [
612+
"string"
613+
],
614+
"enum": [
615+
"github",
616+
"gitlab",
617+
"sourcehut",
618+
"gitea",
619+
"gogs",
620+
"forgejo",
621+
"codeberg",
622+
"radicle"
623+
]
624+
},
625+
"remote_url": {
626+
"title": "The remote URL.",
627+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.GitInfo.remote_url",
628+
"type": [
629+
"string"
630+
]
631+
},
632+
"commit_hash": {
633+
"title": "The commit hash.",
634+
"markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.GitInfo.commit_hash",
635+
"type": [
636+
"string"
637+
]
638+
}
639+
},
640+
"additionalProperties": false,
641+
"required": [
642+
"repository",
643+
"service",
644+
"remote_url",
645+
"commit_hash"
646+
]
582647
}
583648
}
584649
}

duties.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,13 @@ def check_api(ctx: Context, *cli_args: str) -> None:
302302
*cli_args: Additional Griffe CLI arguments.
303303
"""
304304
ctx.run(
305-
tools.griffe.check("griffe", search=["src"], color=True).add_args(*cli_args),
305+
tools.griffe.check(
306+
"griffe",
307+
search=["src"],
308+
color=True,
309+
# YORE: Bump 2: Remove line.
310+
extensions=["scripts/griffe_exts.py"],
311+
).add_args(*cli_args),
306312
title="Checking for API breaking changes",
307313
nofail=True,
308314
)

mkdocs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ nav:
5050
- Support custom decorators: guide/users/how-to/support-decorators.md
5151
- Selectively inspect objects: guide/users/how-to/selectively-inspect.md
5252
- Set objects' docstring style: guide/users/how-to/set-docstring-styles.md
53+
- Set Git info on objects: guide/users/how-to/set-git-info.md
5354
- Contributor guide:
5455
- guide/contributors.md
5556
- Environment setup: guide/contributors/setup.md
@@ -108,6 +109,7 @@ nav:
108109
- Parsers: reference/api/docstrings/parsers.md
109110
- Exceptions: reference/api/exceptions.md
110111
- Expressions: reference/api/expressions.md
112+
# YORE: Bump 2: Remove line.
111113
- Git utilities: reference/api/git.md
112114
- Loggers: reference/api/loggers.md
113115
- Helpers: reference/api/helpers.md
@@ -232,6 +234,8 @@ plugins:
232234
docstring_section_style: list
233235
extensions:
234236
- griffe_inherited_docstrings
237+
# YORE: Bump 2: Remove line.
238+
- scripts/griffe_exts.py
235239
heading_level: 2
236240
inherited_members: true
237241
merge_init_into_class: true

scripts/griffe_exts.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# YORE: Bump 2: Remove file.
2+
3+
from typing import Any
4+
5+
import griffe
6+
7+
8+
class ModuleGetAttrExtension(griffe.Extension):
9+
def on_package(self, *, pkg: griffe.Module, **kwargs: Any) -> None: # noqa: ARG002,D102
10+
if pkg.name == "griffe":
11+
for name in griffe._deprecated_names:
12+
try:
13+
target = pkg[f"_internal.git._{name}"]
14+
except KeyError:
15+
# Old version where the utility was not yet renamed.
16+
continue
17+
pkg.set_member(name, griffe.Alias(name, target=f"griffe._internal.git._{name}"))
18+
admonition = griffe.DocstringSectionAdmonition(
19+
kind="danger",
20+
text="",
21+
title="This function is deprecated and will become unavailable in the next major version.",
22+
)
23+
target.docstring.parsed.insert(1, admonition)
24+
target.labels.add("deprecated")

0 commit comments

Comments
 (0)