Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added black as code formatter #530

Merged
merged 5 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
name: Bootstrap the project
command: |
. venv/bin/activate
make
make test

- persist_to_workspace:
root: .
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
run: pip install cookiecutter

- name: Bootstrap the project
run: make
run: make test
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* [Whitenoise](http://whitenoise.evans.io) for effortless static files hosting
* Sentry. Set `SENTRY_DSN` env var if you need it.
* cloudflare-ready with [django-ipware](https://github.com/un33k/django-ipware)
* [black](https://github.com/psf/black) as uncompromising code formatter

## Optional next steps
You definetely should consider this steps after installation:
Expand Down
3 changes: 3 additions & 0 deletions hooks/post_gen_project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ echo Running initial migrations...
./manage.py migrate

cd ../
echo Apply formatting..
make frmt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А не лучше ли здесь будет написать fmt вместо frmt, по аналогии с gofmt?


echo Running flake8..
make lint

Expand Down
4 changes: 4 additions & 0 deletions {{cookiecutter.project_slug}}/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ deps:
dev-deps: deps
pip-compile --extra=dev --output-file=dev-requirements.txt pyproject.toml

frmt:
cd src && isort .
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А разве isort не нужны какие-то параметры, чтобы он форматировал импорты без подтверждения от пользователя?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не нужно

cd src && black .

lint:
dotenv-linter src/app/.env.ci
flake8 src
Expand Down
18 changes: 18 additions & 0 deletions {{cookiecutter.project_slug}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ dev = [
"pytest-mock",
"pytest-randomly",

"black",

"dotenv-linter",

"freezegun",
Expand All @@ -42,6 +44,7 @@ dev = [
"jedi",
"flake8-absolute-import",
"flake8-aaa",
"flake8-black",
"flake8-bugbear",
"flake8-cognitive-complexity",
"flake8-commas",
Expand Down Expand Up @@ -74,6 +77,7 @@ dev = [

[tool.flake8]
max-line-length = 160
inline-quotes = "\""
ignore = [
"DJ05", # URLs include() should set a namespace
"E501", # Line too long
Expand All @@ -82,6 +86,8 @@ ignore = [
"PT001", # Use @pytest.fixture() over @pytest.fixture
"SIM102", # Use a single if-statement instead of nested if-statements
"SIM113", # Use enumerate instead of manually incrementing a counter
"E203", # whitespace before ':', disabled for black purposes https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices
"C812", # Missing trailing comma, disabled for black purposes https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#the-magic-trailing-comma
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Скажи, а остаётся ли какой-нибудь смысл в наличии flake8-commas теперь?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Интересный вопрос. Пару месяцев назад я думал, что еще нужен, тк были еще проверки C818 C819

Сейчас решил перечитать доки и оказывается, что flake8-commas устарел PyCQA/flake8-commas#69, причем в пользу black PyCQA/flake8-commas#63. Удалю эту батарейку

]
exclude = [
"static",
Expand All @@ -98,3 +104,15 @@ line_length = 160
extra_standard_library = ["pytest"]
known_django = ["django", "restframework"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "DJANGO", "FIRSTPARTY", "LOCALFOLDER"]
use_parentheses = true
include_trailing_comma = true
multi_line_output = 3


[tool.black]
exclude = '''
/(
| migrations
)/
'''
line_length = 160
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ from app.base_config import AppConfig


class {{ camel_case_app_name }}Config(AppConfig):
name = '{{ app_name }}'
name = "{{ app_name }}"
3 changes: 2 additions & 1 deletion {{cookiecutter.project_slug}}/src/a12n/api/throttling.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

class AuthAnonRateThrottle(ConfigurableThrottlingMixin, AnonRateThrottle):
"""Throttle for any authorization views."""
scope = 'anon-auth'

scope = "anon-auth"
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,60 @@ def _enable_django_axes(settings):
@pytest.fixture
def get_token(as_user):
def _get_token(username, password, expected_status=201):
return as_user.post('/api/v1/auth/token/', {
'username': username,
'password': password,
}, format='json', expected_status=expected_status)
return as_user.post(
"/api/v1/auth/token/",
{
"username": username,
"password": password,
},
format="json",
expected_status=expected_status,
)

return _get_token


def _decode(response):
return json.loads(response.content.decode('utf-8', errors='ignore'))
return json.loads(response.content.decode("utf-8", errors="ignore"))


def test_getting_token_ok(as_user, get_token):
result = get_token(as_user.user.username, as_user.password)

assert 'token' in result
assert "token" in result


def test_getting_token_is_token(as_user, get_token):
result = get_token(as_user.user.username, as_user.password)

assert len(result['token']) > 32 # every stuff that is long enough, may be a JWT token
assert len(result["token"]) > 32 # every stuff that is long enough, may be a JWT token


def test_getting_token_with_incorrect_password(as_user, get_token):
result = get_token(as_user.user.username, 'z3r0c00l', expected_status=400)
result = get_token(as_user.user.username, "z3r0c00l", expected_status=400)

assert 'nonFieldErrors' in result
assert "nonFieldErrors" in result


def test_getting_token_with_incorrect_password_creates_access_attempt_log_entry(as_user, get_token):
get_token(as_user.user.username, 'z3r0c00l', expected_status=400) # act
get_token(as_user.user.username, "z3r0c00l", expected_status=400) # act

assert AccessAttempt.objects.count() == 1


@pytest.mark.parametrize(('extract_token', 'status_code'), [ # NOQA: AAA01
(lambda response: response['token'], 200),
(lambda *args: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRpbW90aHk5NSIsImlhdCI6MjQ5MzI0NDgwMCwiZXhwIjoyNDkzMjQ1MTAwLCJqdGkiOiI2MWQ2MTE3YS1iZWNlLTQ5YWEtYWViYi1mOGI4MzBhZDBlNzgiLCJ1c2VyX2lkIjoxLCJvcmlnX2lhdCI6MjQ5MzI0NDgwMH0.YQnk0vSshNQRTAuq1ilddc9g3CZ0s9B0PQEIk5pWa9I', 401),
(lambda *args: 'sh1t', 401),
])
@pytest.mark.parametrize(
("extract_token", "status_code"),
[
(lambda response: response["token"], 200),
(
lambda *args: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRpbW90aHk5NSIsImlhdCI6MjQ5MzI0NDgwMCwiZXhwIjoyNDkzMjQ1MTAwLCJqdGkiOiI2MWQ2MTE3YS1iZWNlLTQ5YWEtYWViYi1mOGI4MzBhZDBlNzgiLCJ1c2VyX2lkIjoxLCJvcmlnX2lhdCI6MjQ5MzI0NDgwMH0.YQnk0vSshNQRTAuq1ilddc9g3CZ0s9B0PQEIk5pWa9I",
401,
),
(lambda *args: "sh1t", 401),
],
)
def test_received_token_works(as_user, get_token, as_anon, extract_token, status_code):
token = extract_token(get_token(as_user.user.username, as_user.password))
as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")

as_anon.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')

as_anon.get('/api/v1/users/me/', expected_status=status_code)
as_anon.get("/api/v1/users/me/", expected_status=status_code) # act
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,67 @@

pytestmark = [
pytest.mark.django_db,
pytest.mark.freeze_time('2049-01-05'),
pytest.mark.freeze_time("2049-01-05"),
]


@pytest.fixture
def refresh_token(as_user):
def _refresh_token(token, expected_status=201):
return as_user.post('/api/v1/auth/token/refresh/', {
'token': token,
}, format='json', expected_status=expected_status)
return as_user.post(
"/api/v1/auth/token/refresh/",
{
"token": token,
},
format="json",
expected_status=expected_status,
)

return _refresh_token


@pytest.fixture
def initial_token(as_user):
with freeze_time('2049-01-03'):
with freeze_time("2049-01-03"):
return get_jwt(as_user.user)


def test_refresh_token_ok(initial_token, refresh_token):
result = refresh_token(initial_token)

assert 'token' in result
assert "token" in result


def test_refreshed_token_is_a_token(initial_token, refresh_token):
result = refresh_token(initial_token)

assert len(result['token']) > 32
assert len(result["token"]) > 32


def test_refreshed_token_is_new_one(initial_token, refresh_token):
result = refresh_token(initial_token)

assert result['token'] != initial_token
assert result["token"] != initial_token


def test_refresh_token_fails_with_incorrect_previous_token(refresh_token):
result = refresh_token('some-invalid-previous-token', expected_status=400)
result = refresh_token("some-invalid-previous-token", expected_status=400)

assert 'nonFieldErrors' in result
assert "nonFieldErrors" in result


def test_token_is_not_allowed_to_refresh_if_expired(initial_token, refresh_token):
with freeze_time('2049-02-05'):
with freeze_time("2049-02-05"):

result = refresh_token(initial_token, expected_status=400)

assert 'expired' in result['nonFieldErrors'][0]
assert "expired" in result["nonFieldErrors"][0]


def test_received_token_works(as_anon, refresh_token, initial_token):
token = refresh_token(initial_token)['token']
as_anon.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
token = refresh_token(initial_token)["token"]
as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {token}")

result = as_anon.get('/api/v1/users/me/')
result = as_anon.get("/api/v1/users/me/")

assert result is not None
6 changes: 3 additions & 3 deletions {{cookiecutter.project_slug}}/src/a12n/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from a12n.api import views

app_name = 'a12n'
app_name = "a12n"
urlpatterns = [
path('token/', views.ObtainJSONWebTokenView.as_view()),
path('token/refresh/', views.RefreshJSONWebTokenView.as_view()),
path("token/", views.ObtainJSONWebTokenView.as_view()),
path("token/refresh/", views.RefreshJSONWebTokenView.as_view()),
]
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/src/app/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ from books.models import Book
@admin.register(Book)
class BookAdmin(ModelAdmin):
fields = [
'name',
"name",
]
```
4 changes: 2 additions & 2 deletions {{cookiecutter.project_slug}}/src/app/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
from app.admin.model_admin import ModelAdmin

__all__ = [
'admin',
'ModelAdmin',
"admin",
"ModelAdmin",
]
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/src/app/api/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@


class AppPagination(PageNumberPagination):
page_size_query_param = 'page_size'
page_size_query_param = "page_size"
max_page_size = settings.MAX_PAGE_SIZE
4 changes: 2 additions & 2 deletions {{cookiecutter.project_slug}}/src/app/api/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


class AppJSONRenderer(CamelCaseJSONRenderer):
charset = 'utf-8' # force DRF to add charset header to the content-type
json_underscoreize = {'no_underscore_before_number': True} # https://github.com/vbabiy/djangorestframework-camel-case#underscoreize-options
charset = "utf-8" # force DRF to add charset header to the content-type
json_underscoreize = {"no_underscore_before_number": True} # https://github.com/vbabiy/djangorestframework-camel-case#underscoreize-options
2 changes: 1 addition & 1 deletion {{cookiecutter.project_slug}}/src/app/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")

application = get_asgi_application()
3 changes: 2 additions & 1 deletion {{cookiecutter.project_slug}}/src/app/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class AppConfig(BaseAppConfig):
Allthough, if you wish to use signals, place handlers to the `signals/handlers.py`:
your code be automatically imported and used.
"""

def ready(self) -> None:
"""Import a module with handlers if it exists to avoid boilerplate code."""
with contextlib.suppress(ModuleNotFoundError):
importlib.import_module('.signals.handlers', self.module.__name__) # type: ignore
importlib.import_module(".signals.handlers", self.module.__name__) # type: ignore
Loading