Skip to content

Commit

Permalink
jinja2
Browse files Browse the repository at this point in the history
  • Loading branch information
daizutabi committed Feb 8, 2024
1 parent 3403941 commit 0db28ee
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 129 deletions.
7 changes: 4 additions & 3 deletions docs/usage/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ the unaliased fullname by hovering mouse cursor on the names.

Python module has classes, functions, or attributes as its members.
A Module documentation can be a docstring of module itself written by the
author and members list automatically generated by MkAPI.
author.
MkAPIT adds members list automatically.

```markdown
::: examples.styles.google
Expand Down Expand Up @@ -191,10 +192,10 @@ Let's see the output.
A `[source]` tag is added in the right side of the heading .
You can click it to see the code.

A function or attribute can also be embeded in Markdown source
Functions or attributes can also be embeded in Markdown source
in the same way as described above.

There are another useful feature.
There is another useful feature.
The heading of object documentation contains the fullname of an object.
This fullname has hierarchical links to parent objects.
In the below examples, the fullname is:
Expand Down
7 changes: 4 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ nav:
- usage/object.md
- usage/page.md
- usage/config.md
- API: <api>/mkapi.***
- Examples: <api>/examples.**
- Schemdraw: <api>/schemdraw.***
- Polars: <api>/polars.***
- Altair: <api>/altair.***
# - Schemdraw: <api>/schemdraw.***
# - Polars: <api>/polars.***
# - Altair: <api>/altair.***
extra_css:
- stylesheets/extra.css
32 changes: 6 additions & 26 deletions src/mkapi/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@

def iter_decorator_names(obj: Class | Function) -> Iterator[str]:
"""Yield decorator_names."""
if not obj.module:
return
for deco in obj.node.decorator_list:
deco_name = next(mkapi.ast.iter_identifiers(deco))
if name := get_fullname(obj.module.name, deco_name):
Expand All @@ -33,8 +31,6 @@ def iter_decorator_names(obj: Class | Function) -> Iterator[str]:

def get_decorator(obj: Class | Function, name: str) -> ast.expr | None:
"""Return a decorator expr by name."""
if not obj.module:
return None
for deco in obj.node.decorator_list:
deco_name = next(mkapi.ast.iter_identifiers(deco))
if get_fullname(obj.module.name, deco_name) == name:
Expand Down Expand Up @@ -93,7 +89,7 @@ def iter_dataclass_parameters(cls: Class) -> Iterator[Parameter]:
class Part:
"""Signature part."""

text: str
markdown: str
kind: str


Expand All @@ -106,27 +102,18 @@ class Signature:
def __iter__(self) -> Iterator[Part]:
return iter(self.parts)

def __repr__(self) -> str:
return self.markdown

@property
def markdown(self) -> str:
"""Return Markdown of signature."""
markdowns = [f'<span class="{p.kind}">{p.text}</span>' for p in self]
return "".join(markdowns)


def _iter_sep(kind: str | None, prev_kind: str | None) -> Iterator[tuple[str, str]]:
if prev_kind == "posonlyargs" and kind != prev_kind:
yield "/", "sep"
yield "/", "slash"
yield ", ", "comma"
if kind == "kwonlyargs" and prev_kind not in [kind, "vararg"]:
yield "\\*", "sep"
yield r"\*", "star"
yield ", ", "comma"
if kind == "vararg":
yield "\\*", "star"
yield r"\*", "star"
if kind == "kwarg":
yield "\\*\\*", "star"
yield r"\*\*", "star"


def _iter_param(param: Parameter) -> Iterator[tuple[str, str]]:
Expand Down Expand Up @@ -156,7 +143,7 @@ def iter_signature(obj: Class | Function) -> Iterator[tuple[str, str]]:
prev_kind = kind
if kind == "posonlyargs":
yield ", ", "comma"
yield "/", "sep"
yield "/", "slash"
yield ")", "paren"
if not hasattr(obj, "returns") or not obj.returns: # type: ignore
return
Expand All @@ -168,10 +155,3 @@ def get_signature(obj: Class | Function) -> Signature:
"""Return signature."""
parts = [Part(*args) for args in iter_signature(obj)]
return Signature(parts)


# Parameter.POSITIONAL_ONLY: "posonlyargs",
# Parameter.POSITIONAL_OR_KEYWORD: "args",
# Parameter.VAR_POSITIONAL: "vararg",
# Parameter.KEYWORD_ONLY: "kwonlyargs",
# Parameter.VAR_KEYWORD: "kwarg",
8 changes: 4 additions & 4 deletions src/mkapi/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,11 @@ def create_returns(name: str, text: str, style: str) -> Returns:
return Returns(name, Type(), Text(), returns)


@dataclass(repr=False)
class Bases(Section):
"""Bases section."""
# @dataclass(repr=False)
# class Bases(Section):
# """Bases section."""

items: list[Base]
# items: list[Base]


@dataclass(repr=False)
Expand Down
20 changes: 10 additions & 10 deletions src/mkapi/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from mkapi.items import (
Assign,
Assigns,
Bases,
# Bases,
Default,
Parameters,
Raises,
Expand Down Expand Up @@ -179,7 +179,7 @@ class Class(Callable):

def __iter__(self) -> Iterator[Type | Text]:
"""Yield [Type] or [Text] instances."""
for item in itertools.chain(self.attributes, self.parameters):
for item in itertools.chain(self.bases, self.attributes, self.parameters):
yield from item


Expand Down Expand Up @@ -265,8 +265,8 @@ def merge_items(module: Module) -> None:
if isinstance(obj, Module | Class):
_add_doc_comments(obj.attributes, module.source)
merge_attributes(obj)
if isinstance(obj, Class):
merge_bases(obj)
# if isinstance(obj, Class):
# merge_bases(obj)


def _add_doc_comments(attrs: list[Attribute], source: str | None = None) -> None:
Expand Down Expand Up @@ -345,12 +345,12 @@ def merge_attributes(obj: Module | Class) -> None:
obj.attributes = attributes


def merge_bases(obj: Class) -> None:
"""Merge bases."""
if not obj.bases:
return
section = Bases("Bases", Type(), Text(), obj.bases)
obj.doc.sections.insert(0, section)
# def merge_bases(obj: Class) -> None:
# """Merge bases."""
# if not obj.bases:
# return
# section = Bases("Bases", Type(), Text(), obj.bases)
# obj.doc.sections.insert(0, section)


def set_markdown(module: Module) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/mkapi/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _render_object(
context: dict[str, Any],
) -> str:
if isinstance(obj, Class | Function):
context["signature"] = get_signature(obj).markdown
context["signature"] = get_signature(obj)
return templates["object"].render(context)


Expand Down
54 changes: 29 additions & 25 deletions src/mkapi/templates/object.jinja2
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
<div class="mkapi-container" markdown="1">
{% if heading %}<{{ heading }} id="{{ obj.fullname }}" class="mkapi-heading" markdown="1">{% endif %}

<{{ heading }} class="mkapi-heading" id="{{ obj.fullname }}" markdown="1">
<span class="mkapi-heading-name">{{ fullname|safe }}</span>
{%- if "sourcelink" in filters %}
{% if "sourcelink" in filters -%}
<span class="mkapi-source-link">[source][__mkapi__.__source__.{{ obj.fullname }}]</span>
{%- endif -%}
{%- if heading %}</{{ heading }}>{% endif %}
{% endif %}</{{ heading }}>

<p class="mkapi-object" markdown="1">
<span class="mkapi-object-kind">{{ obj.kind }}</span>
<span class="mkapi-object-name">
{%- for name, kind in qualnames -%}
<span class="{{ kind }}">{{ name }}</span>
{%- if not loop.last %}<span class="dot">.</span>{% endif -%}
{%- endfor -%}
</span>
{%- if signature -%}
<span class="mkapi-signature">{{ signature|safe }}</span>
{%- endif -%}
{%- if obj.kind in ["attribute", "property"] %} :
<span class="mkapi-object-type">{{ obj.type.markdown|safe }}</span>
{%- endif -%}
</p>
<span class="mkapi-object-kind">{{ obj.kind }}</span>
<span class="mkapi-object-name">
{%- for name, kind in qualnames -%}
<span class="mkapi-{{ kind }}">{{ name }}</span>
{%- if not loop.last %}<span class="mkapi-dot">.</span>{% endif -%}
{%- endfor -%}
</span>

{%- if signature %}<span class="mkapi-signature">{% endif -%}
{%- for s in signature -%}
<span class="mkapi-{{ s.kind }}">{{ s.markdown|safe }}</span>
{%- endfor -%}
{%- if signature %}</span>{%- endif -%}

{%- if obj.kind in ["attribute", "property"] and obj.type.markdown %}
<span class="mkapi-colon">:</span>
<span class="mkapi-object-type">{{ obj.type.markdown|safe }}</span>
{%- endif -%}
{{ "\n" }}</p>

{% if obj.kind in ["class"] and obj.bases %}
<p class="mkapi-object-bases" markdown="1">
Bases :
{% for base in obj.bases %}<span class="mkapi-object-base">{{ base.type.markdown|safe }}<span>
{%- if not loop.last %}<span class="comma">, </span>{% endif -%}
{%- endfor -%}
</p>
{% if obj.kind in ["class", "dataclass"] and obj.bases -%}
<p class="mkapi-object-bases" markdown="1">Bases :
{% for base in obj.bases %}<span class="mkapi-object-base">{{ base.type.markdown|safe }}</span>
{%- if not loop.last %}<span class="mkapi-comma">, </span>{% else %}
</p>{% endif %}
{% endfor -%}
{% endif %}

{% if doc.text.markdown %}
Expand Down
102 changes: 55 additions & 47 deletions tests/test_inspect.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import ast

import pytest
import dataclasses

from mkapi.inspect import (
get_decorator,
get_signature,
is_classmethod,
is_dataclass,
is_staticmethod,
iter_decorator_names,
iter_signature,
)
from mkapi.objects import Class, Function, create_module
from mkapi.objects import Class, Function, Member, create_module
from mkapi.utils import get_by_name, get_module_node


def test_iter_decorator_names():
src = "@a(x)\n@b.c(y)\n@d\ndef f():\n pass"
node = ast.parse(src)
module = create_module("a", node)
f = module.functions[0]
assert list(iter_decorator_names(f)) == ["a", "b.c", "d"]


def test_get_decorator():
name = "mkapi.objects"
node = get_module_node(name)
Expand All @@ -24,6 +32,36 @@ def test_get_decorator():
assert isinstance(cls, Class)
assert get_decorator(cls, "dataclasses.dataclass")
assert is_dataclass(cls)
assert dataclasses.is_dataclass(Member)
src = "@a(x)\n@b.c(y)\n@d\ndef f():\n pass"
node = ast.parse(src)
module = create_module("a", node)
f = module.functions[0]
assert get_decorator(f, "d")
assert not get_decorator(f, "x")


def test_is_method():
src = "class A:\n @classmethod\n def f(cls): pass"
node = ast.parse(src)
module = create_module("a", node)
cls = module.classes[0]
assert isinstance(cls, Class)
assert is_classmethod(cls.functions[0])
src = "class A:\n @staticmethod\n def f(cls): pass"
node = ast.parse(src)
module = create_module("a", node)
cls = module.classes[0]
assert isinstance(cls, Class)
assert is_staticmethod(cls.functions[0])


# TODO
# def test_iter_dataclass_parameters():
# obj = get_object("mkapi.objects.Class")
# assert isinstance(obj, Class)
# print(obj.attributes)
# print(list(iter_dataclass_parameters(obj)))


def get(src: str) -> Function:
Expand Down Expand Up @@ -51,48 +89,18 @@ def sig(src: str) -> str:
def test_iter_signature_kind():
assert sig("x,y,z") == "(x,y,z)"
assert sig("x,/,y,z") == "(x,/,y,z)"
assert sig("x,/,*,y,z") == r"(x,/,\*,y,z)"
assert sig("x,/,y,*,z") == r"(x,/,y,\*,z)"
assert sig("x,/,*,y,z") == "(x,/,\\*,y,z)"
assert sig("x,/,y,*,z") == "(x,/,y,\\*,z)"
assert sig("x,y,z,/") == "(x,y,z,/)"
assert sig("*,x,y,z") == r"(\*,x,y,z)"
assert sig("*x,y,**z") == r"(\*x,y,\*\*z)"
assert sig("x,y,/,**z") == r"(x,y,/,\*\*z)"


def test_markdown():
name = "mkapi.objects"
node = get_module_node(name)
assert node
module = create_module(name, node)
assert module
func = get_by_name(module.functions, "create_module")
assert isinstance(func, Function)
sig = get_signature(func)
m = sig.markdown
assert '<span class="ann">[ast][__mkapi__.ast].Module</span>' in m


@pytest.fixture(scope="module")
def DataFrame() -> Class: # noqa: N802
node = get_module_node("polars.dataframe.frame")
assert node
module = create_module("polars.dataframe.frame", node)
assert module
cls = get_by_name(module.classes, "DataFrame")
assert isinstance(cls, Class)
return cls


def test_markdown_polars(DataFrame): # noqa: N803
func = get_by_name(DataFrame.functions, "to_pandas")
assert isinstance(func, Function)
sig = get_signature(func)
m = sig.markdown
assert r'<span class="sep">\*</span>' in m


def test_method(DataFrame): # noqa: N803
func = get_by_name(DataFrame.functions, "_from_arrow")
assert isinstance(func, Function)
assert "classmethod" in iter_decorator_names(func)
assert is_classmethod(func)
assert sig("*,x,y,z") == "(\\*,x,y,z)"
assert sig("*x,y,**z") == "(\\*x,y,\\*\\*z)"
assert sig("x,y,/,**z") == "(x,y,/,\\*\\*z)"


def test_get_signature():
obj = get("def f(x_:str='s',/,*y_,z_=1,**kwargs)->int: pass")
s = list(get_signature(obj))
assert s[0].kind == "paren"
assert s[0].markdown == "("
assert s[1].kind == "arg"
assert s[1].markdown == "x\\_"
Loading

0 comments on commit 0db28ee

Please sign in to comment.