From 22b7eaf5932d8edf77dc2135af16f53cbd4d2dd1 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Fri, 28 Apr 2023 08:20:41 -0500 Subject: [PATCH] clean up and refactor --- ....yml => .pydantic-typer-copier-answers.yml | 0 examples/models.py | 28 ++ examples/person.py | 13 + examples/person_cli.py | 30 +++ pydantic_typer/__init__.py | 60 +---- pydantic_typer/__main__.py | 9 - pydantic_typer/cli/__init__.py | 0 pydantic_typer/cli/app.py | 62 ----- pydantic_typer/cli/common.py | 6 - pydantic_typer/cli/config.py | 29 --- pydantic_typer/cli/tui.py | 18 -- pydantic_typer/config.py | 3 - pydantic_typer/standard_config.py | 239 ------------------ pydantic_typer/tui/app.css | 18 -- pydantic_typer/tui/app.py | 62 ----- pyproject.toml | 59 +++++ 16 files changed, 136 insertions(+), 500 deletions(-) rename .{{package_name}}-copier-answers.yml => .pydantic-typer-copier-answers.yml (100%) create mode 100644 examples/models.py create mode 100644 examples/person.py create mode 100644 examples/person_cli.py delete mode 100644 pydantic_typer/__main__.py delete mode 100644 pydantic_typer/cli/__init__.py delete mode 100644 pydantic_typer/cli/app.py delete mode 100644 pydantic_typer/cli/common.py delete mode 100644 pydantic_typer/cli/config.py delete mode 100644 pydantic_typer/cli/tui.py delete mode 100644 pydantic_typer/config.py delete mode 100644 pydantic_typer/standard_config.py delete mode 100644 pydantic_typer/tui/app.css delete mode 100644 pydantic_typer/tui/app.py diff --git a/.{{package_name}}-copier-answers.yml b/.pydantic-typer-copier-answers.yml similarity index 100% rename from .{{package_name}}-copier-answers.yml rename to .pydantic-typer-copier-answers.yml diff --git a/examples/models.py b/examples/models.py new file mode 100644 index 0000000..34b7d5d --- /dev/null +++ b/examples/models.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field +from typing import Optional + + +class Alpha(BaseModel): + a: int + + +class Color(BaseModel): + r: int + g: int + b: int + alpha: Alpha + + +class Hair(BaseModel): + color: Color + length: int + + +class Person(BaseModel): + name: str + other_name: Optional[str] = None + age: int + email: Optional[str] + pet: str = "dog" + address: str = Field("123 Main St", description="Where the person calls home.") + hair: Hair diff --git a/examples/person.py b/examples/person.py new file mode 100644 index 0000000..22401fa --- /dev/null +++ b/examples/person.py @@ -0,0 +1,13 @@ + +from examples.models import Person +from pydantic_typer import expand_pydantic_args + + +@expand_pydantic_args() +def get_person(person: Person, thing: str = None) -> Person: + """mydocstring""" + from rich import print + + print(str(thing)) + + print(person) diff --git a/examples/person_cli.py b/examples/person_cli.py new file mode 100644 index 0000000..77f8340 --- /dev/null +++ b/examples/person_cli.py @@ -0,0 +1,30 @@ +import typer + +from examples.models import Person +from pydantic_typer import expand_pydantic_args + +app = typer.Typer( + name="pydantic_typer", + help="a demo app", +) + + +@app.callback() +def main() -> None: + return + + +@app.command() +@expand_pydantic_args(typer=True) +def get_person(person: Person, thing: str, another: str = "this") -> Person: + """Get a person's information.""" + from rich import print + + print(thing) + print(another) + + print(person) + + +if __name__ == "__main__": + typer.run(get_person) diff --git a/pydantic_typer/__init__.py b/pydantic_typer/__init__.py index 8e4f310..cd5b9ce 100644 --- a/pydantic_typer/__init__.py +++ b/pydantic_typer/__init__.py @@ -1,44 +1,13 @@ -# SPDX-FileCopyrightText: 2023-present Waylon S. Walker -## -# SPDX-License-Identifier: MIT - from functools import wraps import inspect -from typing import Callable, Optional +from typing import Callable -from pydantic import BaseModel, Field import typer __all__ = ["typer"] -class Alpha(BaseModel): - a: int - - -class Color(BaseModel): - r: int - g: int - b: int - alpha: Alpha - - -class Hair(BaseModel): - color: Color - length: int - - -class Person(BaseModel): - name: str - other_name: Optional[str] = None - age: int - email: Optional[str] - pet: str = "dog" - address: str = Field("123 Main St", description="Where the person calls home.") - hair: Hair - - -def make_annotation(name, field, names, typer=False): +def _make_annotation(name, field, names, typer=False): panel_name = names.get(name) next_name = panel_name while next_name is not None: @@ -76,7 +45,7 @@ def make_annotation(name, field, names, typer=False): return f"{name}: {annotation}{default}" -def make_signature(func, wrapper, typer=False, more_args={}): +def _make_signature(func, wrapper, typer=False, more_args={}): sig = inspect.signature(func) names = {} for name, param in sig.parameters.items(): @@ -111,7 +80,7 @@ def make_signature(func, wrapper, typer=False, more_args={}): ) + f"\nalso accepts {more_args.keys()} in place of person model" # fields = Person.__fields__ raw_args = [ - make_annotation( + _make_annotation( name, field, names=names, @@ -153,7 +122,7 @@ def {func.__name__}({aargs}{', ' if aargs else ''}{kwargs}): sig = inspect.signature(new_func) for name, param in sig.parameters.items(): if hasattr(param.annotation, "__fields__"): - return make_signature(new_func, wrapper, typer=typer, more_args=more_args) + return _make_signature(new_func, wrapper, typer=typer, more_args=more_args) return new_func @@ -193,23 +162,6 @@ def expand_pydantic_args(typer: bool = False) -> Callable: def wrapper(*args, **kwargs): return func(**_expand_kwargs(func, kwargs)) - return make_signature(func, wrapper, typer=typer) + return _make_signature(func, wrapper, typer=typer) return decorator - - -def get_person_vanilla(person: Person) -> Person: - from rich import print - - print(person) - return person - - -@expand_pydantic_args() -def get_person(person: Person, thing: str = None) -> Person: - """mydocstring""" - from rich import print - - print(str(thing)) - - print(person) diff --git a/pydantic_typer/__main__.py b/pydantic_typer/__main__.py deleted file mode 100644 index 04b645e..0000000 --- a/pydantic_typer/__main__.py +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: 2023-present Waylon S. Walker -# -# SPDX-License-Identifier: MIT -import sys - -if __name__ == '__main__': - from .cli import {{python_package}} - - sys.exit({{python_package}}()) diff --git a/pydantic_typer/cli/__init__.py b/pydantic_typer/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pydantic_typer/cli/app.py b/pydantic_typer/cli/app.py deleted file mode 100644 index c629c96..0000000 --- a/pydantic_typer/cli/app.py +++ /dev/null @@ -1,62 +0,0 @@ -import typer - -from pydantic_typer import Person, expand_pydantic_args -from pydantic_typer.cli.common import verbose_callback -from pydantic_typer.cli.config import config_app -from pydantic_typer.cli.tui import tui_app - -app = typer.Typer( - name="pydantic_typer", - help="A rich terminal report for coveragepy.", -) -app.add_typer(config_app) -app.add_typer(tui_app) - - -def version_callback(value: bool) -> None: - """Callback function to print the version of the pydantic-typer package. - - Args: - value (bool): Boolean value to determine if the version should be printed. - - Raises: - typer.Exit: If the value is True, the version will be printed and the program will exit. - - Example: - version_callback(True) - """ - if value: - from pydantic_typer.__about__ import __version__ - - typer.echo(f"{__version__}") - raise typer.Exit() - - -@app.callback() -def main( - version: bool = typer.Option( - None, - "--version", - callback=version_callback, - is_eager=True, - ), - verbose: bool = typer.Option( - False, - callback=verbose_callback, - help="show the log messages", - ), -) -> None: - return - - -@app.command() -@expand_pydantic_args -def get_person(person: Person) -> Person: - """mydocstring""" - from rich import print - - print(person) - - -if __name__ == "__main__": - typer.run(main) diff --git a/pydantic_typer/cli/common.py b/pydantic_typer/cli/common.py deleted file mode 100644 index 2957684..0000000 --- a/pydantic_typer/cli/common.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydantic_typer.console import console - - -def verbose_callback(value: bool) -> None: - if value: - console.quiet = False diff --git a/pydantic_typer/cli/config.py b/pydantic_typer/cli/config.py deleted file mode 100644 index 6699fe7..0000000 --- a/pydantic_typer/cli/config.py +++ /dev/null @@ -1,29 +0,0 @@ -from rich.console import Console -import typer - -from pydantic_typer.cli.common import verbose_callback -from pydantic_typer.config import config as configuration - -config_app = typer.Typer() - - -@config_app.callback() -def config( - verbose: bool = typer.Option( - False, - callback=verbose_callback, - help="show the log messages", - ), -): - "configuration cli" - - -@config_app.command() -def show( - verbose: bool = typer.Option( - False, - callback=verbose_callback, - help="show the log messages", - ), -): - Console().print(configuration) diff --git a/pydantic_typer/cli/tui.py b/pydantic_typer/cli/tui.py deleted file mode 100644 index 0608e8a..0000000 --- a/pydantic_typer/cli/tui.py +++ /dev/null @@ -1,18 +0,0 @@ -import typer - -from pydantic_typer.cli.common import verbose_callback -from pydantic_typer.tui.app import run_app - -tui_app = typer.Typer() - - -@tui_app.callback(invoke_without_command=True) -def i( - verbose: bool = typer.Option( - False, - callback=verbose_callback, - help="show the log messages", - ), -): - "interactive tui" - run_app() diff --git a/pydantic_typer/config.py b/pydantic_typer/config.py deleted file mode 100644 index fbec538..0000000 --- a/pydantic_typer/config.py +++ /dev/null @@ -1,3 +0,0 @@ -from pydantic_typer.standard_config import load - -config = load("pydantic_typer") diff --git a/pydantic_typer/standard_config.py b/pydantic_typer/standard_config.py deleted file mode 100644 index 0f99499..0000000 --- a/pydantic_typer/standard_config.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Standard Config. -A module to load tooling config from a users project space. - -Inspired from frustrations that some tools have a tool.ini, .tool.ini, -setup.cfg, or pyproject.toml. Some allow for global configs, some don't. Some -properly follow the users home directory, others end up in a weird temp -directory. Windows home directory is only more confusing. Some will even -respect the users `$XDG_HOME` directory. - - -This file is for any project that can be configured in plain text such as `ini` -or `toml` and not requiring a .py file. Just name your tool and let users put -config where it makes sense to them, no need to figure out resolution order. - -## Usage: - -``` python -from standard_config import load - -# Retrieve any overrides from the user -overrides = {'setting': True} -config = load('my_tool', overrides) -``` - -## Resolution Order - -* First global file with a tool key -* First local file with a tool key -* Environment variables prefixed with `TOOL` -* Overrides - -### Tool Specific Ini files - -Ini file formats must include a `` key. - -``` ini -[my_tool] -setting = True -``` - -### pyproject.toml - -Toml files must include a `tool.` key - -``` toml -[tool.my_tool] -setting = True -``` - -### setup.cfg - -setup.cfg files must include a `tool:` key - -``` ini -[tool:my_tool] -setting = True -``` - - -### global files to consider - -* /tool.ini -* /.tool -* /.tool.ini -* /.config/tool.ini -* /.config/.tool -* /.config/.tool.ini - -### local files to consider - -* /tool.ini -* /.tool -* /.tool.ini -* /pyproject.toml -* /setup.cfg - -""" - -import os -from pathlib import Path -from typing import Dict, List, Union - -import anyconfig - -# path_spec_type = List[Dict[str, Union[Path, str, List[str\}\}\}\} -path_spec_type = List - - -def _get_global_path_specs(tool: str) -> path_spec_type: - """ - Generate a list of standard pathspecs for global config files. - - Args: - tool (str): name of the tool to configure - """ - try: - home = Path(os.environ["XDG_HOME"]) - except KeyError: - home = Path.home() - - return [ - {"path_specs": home / f"{tool}.ini", "ac_parser": "ini", "keys": [tool]}, - {"path_specs": home / f".{tool}", "ac_parser": "ini", "keys": [tool]}, - {"path_specs": home / f".{tool}.ini", "ac_parser": "ini", "keys": [tool]}, - { - "path_specs": home / ".config" / f"{tool}.ini", - "ac_parser": "ini", - "keys": [tool], - }, - { - "path_specs": home / ".config" / f".{tool}", - "ac_parser": "ini", - "keys": [tool], - }, - { - "path_specs": home / ".config" / f".{tool}.ini", - "ac_parser": "ini", - "keys": [tool], - }, - ] - - -def _get_local_path_specs(tool: str, project_home: Union[str, Path]) -> path_spec_type: - """ - Generate a list of standard pathspecs for local, project directory config files. - - Args: - tool (str): name of the tool to configure - """ - return [ - { - "path_specs": Path(project_home) / f"{tool}.ini", - "ac_parser": "ini", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / f".{tool}", - "ac_parser": "ini", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / f".{tool}.ini", - "ac_parser": "ini", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / f"{tool}.yml", - "ac_parser": "yaml", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / f".{tool}.yml", - "ac_parser": "yaml", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / f"{tool}.toml", - "ac_parser": "toml", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / f".{tool}.toml", - "ac_parser": "toml", - "keys": [tool], - }, - { - "path_specs": Path(project_home) / "pyproject.toml", - "ac_parser": "toml", - "keys": ["tool", tool], - }, - { - "path_specs": Path(project_home) / "setup.cfg", - "ac_parser": "ini", - "keys": [f"tool.{tool}"], - }, - ] - - -def _get_attrs(attrs: list, config: Dict) -> Dict: - """Get nested config data from a list of keys. - - specifically written for pyproject.toml which needs to get `tool` then `` - """ - for attr in attrs: - config = config[attr] - return config - - -def _load_files(config_path_specs: path_spec_type) -> Dict: - """Use anyconfig to load config files stopping at the first one that exists. - - config_path_specs (list): a list of pathspecs and keys to load - """ - for file in config_path_specs: - if file["path_specs"].exists(): - config = anyconfig.load(**file) - else: - # ignore missing files - continue - - try: - return _get_attrs(file["keys"], config) - except KeyError: - # ignore incorrect keys - continue - - return {} - - -def _load_env(tool: str) -> Dict: - """Load config from environment variables. - - Args: - tool (str): name of the tool to configure - """ - vars = [var for var in os.environ.keys() if var.startswith(tool.upper())] - return { - var.lower().strip(tool.lower()).strip("_").strip("-"): os.environ[var] - for var in vars - } - - -def load(tool: str, project_home: Union[Path, str] = ".", overrides: Dict = {}) -> Dict: - """Load tool config from standard config files. - - Resolution Order - - * First global file with a tool key - * First local file with a tool key - * Environment variables prefixed with `TOOL` - * Overrides - - Args: - tool (str): name of the tool to configure - """ - global_config = _load_files(_get_global_path_specs(tool)) - local_config = _load_files(_get_local_path_specs(tool, project_home)) - env_config = _load_env(tool) - return {**global_config, **local_config, **env_config, **overrides} diff --git a/pydantic_typer/tui/app.css b/pydantic_typer/tui/app.css deleted file mode 100644 index 7ed9fce..0000000 --- a/pydantic_typer/tui/app.css +++ /dev/null @@ -1,18 +0,0 @@ -Screen { - align: center middle; - layers: main footer; -} - -Sidebar { - height: 100vh; - width: auto; - min-width: 20; - background: $secondary-background-darken-2; - dock: left; - margin-right: 1; - layer: main; -} - -Footer { - layer: footer; -} diff --git a/pydantic_typer/tui/app.py b/pydantic_typer/tui/app.py deleted file mode 100644 index a213b27..0000000 --- a/pydantic_typer/tui/app.py +++ /dev/null @@ -1,62 +0,0 @@ -from pathlib import Path - -from textual.app import App, ComposeResult -from textual.containers import Container -from textual.css.query import NoMatches -from textual.widgets import Footer, Static - -from pydantic_typer.config import config - -config["tui"] = {} -config["tui"]["bindings"] = {} - - -class Sidebar(Static): - def compose(self) -> ComposeResult: - yield Container( - Static("sidebar"), - id="sidebar", - ) - - -class Tui(App): - """A Textual app to manage requests.""" - - CSS_PATH = Path("__file__").parent / "app.css" - BINDINGS = [tuple(b.values()) for b in config["tui"]["bindings"]] - - def compose(self) -> ComposeResult: - """Create child widgets for the app.""" - yield Container(Static("hello world")) - yield Footer() - - def action_toggle_dark(self) -> None: - """An action to toggle dark mode.""" - self.dark = not self.dark - - def action_toggle_sidebar(self): - try: - self.query_one("PromptSidebar").remove() - except NoMatches: - self.mount(Sidebar()) - - -def run_app(): - import os - import sys - - from textual.features import parse_features - - dev = "--dev" in sys.argv - features = set(parse_features(os.environ.get("TEXTUAL", ""))) - if dev: - features.add("debug") - features.add("devtools") - - os.environ["TEXTUAL"] = ",".join(sorted(features)) - app = Tui() - app.run() - - -if __name__ == "__main__": - run_app() diff --git a/pyproject.toml b/pyproject.toml index 1090428..80e1cec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,3 +95,62 @@ testpaths = ["tests"] [tool.coverage_rich] fail-under=80 + +[tool.ruff] +ignore = ["E501"] +target-version = "py37" + + +select = [ +"F", # Pyflakes +"E", # Error +"W", # Warning +"C90", # mccabe +"I", # isort +"N", # pep8-naming +"D", # pydocstyle +"UP", # pyupgrade +"YTT", # flake8-2020 +"ANN", # flake8-annotations +"S", # flake8-bandit +"BLE", # flake8-blind-except +"FBT", # flake8-boolean-trap +"B", # flake8-bugbear +"A", # flake8-builtins +"COM", # flake8-commas +"C4", # flake8-comprehensions +"DTZ", # flake8-datetimez +"T10", # flake8-debugger +"DJ", # flake8-django +"EM", # flake8-errmsg +"EXE", # flake8-executable +"ISC", # flake8-implicit-str-concat +"ICN", # flake8-import-conventions +"G", # flake8-logging-format +"INP", # flake8-no-pep420 +"PIE", # flake8-pie +"T20", # flake8-print +"PYI", # flake8-pyi +"PT", # flake8-pytest-style +"Q", # flake8-quotes +"RSE", # flake8-raise +"RET", # flake8-return +"SLF", # flake8-self +"SIM", # flake8-simplify +"TID", # flake8-tidy-imports +"TCH", # flake8-type-checking +"INT", # flake8-gettext +"ARG", # flake8-unused-arguments +"PTH", # flake8-use-pathlib +"ERA", # eradicate +"PD", # pandas-vet +"PGH", # pygrep-hooks +"PL", # Pylint +"PLC", # Convention +"PLE", # Error +"PLR", # Refactor +"PLW", # Warning +"TRY", # tryceratops +"NPY", # NumPy-specific rules +"RUF", # Ruff-specific rules +]