Skip to content

Commit

Permalink
refactor: Move "about" formatting logic into dedicated classes (#1570)
Browse files Browse the repository at this point in the history
* refactor: Move _about_ formatting logic into dedicated classes

* Test with snapshots

* Rename snapshots

* Move `singer_sdk/internal/about.py -> singer_sdk/about.py`
  • Loading branch information
edgarrmondragon authored Apr 10, 2023
1 parent f544b40 commit 6dc1c49
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ repos:
cookiecutter/.*|
docs/.*|
samples/.*\.json|
tests/snapshots/.*/.*\.json
tests/snapshots/.*
)$
- id: trailing-whitespace
exclude: |
Expand Down
192 changes: 192 additions & 0 deletions singer_sdk/about.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""About information for a plugin."""

from __future__ import annotations

import abc
import dataclasses
import json
import typing as t
from collections import OrderedDict
from textwrap import dedent

if t.TYPE_CHECKING:
from singer_sdk.helpers.capabilities import CapabilitiesEnum

__all__ = [
"AboutInfo",
"AboutFormatter",
"JSONFormatter",
"MarkdownFormatter",
]


@dataclasses.dataclass
class AboutInfo:
"""About information for a plugin."""

name: str
description: str | None
version: str
sdk_version: str

capabilities: list[CapabilitiesEnum]
settings: dict


class AboutFormatter(abc.ABC):
"""Abstract base class for about formatters."""

formats: t.ClassVar[dict[str, type[AboutFormatter]]] = {}
format_name: str

def __init_subclass__(cls, format_name: str) -> None:
"""Initialize subclass.
Args:
format_name: Name of the format.
"""
cls.formats[format_name] = cls
super().__init_subclass__()

@classmethod
def get_formatter(cls, name: str) -> AboutFormatter:
"""Get a formatter by name.
Args:
name: Name of the formatter.
Returns:
A formatter.
"""
return cls.formats[name]()

@abc.abstractmethod
def format_about(self, about_info: AboutInfo) -> str:
"""Render about information.
Args:
about_info: About information.
"""
...


class TextFormatter(AboutFormatter, format_name="text"):
"""About formatter for text output."""

def format_about(self, about_info: AboutInfo) -> str:
"""Render about information.
Args:
about_info: About information.
Returns:
A formatted string.
"""
return dedent(
f"""\
Name: {about_info.name}
Description: {about_info.description}
Version: {about_info.version}
SDK Version: {about_info.sdk_version}
Capabilities: {about_info.capabilities}
Settings: {about_info.settings}""",
)


class JSONFormatter(AboutFormatter, format_name="json"):
"""About formatter for JSON output."""

def __init__(self) -> None:
"""Initialize a JSONAboutFormatter."""
self.indent = 2
self.default = str

def format_about(self, about_info: AboutInfo) -> str:
"""Render about information.
Args:
about_info: About information.
Returns:
A formatted string.
"""
data = OrderedDict(
[
("name", about_info.name),
("description", about_info.description),
("version", about_info.version),
("sdk_version", about_info.sdk_version),
("capabilities", [c.value for c in about_info.capabilities]),
("settings", about_info.settings),
],
)
return json.dumps(data, indent=self.indent, default=self.default)


class MarkdownFormatter(AboutFormatter, format_name="markdown"):
"""About formatter for Markdown output."""

def format_about(self, about_info: AboutInfo) -> str:
"""Render about information.
Args:
about_info: About information.
Returns:
A formatted string.
"""
max_setting_len = t.cast(
int,
max(len(k) for k in about_info.settings["properties"]),
)

# Set table base for markdown
table_base = (
f"| {'Setting':{max_setting_len}}| Required | Default | Description |\n"
f"|:{'-' * max_setting_len}|:--------:|:-------:|:------------|\n"
)

# Empty list for string parts
md_list = []
# Get required settings for table
required_settings = about_info.settings.get("required", [])

# Iterate over Dict to set md
md_list.append(
f"# `{about_info.name}`\n\n"
f"{about_info.description}\n\n"
f"Built with the [Meltano Singer SDK](https://sdk.meltano.com).\n\n",
)

# Process capabilities and settings

capabilities = "## Capabilities\n\n"
capabilities += "\n".join([f"* `{v}`" for v in about_info.capabilities])
capabilities += "\n\n"
md_list.append(capabilities)

setting = "## Settings\n\n"

for k, v in about_info.settings.get("properties", {}).items():
md_description = v.get("description", "").replace("\n", "<BR/>")
table_base += (
f"| {k}{' ' * (max_setting_len - len(k))}"
f"| {'True' if k in required_settings else 'False':8} | "
f"{v.get('default', 'None'):7} | "
f"{md_description:11} |\n"
)

setting += table_base
setting += (
"\n"
+ "\n".join(
[
"A full list of supported settings and capabilities "
f"is available by running: `{about_info.name} --about`",
],
)
+ "\n"
)
md_list.append(setting)

return "".join(md_list)
1 change: 1 addition & 0 deletions singer_sdk/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Internal utilities for the Singer SDK."""
131 changes: 48 additions & 83 deletions singer_sdk/plugin_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@
from __future__ import annotations

