python - flake8, Black 도입, pre-commit & clean code-style 실천하기

정현우·2022년 12월 15일
6
post-thumbnail
post-custom-banner

Python Code Style Linter

save and auto / shift + option + f 는 이제 넣어둘때입니다! PEP 8 기준 python code style tool - Linter로 사용할 수 있는 flake8 그리고 조합이 좋은 black을 살펴보고 pre-commit 까지 설정해보자.

1. code style base, 왜 필요한가

  • 처음부터 끝까지 나 혼자만 작업하는 코드, toy project라면 코드 스타일은 어짜피 하나 밖에 없어서 혼란이 없다. 하지만 거의 대부분의 경우엔 협업으로 진행한다. (아이러니하게 혼자만든 코드도 다음날 보면 왜 이렇지? 하는게 많다 🥹)

  • 사람마다 선호하는 코드 스타일이 있다. 대표적으로 sigle('), double quotation(")을 생각할 수 있다. 언어의 유연함이나 자유로움이 크면 클 수록 이런 면모가 두드러진다.

  • 하지만 개발을 하는 사람이 많아질 수 록 _ 언더바 사용의 경우, 한 라인의 글자 수 제한하기, 함수 정의시 매개변수를 줄바꿈으로 묶어주기, in-line으로 매개변수 정의하기 등 과 같은 경우는 다양해진다.

  • 처음보는 code는 책과 같다. 화자의 의도를 파악해야 한다. 근데 그 화자가 10명, 20명이면 가독성은 산으로 간다. 나와 남을 위해 마치 한 사람이 짜서 맞춘 스타일의 코드 가 필요하다.

2. flake8

린트(lint) 또는 린터(linter)는 소스코드를 분석하여 의심스럽거나, 에러를 발생하기 쉬운 코드에 표시(flag)를 달아 놓는 것을 말한다. 원래는 C 언어에서 사용하던 용어였으나 지금은 다른 언어에서도 일반적으로 사용된다.

  1. PyFlakes (pyflakes : 코드의 에러 체크)
  2. pycodestyle (pycodestyle : PEP8에 준거하고 있는지를 체크)
  3. Ned Batchelder's McCabe script (mccabe : 순환 복잡도를 체크)

1) 사용법

  • pip install flake8 설치 후 linting 하고 싶은 file을 flake8 filename 으로 사용하면 된다. 파일 명 뿐 아니라 특정 디렉토리를 적어도 실행이 가능하다.
>> flake8 ./proxy_working_test.py --show-source
./proxy_working_test.py:17:80: E501 line too long (87 > 79 characters)
            res = requests.get("https://icanhazip.com/", proxies=proxy_dict, timeout=2)
                                                                               ^
  • --show-source option으로 수정 사항을 소스 코드에서 바로 표기해 준다.
> flake8 --select E123,W503 ./proxy_working_test.py
> flake8 --extend-ignore E203,W234 ./proxy_working_test.py
./proxy_working_test.py:17:80: E501 line too long (87 > 79 characters)
  • 위와 같이 특정 exception(error)를 제외 또는 타겟팅해서 linting이 가능하다. 다양한 option과 error code에 대한 설명은 공식가이드에 상세하게 설명되어 있다.

  • Flake8 Rules 와 같이 정리해둔 곳에서 찾아보는 것이 더 빠르다.

2) 전역 사용 설정

  • Configuration File 을 만들어서 (user 별로) 전역 사용이 가능하다.

    • Linux, OS X: ~/.config/flake8
    • Windows : ~.flake8
  • 위와 같이 전역 파일을 생성하여 flake8 사용 설정이 가능하다. 하지만 가장 많이 사용되는 방법은 project의 최상위 경로에 보통 setup.cfg, tox.ini, .flake8 등과 같은 파일을 만들어서 설정을 한다.

  • .flake8 파일을 아래와 같이 만들어두면 좋다.

[flake8]
show-source = true
extend-ignore = E203
exclude =
    # No need to traverse our git directory
    .git,
    # There's no value in checking cache directories
    __pycache__,
    # The conf file is mostly autogenerated, ignore it
    docs/source/conf.py,
    # The old directory contains Flake8 2.0
    old,
    # This contains our built documentation
    build,
    # This contains builds of flake8 that we don't want to check
    dist
max-complexity = 10
  • 이런 설정 파일은 flake8 만 cli로 실행해도 다음 cli를 실행한 것과 같은 효과를 얻는다. (참고로 E203는 Whitespace before ':' 이다)
flake8 --show-source --extend-ignore E203 \
         --exclude .git,__pycache__,docs/source/conf.py,old,build,dist \
         --max-complexity 10
  • 하지만 flake8은 코드 스타일의 강제화와 강제 변환이 없는 단점(노력하면 사실 장점..)이 있다. 그래서 black과 많이들 섞어 사용한다.

  • black에서 보통 line 제한을 88개로 하기 때문에 flake8에서 max-line-length = 88 이상으로 하는것이 서로 충돌안하게 좋다.


