Skip to content

Commit 8ae05ad

Browse files
authored
Type-check Model.__init__
1 parent 2738bf7 commit 8ae05ad

16 files changed

+558
-238
lines changed

.github/workflows/release.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
deploy:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v2
12+
- name: Set up Python
13+
uses: actions/setup-python@v2
14+
with:
15+
python-version: '3.x'
16+
- name: Install build dependencies
17+
run: |
18+
pip install build
19+
20+
- name: Build packages
21+
run: |
22+
python -m build
23+
24+
- name: Publish to PyPI
25+
uses: pypa/gh-action-pypi-publish@release/v1
26+
with:
27+
user: __token__
28+
password: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/test.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Python package
2+
3+
on:
4+
push:
5+
branches: [ "master" ]
6+
pull_request:
7+
branches: [ "master" ]
8+
9+
jobs:
10+
build:
11+
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: ["3.8", "3.9", "3.10"]
17+
18+
steps:
19+
- uses: actions/checkout@v3
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v3
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
- name: Install test dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -r requirements.txt
28+
- name: Test with pytest
29+
run: |
30+
pytest -v

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414
build/
1515
dist/
1616
venv/
17+
tests/playground.py
18+
touch.sh

.pre-commit-config.yaml

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
repos:
22
- repo: https://github.com/asottile/reorder_python_imports
3-
rev: v1.3.4
3+
rev: v3.8.5
44
hooks:
55
- id: reorder-python-imports
66
- repo: https://github.com/pre-commit/pre-commit-hooks
7-
rev: v2.0.0
7+
rev: v4.3.0
88
hooks:
99
- id: check-ast
1010
- id: check-docstring-first
@@ -15,19 +15,24 @@ repos:
1515
- id: debug-statements
1616
- id: end-of-file-fixer
1717
- id: trailing-whitespace
18-
- repo: https://gitlab.com/pycqa/flake8
19-
rev: 3.8.1
20-
hooks:
21-
- id: flake8
22-
additional_dependencies:
23-
- flake8-bugbear==18.8.0
24-
- flake8-comprehensions==1.4.1
25-
- flake8-tidy-imports==1.1.0
2618
- repo: https://github.com/asottile/pyupgrade
27-
rev: v1.11.0
19+
rev: v3.1.0
2820
hooks:
2921
- id: pyupgrade
22+
args: [--py38-plus]
23+
- repo: https://github.com/psf/black
24+
rev: 22.8.0
25+
hooks:
26+
- id: black
27+
- repo: https://github.com/pycqa/flake8
28+
rev: 5.0.4
29+
hooks:
30+
- id: flake8
31+
additional_dependencies:
32+
- flake8-bugbear==22.9.23
33+
- flake8-comprehensions==3.10.0
34+
- flake8-tidy-imports==4.8.0
3035
- repo: https://github.com/pre-commit/mirrors-mypy
31-
rev: v0.770
36+
rev: v0.982
3237
hooks:
33-
- id: mypy
38+
- id: mypy

.travis.yml

Lines changed: 0 additions & 25 deletions
This file was deleted.

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,30 @@ would have to be changed to, e.g.:
3131
if my_model.my_value and my_model.my_value.lower() == 'foo':
3232
print("Value is foo")
3333
```
34+
35+
# Typed model initializers
36+
37+
When declaring models, the `__init__` method would be typed to accept only the attributes declared in the model. For example:
38+
```py
39+
from pynamodb.models import Model
40+
from pynamodb.attributes import NumberAttribute
41+
from pynamodb.attributes import UnicodeAttribute
42+
43+
class MyModel(Model):
44+
my_key = UnicodeAttribute(hash_key=True)
45+
my_value = NumberAttribute(null=True)
46+
47+
# Existing attributes would be enforced:
48+
_ = MyModel(my_key='key', my_value=42, my_other_value='other_value') # error: Unexpected keyword argument "my_other_value" for "MyModel"
49+
50+
# Typing would be enforced:
51+
_ = MyModel(my_key='key', my_value='42') # error: Argument 2 to "MyModel" has incompatible type "str"; expected "Optional[int]"
52+
53+
# Nullability will be enforced::
54+
_ = MyModel(my_key='key', my_value=None)
55+
_ = MyModel(my_key=None, my_value=None) # error: Argument "my_key" to "MyModel" has incompatible type "None"; expected "str"
56+
57+
# The hash key and range key can also be passed as positional arguments:
58+
_ = MyModel('key')
59+
_ = MyModel(42) # error: Argument 1 to "MyModel" has incompatible type "int"; expected "str"
60+
```

pynamodb_mypy/_private_api.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Here we'll be using mypy private API shamelessly.
3+
"""
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
import mypy.checker
9+
import mypy.checkmember
10+
from mypy.nodes import Context
11+
from mypy.types import Type
12+
13+
14+
def get_descriptor_access_type(ctx: Context, chk: mypy.checker.TypeChecker, descriptor: Type) -> Optional[Type]:
15+
"""
16+
Given a descriptor type, returns the type its __get__ method returns.
17+
"""
18+
return mypy.checkmember.analyze_descriptor_access(
19+
descriptor,
20+
mypy.checkmember.MemberContext(
21+
is_lvalue=False,
22+
is_super=False,
23+
is_operator=False,
24+
original_type=descriptor,
25+
context=ctx,
26+
msg=chk.msg,
27+
chk=chk,
28+
self_type=None,
29+
),
30+
)

0 commit comments

Comments
 (0)