Repository navigation
docs: add the Markdown reference adapter #1182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
+579
β14
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
d811ca2
docs: add the Markdown reference adapter
gewenyu99 b5a6328
docs: render the reference in one Python script
gewenyu99 7c20b46
docs: write the latest reference as its own file, with the site's froβ¦
gewenyu99 bdfa218
docs: leave indexing to the site
gewenyu99 23e61f0
docs: render the reference with griffe2md
gewenyu99 ebbc5b4
Merge main into markdown-reference-adapter
gewenyu99 6f5be01
docs: write signatures with griffe and mark async methods
gewenyu99 4e27330
docs: map capabilities in docs/capabilities.json instead of docstrings
gewenyu99 47f7b75
docs: list v1's context option methods in SDK utils
gewenyu99 51c7b46
test: check the reference generator's signatures
gewenyu99 72316e6
docs: leave private parameters out of reference signatures
gewenyu99 325861c
docs: keep whole summaries and Returns, and call constructors by clasβ¦
gewenyu99 fb0019a
docs: fix three docstring examples
gewenyu99 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| """Generate the SDK reference Markdown from source with griffe2md, which never imports the SDK. | ||
|
|
||
| docs/capabilities.json maps each entry's label to a PostHog/sdk-specs capability, shown in | ||
| the sdk-specs README's order. Its `utils` lists the Client members that go to SDK utils, and | ||
| its `next` maps labels that only the next major has. | ||
|
|
||
| python bin/docs_scripts/generate_markdown.py --sdk-specs DIR [--major 8] | ||
| """ | ||
|
|
||
| import argparse | ||
| import copy | ||
| import json | ||
| import logging | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
| import griffe | ||
| import griffe2md | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[2] | ||
| TYPES = """args.OptionalCaptureArgs feature_flag_evaluations.FeatureFlagEvaluations types.FeatureFlagResult | ||
| types.FeatureFlag types.FlagsAndPayloads types.UnresolvedFlagReason feature_flags.InconclusiveMatchError | ||
| feature_flags.RequiresServerEvaluation capture_send.CaptureError tracing.span.Span emails.Emails | ||
| emails.EmailError emails.SendEmailResult args.OptionalSetArgs emails.CodeEmailArgs""".split() | ||
| UTILS = """integrations.django.PosthogContextMiddleware integrations.flask.PosthogFlaskIntegration | ||
| integrations.asgi.PosthogASGIMiddleware integrations.celery.PosthogCeleryIntegration | ||
| request.set_socket_options request.enable_keep_alive request.disable_connection_reuse""".split() | ||
| FIRST = ["setup", "capture", "identify"] | ||
| ROLE = re.compile( | ||
| r":(?:py:)?(?:meth|func|class|attr|obj|data|exc|mod|const):`~?([^`]+)`" | ||
| ) | ||
| CONFIG = { | ||
| **griffe2md.default_config, | ||
| "docstring_section_style": "table", | ||
| "show_root_full_path": False, | ||
| "show_signature_annotations": True, | ||
| "separate_signature": True, | ||
|
gewenyu99 marked this conversation as resolved.
Resolved
|
||
| "heading_level": 3, | ||
| } | ||
|
|
||
|
|
||
| def clean(obj) -> None: | ||
| """Strips Sphinx roles, and the Category section the JSON docs read, from the docstrings.""" | ||
| for o in [obj, *(m for m in obj.members.values() if not m.is_alias)]: | ||
| if o.docstring: | ||
| text = ROLE.sub(r"`\1`", o.docstring.value) | ||
| head, _, tail = text.partition("\nCategory:\n") | ||
| text = head.rstrip() + "".join(f"\n{t}" for t in tail.split("\n")[1:]) | ||
| o.docstring.value = re.sub(r"(?<!`)``([^`]+)``(?!`)", r"`\1`", text) | ||
|
|
||
|
|
||
| def signature(name, function, *_, **__) -> str: | ||
| """griffe2md's signature template gives an unannotated parameter the previous one's | ||
| annotation and drops `async`, so griffe writes the signature instead. Private | ||
| parameters, which start with `_`, are left out.""" | ||
| prefix = "async " if "async" in function.labels else "" | ||
| public = copy.copy(function) | ||
| public.parameters = griffe.Parameters( | ||
| *(p for p in function.parameters if not p.name.startswith("_")) | ||
| ) | ||
| # A constructor is called by its class's name. | ||
| name = function.parent.name if function.name == "__init__" else str(name).strip() | ||
| return prefix + public.signature(name=name) | ||
|
|
||
|
|
||
| def whole_summaries(build): | ||
| """griffe2md's summary tables keep the first line of each docstring, which can stop | ||
| mid-sentence, so they keep the whole first paragraph instead.""" | ||
|
|
||
| def section(objects, **kwargs): | ||
| out = build(objects, **kwargs) | ||
| docstrings = {o.name: o.docstring for o in objects} | ||
| for item in out.value: | ||
| if docstring := docstrings[item.name]: | ||
| item.description = " ".join(docstring.value.split("\n\n", 1)[0].split()) | ||
| return out | ||
|
|
||
| return section | ||
|
|
||
|
|
||
| ENV = griffe2md.prepare_env() | ||
| ENV.filters["format_signature"] = signature | ||
| for kind in ("functions", "classes", "attributes"): | ||
| ENV.filters[f"as_{kind}_section"] = whole_summaries( | ||
| ENV.filters[f"as_{kind}_section"] | ||
| ) | ||
|
|
||
|
|
||
| def entry(obj, name: str) -> str: | ||
| template = ENV.get_template(f"{obj.kind.value}.md.jinja") | ||
| md = template.render(**griffe2md.prepare_context(obj, CONFIG)) | ||
| # Annotation links point at anchors this page doesn't have. | ||
| md = re.sub(r"\[([^\]]+)\]\(#[\w.]+\)", r"\1", md) | ||
| # The site reads Markdown as MDX 1: HTML must be JSX, and braces in <code> are expressions. | ||
| md = re.sub(r"<code>(.*?)</code>", r"`\1`", md.replace("<br>", "<br />")) | ||
| # A parameter documented only in the docstring, like a **kwargs key, isn't required. | ||
| md = re.sub(r"^(`\w+` \| \| .*) \| \*required\*$", r"\1 | ", md, flags=re.M) | ||
| return re.sub(r"^### .*$", f"### `{name}`", md.strip(), count=1, flags=re.M) | ||
|
|
||
|
|
||
| def methods(cls) -> list: | ||
| """Public methods and properties, and `__init__`.""" | ||
| return [ | ||
| m | ||
| for n, m in cls.members.items() | ||
| if not m.is_alias | ||
| and (m.is_function or "property" in m.labels) | ||
| and (not n.startswith("_") or n == "__init__") | ||
| ] | ||
|
|
||
|
|
||
| def label(prefix: str, m) -> str: | ||
| return f"{prefix}(...)" if m.name == "__init__" else f"{prefix}.{m.name}" | ||
|
|
||
|
|
||
| def capabilities(checkout: str) -> dict[str, str]: | ||
| """Each sdk-specs capability's slug and title in sentence case, in the README's order.""" | ||
| readme = (Path(checkout) / "README.md").read_text() | ||
| rows = re.findall( | ||
| r"^\| ([^|]+?) \| \[`openspec/specs/([a-z0-9-]+)/spec\.md`\]", readme, re.M | ||
| ) | ||
| acronyms = {"id": "ID", "ai": "AI", "http": "HTTP", "mcp": "MCP"} | ||
| titles = { | ||
| s: " ".join(acronyms.get(w.lower(), w.lower()) for w in n.split()) | ||
| for n, s in rows | ||
| } | ||
| # Setup, capture and identify come first; the rest follow the README. | ||
| order = [*FIRST, *(s for s in titles if s not in FIRST)] | ||
| return {s: titles[s][0].upper() + titles[s][1:] for s in order if s in titles} | ||
|
|
||
|
|
||
| def has(pkg, path: str) -> bool: | ||
| try: | ||
| pkg[path] | ||
| return True | ||
| except KeyError: | ||
| return False | ||
|
|
||
|
|
||
| def reference(pkg, caps: dict[str, str], current: bool) -> list[str]: | ||
| """`current` is false for a next major, which can remove what the map names.""" | ||
| order = {slug: i for i, slug in enumerate(caps)} | ||
| utils, types = ([pkg[p] for p in paths if has(pkg, p)] for paths in (UTILS, TYPES)) | ||
| module = [ | ||
| m | ||
| for n, m in pkg.members.items() | ||
| if not m.is_alias and m.is_function and not n.startswith("_") | ||
| ] | ||
| sync, asyncs = ( | ||
| methods(pkg["client.Client"]), | ||
| methods(pkg["async_client.AsyncClient"]), | ||
| ) | ||
| found = {label("Client", m) for m in sync} | { | ||
| label("AsyncClient", m) for m in asyncs | ||
| } | ||
| found |= {f"posthog.{m.name}" for m in module} | ||
| for obj in [*sync, *asyncs, *module, *utils, *types]: | ||
| clean(obj) | ||
| data = json.loads((ROOT / "docs/capabilities.json").read_text()) | ||
| utils_names, later = set(data.pop("utils")), data.pop("next", {}) | ||
| mapped = {**later, **data} | ||
| for name, slug in mapped.items(): | ||
| if current and name not in found and name not in later: | ||
| raise SystemExit(f"docs/capabilities.json: no symbol for {name}") | ||
| if slug not in caps: | ||
| raise SystemExit( | ||
| f"docs/capabilities.json: {name}: {slug} isn't in sdk-specs" | ||
| ) | ||
| for name in sorted(found - mapped.keys() - utils_names): | ||
| if name.startswith("Client"): | ||
| raise SystemExit(f"docs/capabilities.json: map {name} or add it to utils") | ||
| client = [(mapped.get(label("Client", m)), m) for m in sync] | ||
| asyncs = sorted( | ||
| ((mapped.get(label("AsyncClient", m)), m) for m in asyncs), | ||
| key=lambda e: order.get(e[0] or "", len(order)), | ||
| ) | ||
| out = [] | ||
| for slug, title in caps.items(): | ||
| if tagged := [m for t, m in client if t == slug]: | ||
| out += [f"## {title}", *(entry(m, label("Client", m)) for m in tagged)] | ||
| out += ["## SDK utils", *(entry(m, label("Client", m)) for t, m in client if not t)] | ||
| out += [entry(o, f"posthog.{o.path.removeprefix('posthog.')}") for o in utils] | ||
| out += ["## AsyncClient", "Needs the `async` extra, `posthog[async]`."] | ||
| out += [entry(m, label("AsyncClient", m)) for _, m in asyncs] | ||
| out += ["## Module functions", *(entry(m, f"posthog.{m.name}") for m in module)] | ||
| return out + ["## Types", *(entry(o, o.name) for o in types)] | ||
|
|
||
|
|
||
| def front_matter(title: str, sections: list[str]) -> str: | ||
| """The docs site's front matter. Its ToC lists the page's H2 sections only.""" | ||
| toc = [] | ||
| for heading in [s[3:] for s in sections if s.startswith("## ")]: | ||
| anchor = re.sub(r"[^\w\- ]", "", heading.lower()).replace(" ", "-") | ||
| toc.append(f' - value: "{heading}"\n url: {anchor}\n depth: 0') | ||
| head = [f'title: "{title}"', "sidebar: Docs", "showTitle: true"] | ||
| head += [ | ||
| "contentMaxWidthClass: max-w-full", | ||
| "platformLogo: python", | ||
| "tableOfContents:", | ||
| *toc, | ||
| ] | ||
| return "\n".join(["---", *head, "---"]) | ||
|
|
||
|
|
||
| def nest(markdown: str) -> str: | ||
| """Moves each heading down one level and drops the H1. Code stays as written.""" | ||
| out, in_fence = [], False | ||
| for line in markdown.strip().split("\n"): | ||
| in_fence ^= bool(re.match(r"\s*```", line)) | ||
| if in_fence or not re.match(r"#{1,5} ", line): | ||
| out.append(line) | ||
| elif not line.startswith("# "): | ||
| out.append("#" + line) | ||
| return "\n".join(out) | ||
|
|
||
|
|
||
| def write(name: str, sections: list[str]) -> None: | ||
| (ROOT / "references" / name).write_text("\n\n".join(sections) + "\n") | ||
| print(f"wrote references/{name}") | ||
|
|
||
|
|
||
| def main() -> None: | ||
| args = argparse.ArgumentParser(description="Generate the SDK reference Markdown.") | ||
| args.add_argument("--sdk-specs", required=True, help="PostHog/sdk-specs checkout") | ||
| args.add_argument( | ||
| "--major", type=int, help="target major, defaults to the declared one" | ||
| ) | ||
| opts = args.parse_args() | ||
| logging.getLogger("griffe").setLevel(logging.ERROR) | ||
| pkg = griffe.load( | ||
| "posthog", | ||
| search_paths=[str(ROOT)], | ||
| docstring_parser="google", | ||
| # A Returns section is one return value, even when its description wraps. | ||
| docstring_options={"returns_multiple_items": False}, | ||
| resolve_aliases=False, | ||
| ) | ||
| declared = int( | ||
| (ROOT / "posthog/version.py").read_text().split('"')[1].split(".")[0] | ||
| ) | ||
| major = opts.major or declared | ||
| started = nest((ROOT / "docs/getting-started.md").read_text()) | ||
| body = [ | ||
| "## Getting started", | ||
| started, | ||
| *reference(pkg, capabilities(opts.sdk_specs), declared >= major), | ||
| ] | ||
| title = f"PostHog Python SDK {major}.x" + ( | ||
| "" if declared >= major else " (experimental)" | ||
| ) | ||
| write(f"posthog-python-{major}.md", [front_matter(title, body), *body]) | ||
| if declared >= major: | ||
| write( | ||
| "posthog-python-reference.md", | ||
| [front_matter("PostHog Python SDK", body), *body], | ||
| ) | ||
| if guides := sorted((ROOT / "docs").glob("migration-*.md"), reverse=True): | ||
| head = "---\ntitle: PostHog Python SDK migration guides\nsidebar: Docs\nshowTitle: true\n---" | ||
| write( | ||
| "posthog-python-migration.md", | ||
| [head, *(g.read_text().strip() for g in guides)], | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| { | ||
| "AsyncClient(...)": "setup", | ||
| "AsyncClient.alias": "alias", | ||
| "AsyncClient.capture": "capture", | ||
| "AsyncClient.capture_exception": "capture-exception", | ||
| "AsyncClient.capture_immediate": "capture", | ||
| "AsyncClient.emails": "emails", | ||
| "AsyncClient.evaluate_flags": "evaluate-flags", | ||
| "AsyncClient.flush": "flush", | ||
| "AsyncClient.group_identify": "group-identify", | ||
| "AsyncClient.join": "shutdown", | ||
| "AsyncClient.set": "identify", | ||
| "AsyncClient.set_once": "identify", | ||
| "AsyncClient.shutdown": "shutdown", | ||
| "Client(...)": "setup", | ||
| "Client.alias": "alias", | ||
| "Client.capture": "capture", | ||
| "Client.capture_ai": "capture-ai", | ||
| "Client.capture_exception": "capture-exception", | ||
| "Client.emails": "emails", | ||
| "Client.evaluate_flags": "evaluate-flags", | ||
| "Client.feature_enabled": "is-feature-enabled", | ||
| "Client.feature_flag_definitions": "flag-definition-loader", | ||
| "Client.flush": "flush", | ||
| "Client.get_active_span": "traces", | ||
| "Client.get_all_flags": "get-feature-flags", | ||
| "Client.get_all_flags_and_payloads": "get-feature-flags-and-payloads", | ||
| "Client.get_feature_flag": "get-feature-flag", | ||
| "Client.get_feature_flag_payload": "get-feature-flag-payload", | ||
| "Client.get_feature_flag_result": "get-feature-flag-result", | ||
| "Client.get_feature_flags_and_payloads": "get-feature-flags-and-payloads", | ||
| "Client.get_feature_variants": "get-feature-flags", | ||
| "Client.group_identify": "group-identify", | ||
| "Client.join": "shutdown", | ||
| "Client.load_feature_flags": "flag-definition-loader", | ||
| "Client.set": "identify", | ||
| "Client.set_once": "identify", | ||
| "Client.shutdown": "shutdown", | ||
| "Client.start_span": "traces", | ||
| "posthog.alias": "alias", | ||
| "posthog.capture": "capture", | ||
| "posthog.capture_ai": "capture-ai", | ||
| "posthog.capture_exception": "capture-exception", | ||
| "posthog.evaluate_flags": "evaluate-flags", | ||
| "posthog.feature_enabled": "is-feature-enabled", | ||
| "posthog.feature_flag_definitions": "flag-definition-loader", | ||
| "posthog.flush": "flush", | ||
| "posthog.get_active_span": "traces", | ||
| "posthog.get_all_flags": "get-feature-flags", | ||
| "posthog.get_all_flags_and_payloads": "get-feature-flags-and-payloads", | ||
| "posthog.get_feature_flag": "get-feature-flag", | ||
| "posthog.get_feature_flag_payload": "get-feature-flag-payload", | ||
| "posthog.get_feature_flag_result": "get-feature-flag-result", | ||
| "posthog.group_identify": "group-identify", | ||
| "posthog.join": "shutdown", | ||
| "posthog.load_feature_flags": "flag-definition-loader", | ||
| "posthog.set": "identify", | ||
| "posthog.set_once": "identify", | ||
| "posthog.setup": "setup", | ||
| "posthog.shutdown": "shutdown", | ||
| "posthog.start_span": "traces", | ||
| "utils": [ | ||
| "Client.consumers", | ||
| "Client.feature_flags", | ||
| "Client.get_context_options", | ||
| "Client.get_feature_flag_evaluation_runtime", | ||
| "Client.get_feature_flag_keys_by_evaluation_runtime", | ||
| "Client.get_feature_payloads", | ||
| "Client.get_flags_decision", | ||
| "Client.get_remote_config_payload", | ||
| "Client.get_tags", | ||
| "Client.identify_context", | ||
| "Client.metrics", | ||
| "Client.new_context", | ||
| "Client.queue", | ||
| "Client.scoped", | ||
| "Client.set_context_device_id", | ||
| "Client.set_context_option", | ||
| "Client.set_context_session", | ||
| "Client.tag" | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Getting started | ||
|
|
||
| The Python SDK captures events, evaluates feature flags and reports errors from Python 3.10 or later. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| pip install posthog | ||
| ``` | ||
|
|
||
| ## Initialize | ||
|
|
||
| Create one client for your app with your project token and host. Your project token and host are in your [project settings](https://app.posthog.com/project/settings). Keep the token in an environment variable rather than in code. | ||
|
|
||
| ```python | ||
| from posthog import Posthog | ||
|
|
||
| posthog = Posthog("<ph_project_token>", host="<ph_client_api_host>") | ||
| ``` | ||
|
|
||
| ## Capture an event | ||
|
|
||
| ```python | ||
| posthog.capture("user_signed_up", distinct_id="user_123", properties={"plan": "free"}) | ||
| ``` | ||
|
|
||
| `capture` queues the event and returns right away. A background thread sends it. | ||
|
|
||
| ## Shut down | ||
|
|
||
| Call `shutdown()` before your process exits, so queued events are sent. | ||
|
|
||
| ```python | ||
| posthog.shutdown() | ||
| ``` | ||
|
|
||
| In an asyncio app, use `AsyncPosthog` as an async context manager instead. See [AsyncClient](#asyncclient). |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.