3. Black

Black is the uncompromising Python code formatter, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.

  • 해당 링크 에서 black을 적용하면 code style이 어떻게 바뀌는지 바로 보여준다.

1) 사용법

  • pip install black 설치 후 linting 하고 싶은 file을 black filename 으로 사용하면 된다. 파일 명 뿐 아니라 특정 디렉토리를 적어도 실행이 가능하다. (flake8과 동일)

  • 만약 당신이 아래와 같은 코드를 전임자로 부터 받았다고 하자. flake8을 돌리고 나오는 error를 한땀 한땀 고치고 싶을까?

from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
x = {  'a':37,'b':42,

'c':927}

x = 123456789.123456789E123456789

if very_long_variable_name is not None and \
 very_long_variable_name.field > 0 or \
 very_long_variable_name.is_debug:
 z = 'hello '+'world'
else:
 world = 'world'
 a = 'hello {}'.format(world)
 f = rf'hello {world}'
if (this
and that): y = 'hello ''world'#FIXME: https://github.com/psf/black/issues/26
class Foo  (     object  ):
  def f    (self   ):
    return       37*-2
  def g(self, x,y=42):
      return y
def f  (   a: List[ int ]) :
  return      37-a[42-u :  y**3]
def very_important_function(template: str,*variables,file: os.PathLike,debug:bool=False,):
    """Applies `variables` to the `template` and writes to `file`."""
    with open(file, "w") as f:
     ...
# fmt: off
custom_formatting = [
    0,  1,  2,
    3,  4,  5,
    6,  7,  8,
]
# fmt: on
regular_formatting = [
    0,  1,  2,
    3,  4,  5,
    6,  7,  8,
]
  • indent space마저 다른 절망적인 상황이다. 위에서 설정한 flake대로 linting하면 아래와 같은 현실을 마주한다.
> flake8 test.py
test.py:1:1: F401 'seven_dwwarfs.Grumpy' imported but unused
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
^
test.py:1:1: F401 'seven_dwwarfs.Happy' imported but unused
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
^
test.py:1:1: F401 'seven_dwwarfs.Sleepy' imported but unused
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
^
...(중략)

test.py:2:6: E201 whitespace after '{'
x = {  'a':37,'b':42,
     ^
test.py:2:11: E231 missing whitespace after ':'
x = {  'a':37,'b':42,
          ^
test.py:2:14: E231 missing whitespace after ','
x = {  'a':37,'b':42,
             ^
test.py:2:18: E231 missing whitespace after ':'
x = {  'a':37,'b':42,
                 ^
test.py:4:1: E128 continuation line under-indented for visual indent
'c':927}
^
test.py:4:4: E231 missing whitespace after ':'
'c':927}
   ^
test.py:8:4: F821 undefined name 'very_long_variable_name'
if very_long_variable_name is not None and \
 very_long_variable_name.field > 0 or \
 very_long_variable_name.is_debug:
   ^

...(하략)
  • 차라리 새로 짜는게 빠르겠다. 그레서 black 을 쓴다.
> black test.py
reformatted test.py

All done! ✨ 🍰 ✨
1 file reformatted.

2) 사용 옵션 값들

  • -l : 한 라인에 최대 글자 수 (초기값 88)

  • --diff : 파일을 변경하지 않고 변경되는 부분을 콘솔로 보여준다.

  • --color : --diff를 사용했을 때 변경점에 색을 입힌다.

  • 더 다양한 옵션은 공식 가이드 에서 확인할 수 있다.

  • black {파일명 또는 폴더명} -l 80 --diff --color 으로 코드 변경을 강제화 하지않고, 바뀌는 부분을 아래와 같이 체크가 가능하다.

3) 전역 사용 설정

  • .flake8 파일을 만드는 flake8과 같이, PEP518을 통해 .pyproject.toml file을 가지고 설정할 수 있다. 공식 홈페이지 설정 확인하기

  • 조금 다양한 설정 값들이 있는 .pyproject.toml여기 깃허브 링크 에서 체크 가능하다. 심플하게는 아래와 같이 사용할 수 있다.

[tool.black]
line-length = 88
target-version = ['py37', 'py38']
include = '\.pyi?$'
extend-exclude = '''
/(
  # The following are specific to Black, you probably don't want those.
  | blib2to3
  | tests/data
  | profiling
)/
'''
# We use preview style for formatting Black itself. If you
# want stable formatting across releases, you should keep
# this off.
preview = true

[project]
name = "black"
description = "The uncompromising code formatter."
requires-python = ">=3.7"
authors = [{ name = "Nuung", email = "hyeon.wo.dev@gmail.com" }]

4. VScode, Git과 섞어 사용하기

이제 위 code style linting을 vscode save, git commit 할 때 (git hook) 등을 활용해 강제화 해버리자!

1) vscode 에서 설정하기

  • 일단 당연하게 가상환경은 구축된 상태 또는 global한 pip install로 flake8, black이 설치된 상태에서 시작한다. F1 button으로 python linter 검색해서 .vscode > settings.js file을 만들자! 그리고 아래 그럼과 같이 세팅한다.

