Skip to content

Commit

Permalink
rats-devtools improvements (#157)
Browse files Browse the repository at this point in the history
- the `rats.devtools` package no longer has commands in it, but has
libraries for creating commands
- existing commands were moved to more specific sub-packages
  - rats.ci
  - rats.docs
  - rats.pycharm
- i removed the existing `@command` decorator and went back to the
vanilla `@click.command()` decorator
- the current api is verbose but i should be able to re-implement the
simpler decorators in another pr
- started a general purpose `rats.annotations` package for non-container
specific object annotations
- added support for cli sub-commands, with tab-completion support, and
lazy-loading of command groups
- fixed and re-enabled api docs generation for `rats-apps`
- beginnings of a command_tree package

---------

Co-authored-by: Andrew FigPope <[email protected]>
  • Loading branch information
ms-lolo and figpope authored May 7, 2024
1 parent cf57541 commit 78f3a7b
Show file tree
Hide file tree
Showing 71 changed files with 2,441 additions and 485 deletions.
15 changes: 8 additions & 7 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
pipx install -e rats-devtools/
- name: "poetry-install"
run: |
rats-devtools install ${{ matrix.component }}/
rats-devtools ci poetry-install ${{ matrix.component }}/
- id: package-info
name: package-info
run: |
Expand Down Expand Up @@ -155,14 +155,15 @@ jobs:
- name: "poetry-install"
run: |
# I would like to optimize this soon before these steps become too slow
rats-devtools install rats-devtools/
rats-devtools install rats-pipelines/
rats-devtools install rats-processors/
rats-devtools install rats-examples-sklearn/
rats-devtools ci poetry-install rats-apps/
rats-devtools ci poetry-install rats-devtools/
rats-devtools ci poetry-install rats-pipelines/
rats-devtools ci poetry-install rats-processors/
rats-devtools ci poetry-install rats-examples-sklearn/
- name: "build-docs"
run: |
rats-devtools build-api-docs
rats-devtools mkdocs-build
rats-devtools docs sphinx-build
rats-devtools docs mkdocs-build
- name: "upload-gh-pages"
uses: actions/upload-pages-artifact@v3
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@

# rats-devtools generates these files for us
bin/*.pipx

# Aider tracks conversation history in-repo, so ignoring these files
.aider*
2 changes: 1 addition & 1 deletion .idea/watcherTasks.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 18 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ ci:
autoupdate_commit_msg: ":arrow-up: dep-bump(pre-commit): update pre-commit hooks"

repos:
- repo: local
hooks:
- id: ruff-format
name: ruff-format
entry: "bin/ruff-format"
require_serial: true
language: script
pass_filenames: true
files: "src/python/|test/python/"
types: [ python ]
- id: ruff-check
name: ruff-check
entry: "bin/ruff-check"
require_serial: true
language: script
pass_filenames: true
files: "src/python/|test/python/"
types: [ python ]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
Expand All @@ -28,20 +46,6 @@ repos:
- id: name-tests-test
- id: trailing-whitespace

- repo: https://gitlab.com/bmares/check-json5
rev: v1.0.0
hooks:
- id: check-json5

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.2
hooks:
- id: ruff # linter
types_or: [python, pyi, jupyter]
args: [--fix]
- id: ruff-format # formatter
types_or: [python, pyi, jupyter]

- repo: https://github.com/commitizen-tools/commitizen
rev: v3.25.0
hooks:
Expand Down
22 changes: 14 additions & 8 deletions bin/rats-devtools.setup
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
REPO_DIR="$SCRIPT_DIR/.."
export PIPX_BIN_DIR="$SCRIPT_DIR"
echo "In order to make sure that the pipx installation is correct, we will remove the pipx installation and reinstall it."
echo "Executables will be installed in $PIPX_BIN_DIR."
echo "Using a .pipx suffix on executables. These should be .gitignored because they are system specific."
pipx uninstall rats-devtools.pipx
pipx install -e $REPO_DIR/rats-devtools/ --suffix .pipx || exit $?
echo "Installation complete. Run the below command to enable shell completions:"
echo 'eval "$(_RATS_DEVTOOLS_PIPX_COMPLETE=zsh_source rats-devtools.pipx)"'
LOCAL_SUFFIX="pipx"
GLOBAL_SUFFIX=$(echo -n $SCRIPT_DIR | md5sum | awk '{print $1}')
LOCAL_PATH="$SCRIPT_DIR/rats-devtools.$LOCAL_SUFFIX"
GLOBAL_PATH="$(pipx environment --value PIPX_BIN_DIR)/rats-devtools.$GLOBAL_SUFFIX"
echo "Local Executable: $LOCAL_PATH"
echo "Global Executable: $GLOBAL_PATH"
pipx uninstall "rats-devtools.${GLOBAL_SUFFIX}"
pipx install -e $REPO_DIR/rats-devtools/ --suffix .$GLOBAL_SUFFIX || exit $?
rm $LOCAL_PATH || true
ln -s $GLOBAL_PATH $LOCAL_PATH || exit $?
# Disabled this prompt until I find a good way to do it.
#echo "For shell autocompletion add this to your profile:"
#echo "eval '\$(_RATS_DEVTOOLS_${GLOBAL_SUFFIX^^}_COMPLETE=zsh_source rats-devtools.${GLOBAL_SUFFIX})'"
echo "Installation complete!"
58 changes: 36 additions & 22 deletions rats-apps/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rats-apps/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ version-pinning-strategy = "semver"
[tool.poetry.dependencies]
python = "^3.10"
typing_extensions = "*"
click = "*"

[tool.poetry.group.dev.dependencies]
coverage = "*"
Expand Down
25 changes: 25 additions & 0 deletions rats-apps/src/python/rats/annotations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
General purpose library to attach annotations to functions.
Annotations are typically, but not exclusively, attached using decorators.
"""

from ._functions import (
AnnotationsContainer,
DecoratorType,
GroupAnnotations,
T_GroupType,
annotation,
get_annotations,
get_class_annotations,
)

__all__ = [
"annotation",
"DecoratorType",
"AnnotationsContainer",
"get_annotations",
"get_class_annotations",
"GroupAnnotations",
"T_GroupType",
]
125 changes: 125 additions & 0 deletions rats-apps/src/python/rats/annotations/_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# type: ignore
from __future__ import annotations

from collections import defaultdict
from collections.abc import Callable
from functools import cache
from typing import Any, Generic, ParamSpec, TypeVar
from typing import NamedTuple as tNamedTuple

from typing_extensions import NamedTuple

T_GroupType = TypeVar("T_GroupType", bound=NamedTuple)


class GroupAnnotations(NamedTuple, Generic[T_GroupType]):
"""The list of T_GroupType objects identified by a given name in a namespace."""

name: str
"""The name of the annotation, typically the name of the function in a class"""
namespace: str
"""All the groups in a namespace are expected to be of the same T_GroupType."""
groups: tuple[T_GroupType, ...]


class AnnotationsContainer(NamedTuple):
"""
Holds metadata about the annotated service provider.
Loosely inspired by: https://peps.python.org/pep-3107/.
"""

annotations: tuple[GroupAnnotations[...], ...]

@staticmethod
def empty() -> AnnotationsContainer:
return AnnotationsContainer(annotations=())

def with_group(
self,
namespace: str,
group_id: T_GroupType,
) -> AnnotationsContainer:
return AnnotationsContainer(
annotations=tuple(
[
annotation_group
for x in self.with_namespace(namespace)
for annotation_group in x
if group_id in annotation_group.groups
]
),
)

def with_namespace(
self,
namespace: str,
) -> AnnotationsContainer:
return AnnotationsContainer(
annotations=tuple([x for x in self.annotations if x.namespace == namespace]),
)


class AnnotationsBuilder:
_group_ids: dict[str, set[Any]]

def __init__(self) -> None:
self._group_ids = defaultdict(set)

def add(self, namespace: str, group_id: NamedTuple | tNamedTuple) -> None:
self._group_ids[namespace].add(group_id)

def make(self, name: str) -> AnnotationsContainer:
return AnnotationsContainer(
annotations=tuple(
[
GroupAnnotations[Any](name=name, namespace=namespace, groups=tuple(groups))
for namespace, groups in self._group_ids.items()
]
),
)


DecoratorType = TypeVar("DecoratorType", bound=Callable[..., Any])


def annotation(
namespace: str,
group_id: NamedTuple | tNamedTuple,
) -> Callable[[DecoratorType], DecoratorType]:
def decorator(fn: DecoratorType) -> DecoratorType:
if not hasattr(fn, "__rats_annotations__"):
fn.__rats_annotations__ = AnnotationsBuilder() # type: ignore[reportFunctionMemberAccess]

fn.__rats_annotations__.add(namespace, group_id) # type: ignore[reportFunctionMemberAccess]

return fn

return decorator


@cache
def get_class_annotations(cls: type) -> AnnotationsContainer:
tates = []

for method_name in dir(cls):
method = getattr(cls, method_name)
if not hasattr(method, "__rats_annotations__"):
continue

tates.extend(method.__rats_annotations__.make(method_name).annotations)

return AnnotationsContainer(annotations=tuple(tates))


P = ParamSpec("P")


def get_annotations(fn: Callable[..., Any]) -> AnnotationsContainer:
builder: AnnotationsBuilder = getattr(
fn,
"__rats_annotations__",
AnnotationsBuilder(),
)

return builder.make(fn.__name__)
Empty file.
Loading

0 comments on commit 78f3a7b

Please sign in to comment.