Skip to content

Commit

Permalink
Added black as code formatter (#530)
Browse files Browse the repository at this point in the history
* Added configurable formatter option

* Testing formatting with black in CI

* Removed formatter option

* Reformat single quotes to double quotes

* Removed flake8-commas in favour of black PyCQA/flake8-commas#63
  • Loading branch information
kazqvaizer authored Nov 9, 2022
1 parent c32e7a5 commit ae65c7a
Show file tree
Hide file tree
Showing 63 changed files with 318 additions and 282 deletions.
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 fmt

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

fmt:
cd src && isort .
cd src && black .

lint:
dotenv-linter src/app/.env.ci
flake8 src
Expand Down
3 changes: 0 additions & 3 deletions {{cookiecutter.project_slug}}/dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ flake8==5.0.4
# via
# flake8-absolute-import
# flake8-bugbear
# flake8-commas
# flake8-django
# flake8-eradicate
# flake8-isort
Expand All @@ -143,8 +142,6 @@ flake8-bugbear==22.10.27
# via django (pyproject.toml)
flake8-cognitive-complexity==0.1.0
# via django (pyproject.toml)
flake8-commas==2.1.0
# via django (pyproject.toml)
flake8-django==1.1.5
# via django (pyproject.toml)
flake8-eradicate==1.4.0
Expand Down
18 changes: 17 additions & 1 deletion {{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,9 +44,9 @@ dev = [
"jedi",
"flake8-absolute-import",
"flake8-aaa",
"flake8-black",
"flake8-bugbear",
"flake8-cognitive-complexity",
"flake8-commas",
"flake8-django",
"flake8-eradicate",
"flake8-isort>=4.0.0",
Expand Down Expand Up @@ -74,6 +76,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 +85,7 @@ 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
]
exclude = [
"static",
Expand All @@ -98,3 +102,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

0 comments on commit ae65c7a

Please sign in to comment.