Skip to content

Commit

Permalink
Merge pull request #2 from unmade/devel
Browse files Browse the repository at this point in the history
Initial version
  • Loading branch information
unmade authored May 23, 2019
2 parents 7cb4d19 + 5fdcddf commit 6d108c1
Show file tree
Hide file tree
Showing 45 changed files with 1,102 additions and 21 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ source =
parallel = true

[report]
fail_under = 100
show_missing = true
precision = 2
omit = *__main__*
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 88
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
tests/stubs/actual

*.egg-info
pip-wheel-metadata
dist

.tox

__pycache__
Expand Down
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ repos:
- id: isort
additional_dependencies:
- toml
exclude: "example/"
- repo: local
hooks:
- id: pylint
name: pylint
entry: python3 -m pylint
language: system
types: [python]
exclude: "example/"
- id: mypy
name: mypy
entry: python3 -m mypy
Expand All @@ -32,7 +34,6 @@ repos:
rev: v2.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: debug-statements
- id: flake8
4 changes: 3 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ extension-pkg-whitelist=ujson

# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
ignore=CVS,__main__.py

# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
Expand Down Expand Up @@ -339,6 +339,8 @@ good-names=i,
ex,
Run,
_,
to,
f,

# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
Expand Down
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ after_failure:
- more .tox/log/* | cat
- more .tox/*/log/* | cat

after_success:
- pip install poetry
- poetry build

deploy:
provider: pypi
user: $TEST_PYPI_USERNAME
password: $TEST_PYPI_PASSWORD
server: https://test.pypi.org/legacy/
distributions: "sdist bdist_wheel"
on:
tags: true

notifications:
email:
on_success: never
Expand Down
78 changes: 73 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,86 @@
Overview
========

This is simple `.pyi` stubs generator from thrift interfaces
.. start-badges
* Free software: MIT license
.. image:: https://travis-ci.org/unmade/thrift-pyi.svg?branch=master
:alt: Travis-CI Build Status
:target: https://travis-ci.org/unmade/thrift-pyi

Documentation
.. image:: https://codecov.io/github/unmade/thrift-pyi/coverage.svg?branch=master
:alt: Coverage Status
:target: https://codecov.io/github/unmade/thrift-pyi

.. image:: https://api.codacy.com/project/badge/Grade/487480f045594e148309e8b7f1f71351
:alt: Codacy Badge
:target: https://app.codacy.com/app/unmade/thrift-pyi

.. image:: https://requires.io/github/unmade/thrift-pyi/requirements.svg?branch=master
:alt: Requirements Status
:target: https://requires.io/github/unmade/thrift-pyi/requirements/?branch=master

.. image:: https://img.shields.io/pypi/v/thriftpyi.svg
:alt: PyPI Package latest release
:target: https://pypi.org/project/thriftpyi

.. image:: https://img.shields.io/pypi/wheel/thriftpyi.svg
:alt: PyPI Wheel
:target: https://pypi.org/project/thriftpyi

.. image:: https://img.shields.io/pypi/pyversions/thriftpyi.svg
:alt: Supported versions
:target: https://pypi.org/project/thriftpyi

.. image:: https://img.shields.io/badge/License-MIT-purple.svg
:alt: MIT License
:target: https://github.com/unmade/thrift-pyi/blob/master/LICENSE

.. end-badges
This is simple `.pyi` stubs generator from thrift interfaces.
Motivation for this project was to have autocomplete and type checking
for dynamically loaded thrift interfaces

Installation
============

.. code-block:: bash
pip install thriftpyi
Quickstart
=============

To use the project:
Sample usage:

.. code-block:: bash
$ thriftpyi --help
$ thriftpyi example/interfaces --output example/app/interfaces
Additionally to generated stubs you might want to create `__init__.py` that will load thrift interfaces, for example:

.. code-block:: python
from pathlib import Path
from types import ModuleType
from typing import Dict
import thriftpy2
_interfaces_path = Path("example/interfaces")
_interfaces: Dict[str, ModuleType] = {}
def __getattr__(name):
try:
return _interfaces[name]
except KeyError:
interface = thriftpy2.load(str(_interfaces_path.joinpath(f"{name}.thrift")))
_interfaces[name] = interface
return interface
To see more detailed example of usage refer to `example app <https://github.com/unmade/thrift-pyi/blob/master/example>`_

Development
===========
Expand Down
34 changes: 34 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Example app

This example app shows how to use [generated .pyi files](app/interfaces)
in order to have correct autocomplete and type checking for dynamically loaded thrift interfaces

## Overview

Stubs were created with command:

thriftpyi example/interfaces --output example/app/interfaces

Note, that [\_\_init__.py](app/interfaces/__init__.py) was created by hand and not by the script.
This file is responsible for the actual access to thrift interfaces.

## Consideration

Normally you must do import like this:
```python
from example.app import interfaces
```

Imports like this will no work:
```python
# DANGER!!! THIS WILL NOT WORK
from example.app.interfaces.todo import Todo
```

Although, if you still want do import things like shown above you can do it like this:
```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from example.app.interfaces.todo import Todo
```
Empty file added example/__init__.py
Empty file.
Empty file added example/app/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions example/app/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import TYPE_CHECKING

from example.app import interfaces
from thriftpy2.rpc import make_client

if TYPE_CHECKING:
from example.app.interfaces.todo import Todo


if __name__ == "__main__":
client: "Todo" = make_client(interfaces.todo.Todo, "127.0.0.1", 6000)

todo_id = client.create(text="item", type=interfaces.todo.TodoType.NOTE)
print(f"CREATE = {todo_id}")
print(f"GET = {client.get(id=todo_id)}")
try:
client.get(id=todo_id + 1)
except interfaces.shared.NotFound:
print(f"NOT FOUND")
17 changes: 17 additions & 0 deletions example/app/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pathlib import Path
from types import ModuleType
from typing import Dict

import thriftpy2

_interfaces_path = Path("example/interfaces")
_interfaces: Dict[str, ModuleType] = {}


def __getattr__(name):
try:
return _interfaces[name]
except KeyError:
interface = thriftpy2.load(str(_interfaces_path.joinpath(f"{name}.thrift")))
_interfaces[name] = interface
return interface
4 changes: 4 additions & 0 deletions example/app/interfaces/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import dates
from . import shared
from . import todo
from . import todo_v2
15 changes: 15 additions & 0 deletions example/app/interfaces/dates.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass
from typing import *
@dataclass
class DateTime:
year: int
month: int
day: int
hour: int
minute: int
second: int
microsecond: Optional[int] = None

@dataclass
class Date:
pass
13 changes: 13 additions & 0 deletions example/app/interfaces/shared.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from dataclasses import dataclass
from typing import *

class NotFound(Exception):
message: Optional[str] = None

@dataclass
class LimitOffset:
limit: Optional[int] = None
offset: Optional[int] = None

class Service:
def ping(self,) -> str: ...
31 changes: 31 additions & 0 deletions example/app/interfaces/todo.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from dataclasses import dataclass
from enum import IntEnum
from typing import *

from . import shared
from . import dates

class TodoType(IntEnum):
PLAIN = 1
NOTE = 2
CHECKBOXES = 3

@dataclass
class TodoItem:
id: int
text: str
type: int
created: dates.DateTime
is_deleted: bool
picture: Optional[str] = None

class Todo:
def create(self, text: str, type: int) -> int: ...
def update(self, id: int, text: str, type: int) -> None: ...
def get(self, id: int) -> TodoItem: ...
def all(self, pager: shared.LimitOffset) -> List[TodoItem]: ...
def filter(self, ids: List[int]) -> List[TodoItem]: ...
def stats(self,) -> Dict[int, float]: ...
def types(self,) -> Set[int]: ...
def groupby(self,) -> Dict[int, List[TodoItem]]: ...
def ping(self,) -> str: ...
2 changes: 2 additions & 0 deletions example/app/interfaces/todo_v2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class Todo:
pass
45 changes: 45 additions & 0 deletions example/app/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import datetime
from typing import TYPE_CHECKING, Dict

from example.app import interfaces
from thriftpy2.rpc import make_server

if TYPE_CHECKING:
from example.app.interfaces.todo import TodoItem

todos: Dict[int, "TodoItem"] = {}


class Dispatcher(object):
def create(self, text: str, type: int) -> int:
todo_id = max(todos.keys() or [0]) + 1
created = datetime.datetime.now()
todos[todo_id] = interfaces.todo.TodoItem(
id=todo_id,
text=text,
type=type,
created=interfaces.dates.DateTime(
year=created.year,
month=created.month,
day=created.day,
hour=created.hour,
minute=created.minute,
second=created.second,
),
is_deleted=False,
)
return todo_id

def get(self, id: int) -> "TodoItem":
try:
return todos[id]
except KeyError:
raise interfaces.shared.NotFound

def ping(self):
return "pong"


if __name__ == "__main__":
server = make_server(interfaces.todo.Todo, Dispatcher(), "127.0.0.1", 6000)
server.serve()
15 changes: 15 additions & 0 deletions example/interfaces/dates.thrift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace * dates

struct DateTime {
1: required i16 year,
2: required byte month,
3: required byte day
4: required i16 hour,
5: required byte minute,
6: required byte second,
7: optional i64 microsecond
}

struct Date {

}
14 changes: 14 additions & 0 deletions example/interfaces/shared.thrift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
exception NotFound {
1: optional string message
}


struct LimitOffset {
1: optional i32 limit
2: optional i32 offset
}


service Service {
string ping();
}
Loading

0 comments on commit 6d108c1

Please sign in to comment.