{
    "python.linting.enabled": true,
    "python.linting.pylintEnabled": false,
    "python.linting.flake8Enabled": true,
    "python.linting.lintOnSave": true,
    "python.formatting.provider": "black",
    "editor.formatOnSave": true
}

2) vscode - settings 기본 값

  • python.linting.enabled: Lint 기능을 유효화할 것인가?
  • python.linting.pylintEnabled: Linter에 pylint를 사용할 것인가?
    • 부족한 pylint 대신 flake8을 사용할 것이다.
  • python.linting.flake8Enabled: Linter에 flake8을 사용할 것인가
  • python.linting.lintOnSave: 파일 저장시에 Lint를 실행할 것인가?
  • python.formatting.provider: Python 코드의 정형 방식으로 무엇을 사용할 것인가?
  • editor.formatOnSave : 파일 저장시에 자동 정형을 사용할 것인가?
  • 추가로 아래와 같은 설정을 추가해 flake8 과 vscode의 linting 값을 추가할 수 도 있다.
"python.linting.flake8Args": [
  	"--config=django_all_about/.flake8",
    "--max-line-length=120",
    "--ignore=W291",
],
  • --config 값과 같이 .flake8 과 같은 linting config file이 root directory에 존재하지 않는다면 꼭 명시해줘야 한다. 그렇지 않으면 린팅 옵션이 default로만 세팅되어서 [ 왜 안돼!? ] 와 같은 물음이 반복될 것이다.

3) git hook - pre-commit 설정하기

  • Git 에는 특정 이벤트에 특정 스크립트를 실행하는 Hook 이라는 기능이 있다. 이 중 커밋하기 전에 실행되는 훅인 pre-commit 에서 Lint를 하는 작업을 하게 할 수 있다. pip install pre-commit 으로 pip lib을 설치하자. (전역적으로 brew install pre-commit 을 사용할 수 도 있다.)

  • pre-commit sample-config > .pre-commit-config.yaml 으로 sample 기반으로 .pre-commit-config.yaml file을 만들어주자. 기본 내용은 아래와 같다. (다양한 hook 설정값 확인하기)

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files
  • flake8 + black을 pre-commit에 config 하기위해 아래와 같이 세팅한다. 각 설정값은 공식 페이지에서 조금씩 차용해서 가져왔다. (버전은 대충 설정해도 된다. 바로 다음에 할 update 통해 알아서 세팅 된다.)
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: check-yaml
      - id: check-json

  - repo: https://github.com/psf/black
    rev: 22.12.0
    hooks:
      - id: black
        name: black
        description: "Black: The uncompromising Python code formatter"
        entry: black
        language: python
        minimum_pre_commit_version: 2.9.2
        require_serial: true
        types_or: [python, pyi]

  - repo: https://github.com/PyCQA/flake8
    rev: 6.0.0
    hooks:
      - id: flake8
		args: ["--config=django_all_about/.flake8"]      
        name: flake8
        description: "`flake8` is a command-line utility for enforcing style consistency across Python projects."
        entry: flake8
        language: python
        types: [python]
        require_serial: true
  • 이렇게 설정한 뒤 pre-commit autoupdate 를 통해서 우리가 세팅한 .pre-commit-config.yaml file을 알맞게 버전 세팅을 해주자!

  • 참고로 위 .vscode > setting.json 때와 같이 flake8의 conf파일이 root에 없으면 pre-commit conf에서도 args 를 줘서 저렇게 경로 명시를 꼭 해줘야 한다.

  • 이게 끝이 아니라 실제 commit 을 할 때마다 위 파일이 pre-commit 단계에 실행되도록 pre-commit install 를 실행해주자! 그러면 pre-commit installed at .git/hooks/pre-commit 라고 나온다.

  • 최초 실행시에는 env setting을 위한 intalling 이 있어서 시간이 좀 걸린다. 해당 작업이 다 된다면 아래와 같이 나온다.


마무리

이제 [ vscode (IDE) linting config + flake8 & black 전역설정 + pre-commit ] 으로 누구나 어떤 코드를 짜던 한 사람이 짠 것 같은 코드 스타일을 추구할 수 있게 되었다.

  • 하지만 이런 설정은 강제가 되어서는 안된다. PEP8에서도 "절대적으로 따르는 것은 멍청하다" 라는 표현을 한다. 가독성을 살리면서 창의적이고 다른 훨씬 좋은 표현방법을 찾을 수 있도록 해야한다.

  • 하지만 현업에서 쓰는 코드는 본인의 일기장이나 연습장이 아니다. 적어도 지켜야하는 최소한의 규칙은 지키고 유지 보수를 위해 뒤를 위해 읽히는 코드를 작성하자.

  • 마무리로 이번에 ifkakao의 "섬세한 ISFP의 코드 가독성 개선 경험" 글을 추천한다.


출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!
post-custom-banner

0개의 댓글