Skip to content

Commit

Permalink
Use importlib for __all__
Browse files Browse the repository at this point in the history
  • Loading branch information
daizutabi committed Feb 11, 2024
1 parent 5859fdb commit dbe3415
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 41 deletions.
37 changes: 16 additions & 21 deletions docs/usage/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ There are three ways to collect modules:

## Example API documentations

To demonstrate the Page mode, this MkAPI documentation ships with
some references:
To demonstrate the Page mode, try some libraries.
For example, MkAPI is tested using following libraries:

- [Schemdraw](https://schemdraw.readthedocs.io/en/stable/)
- "Schemdraw is a Python package for producing high-quality
Expand All @@ -182,7 +182,19 @@ some references:
- [Altair](https://altair-viz.github.io/)
- "Vega-Altair is a declarative visualization library for Python."

Click section tabs at the top bar or buttons below to see the API documentation.
Use the following `nav` section in your `mkdocs.yml`
if you want to check the output of MkAPI.

```yaml title="mkdocs.yml"
nav:
- index.md
- API: $api/mkapi.** # API documentation of MkAPI itself
- Schemdraw: $api/schemdraw.***
- Polars: $api/polars.***
- Altair: $api/altair.***
```

<!-- Click section tabs at the top bar or buttons below to see the API documentation.

<style type="text/css">
.mkapi-center {
Expand All @@ -195,21 +207,4 @@ Click section tabs at the top bar or buttons below to see the API documentation.
[Schemdraw][schemdraw]{.md-button .md-button--primary}
[Polars][polars]{.md-button .md-button--primary}
[Altair][altair]{.md-button .md-button--primary}
</div>

Here is the actual `nav` section in `mkdocs.yml` of this documentation.
Use this to reproduce the similar navigation structure for your project if you like.

```yaml
nav:
- index.md
- Usage:
- usage/object.md
- usage/page.md
- usage/config.md
- API: $api/mkapi.** # API documentation of MkAPI itself
- Examples: $api/examples.** # for Object mode description
- Schemdraw: $api/schemdraw.*** # for Page mode demonstration
- Polars: $api/polars.*** # for Page mode demonstration
- Altair: $api/altair.*** # for Page mode demonstration
```
</div> -->
6 changes: 3 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ nav:
- 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
watch:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ packages = ["src/mkapi"]
python = ["3.12"]

[tool.hatch.envs.default]
dependencies = ["polars", "pytest-cov", "schemdraw"]
dependencies = ["altair", "polars", "pytest-cov", "schemdraw"]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests src/mkapi}"

Expand Down
46 changes: 44 additions & 2 deletions src/mkapi/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from __future__ import annotations

import ast
import importlib
import inspect
from dataclasses import dataclass
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -118,37 +120,77 @@ def resolve(name: str) -> str | None:
"""Resolve name."""
if get_module_path(name) or "." not in name:
return name

module, _ = name.rsplit(".", maxsplit=1)
if name in _iter_objects(module):
return name

if import_ := get_by_name(_iter_imports(module), name):
if name == import_.fullname:
return None
return resolve(import_.fullname)

return None


def resolve_with_attribute(name: str) -> str | None:
"""Resolve name with attribute."""
if fullname := resolve(name):
return fullname

if "." in name:
name_, attr = name.rsplit(".", maxsplit=1)
if fullname := resolve(name_):
return f"{fullname}.{attr}"

return None


def get_all(module: str) -> dict[str, str]:
"""Return name dictonary of __all__."""
def get_all_from_ast(module: str) -> dict[str, str]:
"""Return name dictonary of __all__ using ast."""
names = {}
n = len(module) + 1

for name in _iter_objects_from_all(module):
if fullname := resolve(name):
names[name[n:]] = fullname

return names


def get_all_from_importlib(module: str) -> dict[str, str]:
"""Return name dictonary of __all__ using importlib."""
try:
module_type = importlib.import_module(module)
except ModuleNotFoundError:
return {}

Check warning on line 166 in src/mkapi/globals.py

View check run for this annotation

Codecov / codecov/patch

src/mkapi/globals.py#L165-L166

Added lines #L165 - L166 were not covered by tests

members = getattr(module_type, "__dict__", {}) # Must use __dict__.
if not isinstance(members, dict) or "__all__" not in members:
return {}

names = {}
for name in members["__all__"]:
if not (obj := members.get(name)):
continue

Check warning on line 175 in src/mkapi/globals.py

View check run for this annotation

Codecov / codecov/patch

src/mkapi/globals.py#L175

Added line #L175 was not covered by tests
if inspect.ismodule(obj):
names[name] = obj.__name__
elif not (modulename := getattr(obj, "__module__", None)):
continue
elif qualname := getattr(obj, "__qualname__", None):
names[name] = f"{modulename}.{qualname}"

return names


def get_all(module: str) -> dict[str, str]:
"""Return name dictonary of __all__."""
all_from_ast = get_all_from_ast(module)
all_from_importlib = get_all_from_importlib(module)
all_from_ast.update(all_from_importlib)
return all_from_ast


@dataclass(repr=False)
class Globals:
"""Globals class."""
Expand Down
21 changes: 11 additions & 10 deletions src/mkapi/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import mkapi.markdown
import mkapi.renderers
from mkapi.globals import resolve_with_attribute
from mkapi.importlib import get_object
from mkapi.importlib import get_object, load_module
from mkapi.objects import Module, is_empty, iter_objects_with_depth
from mkapi.renderers import get_object_filter_for_source
from mkapi.utils import is_module_cache_dirty, split_filters
Expand Down Expand Up @@ -80,7 +80,8 @@ def create_markdown(
is_source: bool = False,
) -> str:
"""Create object page for an object."""
if not (obj := get_object(name)) or not isinstance(obj, Module):
# if not (obj := get_object(name)) or not isinstance(obj, Module):
if not (module := load_module(name)):
return f"!!! failure\n\n module {name!r} not found.\n"

filters_str = "|" + "|".join(filters) if filters else ""
Expand All @@ -89,19 +90,19 @@ def create_markdown(
paths = source_paths if is_source else object_paths

markdowns = []
for child, depth in iter_objects_with_depth(obj, 2, member_only=True):
if is_empty(child):
for obj, depth in iter_objects_with_depth(module, 2, member_only=True):
if is_empty(obj):
continue
if predicate and not predicate(child.fullname):
if predicate and not predicate(obj.fullname):
continue
paths.setdefault(child.fullname, path)
paths.setdefault(obj.fullname, path)

if is_source:
object_filter = get_object_filter_for_source(child, obj)
object_filter = get_object_filter_for_source(obj, module)
object_filter = f"|{object_filter}" if object_filter else ""

heading = "#" * (depth + 1)
markdown = f"{heading} ::: {child.fullname}{filters_str}{object_filter}\n"
markdown = f"{heading} ::: {obj.fullname}{filters_str}{object_filter}\n"
markdowns.append(markdown)

return "\n".join(markdowns)
Expand Down Expand Up @@ -154,8 +155,8 @@ def _replace_source(markdown: str) -> str:

for match in re.finditer(OBJECT_PATTERN, markdown):
name, level, object_filter = _get_level_name_filters(match)
if level == 1 and (obj := get_object(name)) and isinstance(obj, Module):
module = obj
if level == 1 and not (module := load_module(name)):
break

Check warning on line 159 in src/mkapi/pages.py

View check run for this annotation

Codecov / codecov/patch

src/mkapi/pages.py#L159

Added line #L159 was not covered by tests

# Move to renderer.py
if level >= 2:
Expand Down
6 changes: 6 additions & 0 deletions tests/test_globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
_iter_imports_from_import_from,
_iter_objects_from_all,
get_all,
get_all_from_ast,
get_all_from_importlib,
get_fullname,
get_globals,
get_link_from_type,
Expand Down Expand Up @@ -193,3 +195,7 @@ def test_get_all():
x = get_all("polars")
assert x["api"] == "polars.api"
assert x["ArrowError"] == "polars.exceptions.ArrowError"


def test_get_all_from_importlib():
assert get_all_from_importlib("altair")
7 changes: 3 additions & 4 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ def test_nav(mkdocs_config: MkDocsConfig):
nav_dict.update(item)
assert nav_dict["API"] == "$api/mkapi.**"
assert nav_dict["Examples"] == "$api/examples.**"
assert nav_dict["Schemdraw"] == "$api/schemdraw.***"
assert nav_dict["Polars"] == "$api/polars.***"
assert nav_dict["Altair"] == "$api/altair.***"
# assert nav_dict["Schemdraw"] == "$api/schemdraw.***"
# assert nav_dict["Polars"] == "$api/polars.***"
# assert nav_dict["Altair"] == "$api/altair.***"


@pytest.fixture(scope="module")
Expand All @@ -80,7 +80,6 @@ def mkapi_config(mkapi_plugin: MkAPIPlugin):
def test_mkapi_config(mkapi_config: MkAPIConfig):
config = mkapi_config
assert config.config == "config.py"
assert config.exclude == ["altair.vegalite"]
assert config.debug is True


Expand Down

0 comments on commit dbe3415

Please sign in to comment.