Skip to content

Commit

Permalink
Merge pull request #9 from welchbj/devel
Browse files Browse the repository at this point in the history
Merge devel into master for 0.1.0 release
  • Loading branch information
welchbj committed Aug 6, 2020
2 parents fa33007 + cd04c34 commit 6a8ce63
Show file tree
Hide file tree
Showing 96 changed files with 2,530 additions and 573 deletions.
27 changes: 27 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Travis matrix for cross-platform and cross-version testing.
#
# See: https://github.com/brandtbucher/travis-python-matrix

matrix:
include:
- name: CPython 3.8 -- Ubuntu 18.04
language: python
os: linux
dist: bionic
python: 3.8

- name: CPython nightly -- Ubuntu 18.04
language: python
os: linux
dist: bionic
python: nightly

install:
- pip --version
- pip install -r deps/dev-requirements.txt

script:
- python --version
- python tasks.py test
- flake8 .
- mypy .
89 changes: 88 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
<em>a framework for interactive, page-based console applications</em>
</p>
<p align="center">
<a href="https://travis-ci.org/welchbj/almanac">
<img src="https://img.shields.io/travis/welchbj/almanac/devel.svg?style=flat-square&label=linux%20build" alt="linux build status">
</a>
<a href="https://ci.appveyor.com/project/welchbj/almanac">
<img src="https://img.shields.io/appveyor/ci/welchbj/almanac/devel.svg?style=flat-square&label=windows%20build" alt="windows build status">
</a>
<a href="https://pypi.org/project/almanac/">
<img src="https://img.shields.io/pypi/v/almanac.svg?style=flat-square&label=pypi" alt="pypi">
</a>
Expand All @@ -17,7 +23,88 @@

## Synopsis