import abc
import json
import logging
import os
from collections import OrderedDict
from pathlib import PurePath
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Callable, Mapping, cast

import click
from jsonschema import Draft7Validator

from singer_sdk import metrics
from singer_sdk import about, metrics
from singer_sdk.configuration._dict_config import parse_environment_config
from singer_sdk.exceptions import ConfigValidationError
from singer_sdk.helpers._classproperty import classproperty
Expand Down Expand Up @@ -149,31 +147,57 @@ def _env_var_config(cls) -> dict[str, Any]: # noqa: N805

# Core plugin metadata:

@classproperty
def plugin_version(cls) -> str: # noqa: N805
"""Get version.
@staticmethod
def _get_package_version(package: str) -> str:
"""Return the package version number.
Args:
package: The package name.
Returns:
The package version number.
"""
try:
version = metadata.version(cls.name)
version = metadata.version(package)
except metadata.PackageNotFoundError:
version = "[could not be detected]"
return version

@classmethod
def get_plugin_version(cls) -> str:
"""Return the package version number.
Returns:
The package version number.
"""
return cls._get_package_version(cls.name)

@classmethod
def get_sdk_version(cls) -> str:
"""Return the package version number.
Returns:
The package version number.
"""
return cls._get_package_version(SDK_PACKAGE_NAME)

@classproperty
def plugin_version(cls) -> str: # noqa: N805
"""Get version.
Returns:
The package version number.
"""
return cls.get_plugin_version()

@classproperty
def sdk_version(cls) -> str: # noqa: N805
"""Return the package version number.
Returns:
Meltano Singer SDK version number.
"""
try:
version = metadata.version(SDK_PACKAGE_NAME)
except metadata.PackageNotFoundError:
version = "[could not be detected]"
return version
return cls.get_sdk_version()

# Abstract methods:

Expand Down Expand Up @@ -278,23 +302,23 @@ def print_version(
print_fn(f"{cls.name} v{cls.plugin_version}, Meltano SDK v{cls.sdk_version}")

@classmethod
def _get_about_info(cls: type[PluginBase]) -> dict[str, Any]:
def _get_about_info(cls: type[PluginBase]) -> about.AboutInfo:
"""Returns capabilities and other tap metadata.
Returns:
A dictionary containing the relevant 'about' information.
"""
info: dict[str, Any] = OrderedDict({})
info["name"] = cls.name
info["description"] = cls.__doc__
info["version"] = cls.plugin_version
info["sdk_version"] = cls.sdk_version
info["capabilities"] = cls.capabilities

config_jsonschema = cls.config_jsonschema
cls.append_builtin_config(config_jsonschema)
info["settings"] = config_jsonschema
return info

return about.AboutInfo(
name=cls.name,
description=cls.__doc__,
version=cls.get_plugin_version(),
sdk_version=cls.get_sdk_version(),
capabilities=cls.capabilities,
settings=config_jsonschema,
)

@classmethod
def append_builtin_config(cls: type[PluginBase], config_jsonschema: dict) -> None:
Expand Down Expand Up @@ -337,67 +361,8 @@ def print_about(
output_format: Render option for the plugin information.
"""
info = cls._get_about_info()

if output_format == "json":
print(json.dumps(info, indent=2, default=str)) # noqa: T201

elif output_format == "markdown":
max_setting_len = cast(
int,
max(len(k) for k in info["settings"]["properties"]),
)

# Set table base for markdown
table_base = (
f"| {'Setting':{max_setting_len}}| Required | Default | Description |\n"
f"|:{'-' * max_setting_len}|:--------:|:-------:|:------------|\n"
)

# Empty list for string parts
md_list = []
# Get required settings for table
required_settings = info["settings"].get("required", [])

# Iterate over Dict to set md
md_list.append(
f"# `{info['name']}`\n\n"
f"{info['description']}\n\n"
f"Built with the [Meltano Singer SDK](https://sdk.meltano.com).\n\n",
)
for key, value in info.items():
if key == "capabilities":
capabilities = f"## {key.title()}\n\n"
capabilities += "\n".join([f"* `{v}`" for v in value])
capabilities += "\n\n"
md_list.append(capabilities)

if key == "settings":
setting = f"## {key.title()}\n\n"
for k, v in info["settings"].get("properties", {}).items():
md_description = v.get("description", "").replace("\n", "<BR/>")
table_base += (
f"| {k}{' ' * (max_setting_len - len(k))}"
f"| {'True' if k in required_settings else 'False':8} | "
f"{v.get('default', 'None'):7} | "
f"{md_description:11} |\n"
)
setting += table_base
setting += (
"\n"
+ "\n".join(
[
"A full list of supported settings and capabilities "
f"is available by running: `{info['name']} --about`",
],
)
+ "\n"
)
md_list.append(setting)

print("".join(md_list)) # noqa: T201
else:
formatted = "\n".join([f"{k.title()}: {v}" for k, v in info.items()])
print(formatted) # noqa: T201
formatter = about.AboutFormatter.get_formatter(output_format or "text")
print(formatter.format_about(info)) # noqa: T201

@classproperty
def cli(cls) -> Callable: # noqa: N805
Expand Down
Loading

0 comments on commit 6dc1c49

Please sign in to comment.