This framework aims to serve as an intuitive interface for spinning up interactive page-based console applications.
The `almanac` framework aims to serve as an intuitive interface for spinning up interactive, page-based console applications. Think of it as a Python metaprogramming layer on top of [Python Prompt Toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) and [Pygments](https://pygments.org/).

## Example

`almanac` turns this:

```python
"""Welcome to a simple interactive HTTP client.
The current URL to request is the application's current path. Directories will be
created as you cd into them.
"""

import aiohttp
import asyncio

from almanac import highlight_for_mimetype, make_standard_app, PagePath

app = make_standard_app()


@app.on_init()
async def runs_at_start_up():
app.io.raw(__doc__)

app.bag.session = aiohttp.ClientSession()
app.io.info('Session opened!')


@app.on_exit()
async def runs_at_shut_down():
await app.bag.session.close()
app.io.info('Session closed!')


@app.prompt_text()
def custom_prompt():
stripped_path = str(app.page_navigator.current_page.path).lstrip('/')
return f'{stripped_path}> '


@app.hook.before('cd')
async def cd_hook_before(path: PagePath):
if path not in app.page_navigator:
app.page_navigator.add_directory_page(path)


@app.hook.exception(aiohttp.ClientError)
async def handle_aiohttp_errors(exc: aiohttp.ClientError):
app.io.error(f'{exc.__class__.__name__}: {str(exc)}')


@app.cmd.register()
@app.arg.method(choices=['GET', 'POST', 'PUT'], description='HTTP verb for request.')
@app.arg.proto(choices=['http', 'https'], description='Protocol for request.')
async def request(method: str, *, proto: str = 'https', **params: str):
"""Send an HTTP or HTTPS request."""
path = str(app.current_path).lstrip('/')
url = f'{proto}://{path}'
app.io.info(f'Sending {method} request to {url}...')

resp = await app.bag.session.request(method, url, params=params)
async with resp:
text = await resp.text()
highlighted_text = highlight_for_mimetype(text, resp.content_type)

app.io.info(f'Status {resp.status} response from {resp.url}')
app.io.info('Here\'s the content:')
app.io.ansi(highlighted_text)


if __name__ == '__main__':
asyncio.run(app.prompt())
```

Into this:

<p align="center">
<a href="https://asciinema.org/a/352061?autoplay=1&speed=1.5">
<img src="https://asciinema.org/a/352061.png" width="750">
</a>
</p>

## Installation

Expand Down
1 change: 1 addition & 0 deletions almanac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .context import * # noqa
from .core import * # noqa
from .errors import * # noqa
from .hooks import * # noqa
from .io import * # noqa
from .pages import * # noqa
from .parsing import * # noqa
Expand Down
8 changes: 4 additions & 4 deletions almanac/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ async def main():

@app.cmd.register()
@app.cmd(aliases=['literal_eval'])
@app.arg.expr(completer=WordCompleter(['0x10', '["a"]']))
@app.arg.expr(completers=WordCompleter(['0x10', '["a"]']))
async def liteval(expr: str, verbose: Optional[bool] = False) -> int:
"""Evaluation of a Python literal."""
app = current_app()

if verbose:
app.io.print_info('Verbose mode is on!')
app.io.info('Verbose mode is on!')

app.io.print_raw(ast.literal_eval(expr))
app.io.raw(ast.literal_eval(expr))
return 0

await app.run()
await app.prompt()


if __name__ == '__main__':
Expand Down
75 changes: 51 additions & 24 deletions almanac/arguments/argument_base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""A class for encapsulating command arguments."""

from abc import ABC, abstractmethod
from abc import ABC, abstractmethod, abstractproperty
from inspect import Parameter
from typing import Any, Optional
from typing import Any, Iterable, Optional, Union

from prompt_toolkit.completion import Completer, DummyCompleter
from prompt_toolkit.completion import Completer

from ..constants import CommandLineDefaults

Expand All @@ -18,7 +18,8 @@ def __init__(
*,
name: Optional[str] = None,
description: Optional[str] = None,
completer: Optional[Completer] = None,
completers: Optional[Union[Completer, Iterable[Completer]]] = None,
hidden: bool = False
) -> None:
self._param = param

Expand All @@ -29,7 +30,15 @@ def __init__(
description if description is not None else CommandLineDefaults.DOC
)

self._completer = completer if completer is not None else DummyCompleter()
if completers is None:
self._completers = []
elif isinstance(completers, Completer):
self._completers = [completers]
else:
# Assume we have iterable of completers.
self._completers = [x for x in completers]

self._hidden = hidden

@property
def display_name(
Expand All @@ -52,26 +61,11 @@ def _abstract_display_name_setter(
) -> None:
"""Abstract display name setter to allow for access control."""

@property
def completer(
@abstractproperty
def completers(
self
) -> Completer:
"""The completer for this argument."""
return self._completer

@completer.setter
def completer(
self,
new_completer: Completer
) -> None:
self._abstract_completer_setter(new_completer)

@abstractmethod
def _abstract_completer_setter(
self,
new_completer: Completer
) -> None:
"""Abstract display name setter to allow for access control."""
) -> Iterable[Completer]:
"""The registered completers for this argument."""

@property
def description(
Expand All @@ -94,6 +88,27 @@ def _abstract_description_setter(
) -> None:
"""Abstract description setter to allow for access control."""

@property
def hidden(
self
) -> bool:
"""Whether this argument should be hidden in the interactive prompt."""
return self._hidden

@hidden.setter
def hidden(
self,
new_value: bool
) -> None:
self._abstract_hidden_setter(new_value)

@abstractmethod
def _abstract_hidden_setter(
self,
new_value: bool
) -> None:
"""Abstract hidden setter to allow for access control."""

@property
def real_name(
self
Expand Down Expand Up @@ -127,6 +142,18 @@ def is_kw_only(
) -> bool:
return self._param.kind == self._param.KEYWORD_ONLY

@property
def is_var_kw(
self
) -> bool:
return self._param.kind == self._param.VAR_KEYWORD

@property
def is_var_pos(
self
) -> bool:
return self._param.kind == self._param.VAR_POSITIONAL

@property
def has_default_value(
self
Expand Down
31 changes: 24 additions & 7 deletions almanac/arguments/frozen_argument.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Abstraction over an argument with immutable properties."""

from functools import cached_property
from typing import Tuple

from prompt_toolkit.completion import Completer

from .argument_base import ArgumentBase
from ..errors import FrozenAccessError
from ..utils import abbreviated


class FrozenArgument(ArgumentBase):
Expand All @@ -12,16 +16,29 @@ def _abstract_display_name_setter(
self,
new_display_name: str
) -> None:
raise FrozenAccessError('Cannot change the display name of a FrozenCommand')
raise FrozenAccessError('Cannot change the display name of a FrozenArgument')

def _abstract_completer_setter(
def _abstract_description_setter(
self,
new_completer: Completer
new_description: str
) -> None:
raise FrozenAccessError('Cannot change the completer of a FrozenCommand')
raise FrozenAccessError('Cannot change the description of a FrozenArgument')

def _abstract_description_setter(
def _abstract_hidden_setter(
self,
new_description: str
new_value: bool
) -> None:
raise FrozenAccessError('Cannot change the description of a FrozenCommand')
raise FrozenAccessError('Cannot change the hidden status of a FrozenArgument')

@cached_property
def abbreviated_description(
self
) -> str:
"""A shortened version of this arguments description."""
return abbreviated(self._description)

@property
def completers(
self
) -> Tuple[Completer, ...]:
return tuple(self._completers)
23 changes: 16 additions & 7 deletions almanac/arguments/mutable_argument.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Abstraction over an argument with mutable properties."""

from typing import List

from prompt_toolkit.completion import Completer

from .argument_base import ArgumentBase
Expand All @@ -14,24 +16,31 @@ def _abstract_display_name_setter(
) -> None:
self._display_name = new_display_name

def _abstract_completer_setter(
self,
new_completer: Completer
) -> None:
self._completer = new_completer

def _abstract_description_setter(
self,
new_description: str
) -> None:
self._description = new_description

def _abstract_hidden_setter(
self,
new_value: bool
) -> None:
self._hidden = new_value

@property
def completers(
self
) -> List[Completer]:
return self._completers

def freeze(
self
) -> FrozenArgument:
return FrozenArgument(
self.param,
name=self.display_name,
description=self.description,
completer=self.completer
completers=self.completers,
hidden=self.hidden
)

0 comments on commit 6a8ce63

Please sign in to comment.