::: IT인터넷 :::

파이썬 프로젝트 폴더 구성하기

곰탱이푸우 2021. 9. 2. 08:20

이전 포스팅에서 PyCharm과 VS Code를 사용한 개발 환경 구성을 진행했다.

 

PyCharm과 VS Code를 사용한 개발 환경 구성은 다음 포스팅을 참고한다.

 

파이썬 개발 환경 구성 (+ PyCharm)

우분투 20.04 LTS (focal)에서 Anaconda, PyCharm (Community Edition)을 사용한다. 다음 순서로 진행한다. APT 설치 패키지 최신화 Anaconda 설치 Python 환경 설정 PyCharm 설치 PyCharm 설정 변경 PyCharm 프로..

www.bearpooh.com

 

파이썬 개발 환경 구성 (+ VS Code)

우분투 20.04 LTS (focal)에서 Anaconda, Visual Studio Code를 사용한다. 일반적인 패키지 단위 (Wheel) 개발은 IntelliJ의 PyCharm이 편리하다. 그러나 테스트를 위한 코드 스니펫과 단순한 스크립트의 작성과..

www.bearpooh.com

 

이전에 포스팅한 Visual Studio C++ 프로젝트 폴더 구성하기와 마찬가지로 파이썬도 프로젝트 폴더 구성을 해야 한다.

 

Visual Studio C++ 프로젝트 폴더 구성하기는 다음 포스팅을 참고한다.

 

Visual C++ 프로젝트 폴더 구성하기

NAS에 개발 환경을 구축하면서 약 7년 전에 MFC 공부하면서 만들었던 MD5를 추출하는 GUI 툴을 테스트 프로젝트로 활용했다. Visual Studio 2008의 Visual C++과 MFC를 사용해서 만들었다. Visual Studio Express..

www.bearpooh.com

 

이러한 프로젝트 구조는 다음과 같은 장점이 있다.

  • 형상 관리에 도움이 된다. 특히 프로젝트의 규모가 커질 수록 더욱 도움이 된다.
  • 여러 명이 함께 작업하는 경우 일관 된 소스 코드 구조를 유지할 수 있다.
  • 이미 정의 된 형식을 따르므로 빌드와 배포 작업이 용이하다.

 

프로젝트 구조 정의는 이전에 개발팀에 있을때 경험하고 사용하던 프로젝트 템플릿을 중심으로 정의했다.

해당 템플릿은 파이썬 프로젝트 구성의 Best Practice를 참고하여 정의되었기 때문에, 일반적으로 알려진 구조와 거의 비슷하다.

 

파이썬 프로젝트 구성의 Best Practice는 다음 포스팅을 참고한다.

 

Structuring Your Project — The Hitchhiker's Guide to Python

 

docs.python-guide.org

 

다음 순서로 진행한다.

  • 전체 구조
  • 최상위 폴더
  • 소스코드 폴더
  • 테스트 폴더

 

 

전체 구조

파이썬 빌드 테스트를 위해 간단한 예제 프로그램을 만들었다.

이메일 발송을 위해 yml 파일에 정의 된 설정 값을 읽고 적절하게 변환하여 화면에 출력하는 프로그램이다.

빌드 테스트가 주 목적이므로 실제 발송은 하지 않고 이메일 관련 모듈도 사용하지 않았다.

 

전체 구조는 다음과 같다.

\root                        # 파이썬 프로젝트의 최상위 폴더
    \.gitignore              # git 커밋할때 제외할 파일과 폴더 정의
    \README.md                # git의 프로젝트 첫 화면에 출력할 파일
    \LICENSE                  # 해당 프로그램의 라이선스 정보
    \requirements.txt        # 해당 프로그램을 설치할 때 필요한 라이브러리 (Dependency)
    \setup.py                # 빌드, 배포를 위해 필요한 설정
    \test_requirements.txt    # 해당 프로그램을 테스트할 때 필요한 라이브러리 (Dependency)
    \패키지명                  # 해당 프로그램이 수행할 기능을 정의한 소스코드 폴더
        \__init__.py          # 해당 디렉터리가 파이썬 패키지의 일부임을 알려주는 역할
        \패키지명.py          # 해당 프로그램의 전체 기능을 정의하는 소스코드 (main)
        \info.py              # 배포에 사용할 패키지 이름과 버전
        \부가기능.py          # 해당 프로그램을 수행하는데 필요한 부가 기능을 정의한 소스코드
        \resources            # 해당 프로그램을 수행하는데 필요한 정보들을 정의한 폴더
            \__init__.py      # 해당 디렉터리가 파이썬 패키지의 일부임을 알려주는 역할
            \config.yml      # main.py를 수행하는데 필요한 정보들을 정의한 파일
    \tests                    # 소스코드에 정의 된 기능들에 대한 단위(유닛) 테스트를 정의한 폴더
        \__init__.py          # 해당 디렉터리가 파이썬 패키지의 일부임을 알려주는 역할
        \test_부가기능.py      # 각 기능별 테스트를 수행하는 소스 코드
        \resources            # 테스트 수행에 필요한 정보들을 정의한 폴더
            \config.yml      # 테스트를 수행하는데 필요한 정보들을 정의한 파일

 

.gitignore

gitlab이나 github, bitbucket 같이 git 기반의 형상 관리에서 소스코드를 Commit 할때 불필요한 파일을 제외하는데 사용한다.

  • Commit 할때 포함하지 않는 경우도 있다.
  • 최초 Skeleton (프로젝트 기본 구조)을 생성할 때 넣어주면 불필요한 파일을 초기부터 제외할 수 있다.
  • IDE (통합개발환경) 도구 관련 파일, 테스트 결과 파일, 빌드 과정 중 생성 된 파일들을 제외한다.

 

예제로 생성한 파이썬 프로젝트에서는 다음과 같이 정의했다.

# Ignore Files
/.pytest_cache/  # 단위 테스트를 위한 pytest를 실행하면 생성되는 cache 폴더
/__pycache__/    # 단위 테스트와 빌드 과정 중 py 파일이 실행되면 생성되는 pyc 파일 저장 폴더
*.pyc*           # 단위 테스트와 빌드 과정 중 py 파일이 실행되면 생성되는 pyc 파일
*.egg*           # 파이썬 프로젝트 빌드 과정 중 생성되는 코드, 리소스 및 메타 데이터를 정의한 폴더
/dist/           # 최종 산출물인 whl 패키지 파일이 저장되는 폴더
/build/          # 파이썬 프로젝트 빌드에 사용 된 파일이 저장 된 폴더
/.idea/          # PyCharm으로 소스코드 작성하면 생성되는 폴더
*.DS_Store*      # MacOS에서 해당 폴더의 메타 데이터를 정의한 파일 (Desktop Services Store)

 

최상위 폴더에 .gitignore 파일을 생성하고 위와 같이 작성하면 Commit 할때 해당 파일과 폴더들은 제외된다.

 

 

주의할 점은 불필요한 파일들이 이미 Commit 된 경우에는 제외되지 않는다.

이미 Commit 된 경우에는 불필요한 파일들을 모두 삭제하고 다시 Commit 해야 한다.

 

.gitignore 관련 문법과 사용법은 다음 사이트를 참고한다.

 

Git - gitignore Documentation

The optional configuration variable core.excludesFile indicates a path to a file containing patterns of file names to exclude, similar to $GIT_DIR/info/exclude. Patterns in the exclude file are used in addition to those in $GIT_DIR/info/exclude.

git-scm.com

 

아래 사이트에서는 다양한 개발 언어의 .gitignore 템플릿을 참고할 수 있다.

 

GitHub - github/gitignore: A collection of useful .gitignore templates

A collection of useful .gitignore templates. Contribute to github/gitignore development by creating an account on GitHub.

github.com

 

개발 환경과 언어에 따라 자동으로 .gitignore를 생성하려면 아래 사이트를 사용한다.

 

gitignore.io

Create useful .gitignore files for your project

www.toptal.com

 

gitlab에서 프로젝트 생성과 소스코드 관련 작업을 하는 방법은 다음 포스팅을 참고한다.

 

gitlab에서 group, project 생성하기

gitlab을 설치하는 과정은 이전 포스팅에서 진행했다. 시놀로지 NAS에서 GitLab 설정 방법 개요 시놀로지 NAS에서 GitLab도 설치 가능하며, DS21x 모델 기준으로 DS216 부터 추가되었다. 아마도 Docker를 DS216

www.bearpooh.com

 

 

gitlab에서 소스코드 작업하기 (clone, checkout, commit, push)

project와 branch까지 만들었으면 코드 개발을 위해 실제 개발 환경에 코드 저장소를 clone 해야 한다. gitlab에서 project 와 branch 생성하는 것은 아래 포스팅을 참고한다. gitlab에서 group, project 생성하..

www.bearpooh.com

 

 

README.md

설치 방법, 사용법, 라이선스, 소스 코드 설명, 추가 설명 사항 등을 간단하게 기록하는 파일이다.

git 기반의 형상 관리 시스템에서 해당 프로젝트를 소개하는데 사용한다.

 

Markdown 언어를 사용하며 관련 내용은 아래 포스팅을 참고한다.

 

README.md

README.md 파일은 git, github 등과 같이 저장소에서 많이 본 파일입니다. 해당 파일은 소스코드에 앞서 어떠한 목적으로 개발이 되었는지, 코드의 개요, 구조도 등을 처음 사람들에게 노출함으로써

brownbears.tistory.com

 

예전 개발 부서에 있을 때에는 주로 내부에서 작성하는 프로그램을 작성해서 주로 다음 내용을 중심으로 생성했다.

# 프로그램 이름

### 개요
프로그램에 대한 간단한 소개

### 설치 방법
해당 프로그램을 설치하는 방법에 대한 소개
파이썬 패키지의 경우 pip install 내용 작성

### 실행 방법
해당 프로그램의 각 기능별 사용 방법 소개
실행 예시와 결과도 포함하면 더 좋음

### 참고 사항
해당 프로그램 관한 참고 사항
- 개발 관련 문서 URL
- 외부 참고 문서 URL
- 향후 일정 관련 내용
- 오류 관련 Known-issue

 

requirements.txt

해당 프로그램을 설치할 때 필요한 (Dependency) 라이브러리들을 정의한다.

패키지명==버전 으로 정의하며, setup.py에서 사용한다.

 

pip 명령어를 사용하여 requirements.txt에 정의 된 라이브러리들을 한번에 설치할 수 있다.

$ pip install -r requirements.txt

 

이름은 반드시 requirements.txt로 해야 하는 것은 아니지만 requirements.txt로 하는 것을 추천한다.

  • 임의의 파일명을 사용하는 경우 setup.py에서 해당 파일명으로 지정하면 된다.
  • Best Practice에서 requirements.txt로 사용하고 있고, 다수의 프로젝트에서 requirements.txt로 사용중이다.
  • 이런건 대세를 따르자.

 

 

test_requirements.txt

해당 프로그램을 테스트할 때 필요한 (Dependency) 라이브러리들을 정의한다.

 

내용과 사용법은 requirements.txt와 동일하다.

 

setup.py

프로젝트의 최상위 디렉토리에 위치하며, 프로젝트의 테스트, 빌드, 배포에 필요한 정보들을 정의한다.

setuptools라는 패키지를 사용하며 다음 명령어로 설치한다.

$ pip install setuptools

 

setup.py는 정보들을 정의한다.

  • name - 해당 파이썬 패키지의 이름
  • version - 해당 패키지의 배포 버전
  • description (한줄) / long_description (README.md) - 패키지에 대한 설명
  • packages - 프로젝트에 포함되는 패키지 리스트 (find_packages 함수로 __init__.py가 포함 된 모든 폴더 탐색)
  • install_requires - 해당 패키지를 설치할 때 필요한 패키지 목록 (requirements.txt 활용)
  • setup_requires - python setup.py를 실행할때 먼저 설치되어 있어야 하는 패키지 목록 (즉, 빌드에 필요한 패키지 목록) 
  • tests_require - 테스트에 사용하는 패키지 목록
  • python_requires - 해당 패키지를 실행하기 위해 필요한 파이썬의 최소 버전
  • entry_points - 해당 패키지 설치 후 쉘 (Shell) 에서 실행 가능한 명령어와 실행할 함수 지정
  • classifiers - PyPi (또는 사설 PyPi)에 등록될 메타 데이터를 설정한다. (실행 환경, 버전, GUI 여부 등)

 

예제 프로젝트에서 사용한 setup.py는 다음과 같다.

from setuptools import setup, find_packages
from srtest.info import __package_name__, __version__

with open('README.md', 'r', encoding='utf-8') as f:
    readme = f.read()
with open("requirements.txt", "r", encoding="utf-8") as f:
    requires = f.read().splitlines()
with open("test_requirements.txt", "r", encoding="utf-8") as f:
    test_requires = f.read().splitlines()

setup(
    name=__package_name__,
    version=__version__,
    long_description=readme,
    packages=find_packages(exclude=["contrib", "docs", "tests"]),
    package_data={'': ['*.yaml', '*.yml']},
    install_requires=requires,
    setup_requires=[
        "pytest-runner",
    ],
    tests_require=test_requires,
    python_requires=">3.6",
    entry_points={
        "console_scripts": ["srtest-python=srtest.main:main"]
    },
    classifiers=[
        'Environment :: Console',
        'Operating System :: POSIX :: LINUX',
        'Programming Language :: Python :: 3.6'
    ]
)

 

name, version

srtest 폴더의 info.py에 정의 된 __package_name__과 __version__을 import하여 name과 version을 지정했다.

소스 코드에서 해당 패키지 이름과 버전 정보를 사용해야 하는 경우에는 이러한 구조가 편리하다.

소스코드에서 사용할 일이 없다면 name과 version에 직접 기입하는 것이 가독성, 직관성에 더 좋다.

 

entry_points

entry_points 옵션에 console_scripts로 srtest-python=srtest.main:main으로 지정했다.

  • 해당 패키지를 설치하면 명령창에서 srtest-python으로 실행할 수 있다.
  • srtest-python 명령을 실행하면 srtest 폴더의 main.py 내에 정의 된 main 함수가 실행된다.

 

 

소스코드 폴더

해당 프로그램이 수행할 기능을 정의한 소스 코드가 저장 된 폴더이다.

 

__init__.py

해당 디렉터리가 파이썬 패키지의 일부임을 알려주는 역할을 한다.

해당 파일이 없다면 패키지의 구성 요소로 인식되지 않는다.

python3.3 버전부터는 __init__.py 파일이 없어도 패키지로 인식한다고 하는데, 하위 호환을 위해 유지하는 것이 권장된다.

 

보통 파일 내용은 비워두는 경우가 많지만, import 할 항목을 지정할 수도 있다.

자세한 내용은 다음을 참고한다.

https://wikidocs.net/1418#9595init9595py

 

info.py

__package_name__과 __version__을 정의해서 사용한다.

__package_name__ = 'srtest-python'
__version__ = '1.0.0.dev0'

 

setup.py에서 해당 파일을 import하여 name과 version을 지정할 수 있다.

 

이러한 구조가 편리한 경우는 다음과 같다.

  • 소스 코드에서 해당 패키지 이름과 버전 정보를 사용해야 하는 경우
  • 버전 정보 변경이 빈번하여 빠르게 수정하고자 하는 경우

위의 경우에 해당하지 않으면 setup.py에 직접 기입하는 것이 더 낫다.

 

패키지이름.py (또는 main.py)

패키지의 전체 기능을 정의하는 main 소스 코드이다.

보통 해당 패키지 이름으로 정의하는 것이 권장 사항이며, 필요에 따라 main.py로 지정하는 것이 더 직관적일 수 있다.

 

주로 해당 패키지의 주 사용 목적에 따라 구분된다.

  • main.py로 지정 - 해당 패키지를 주로 실행 목적으로 사용 것이 주 용도인 경우
  • 패키지 이름으로 지정 - 다른 패키지에 라이브러리로 사용하는 것이 주 용도인 경우

 

 

일반적으로 아래와 같은 구조로 작성한다.

import ~~~~

def 패키지이름():
    ~~~~

if __name__ == "__main__":
    패키지이름()

 

부가기능.py

해당 프로그램의 기능을 작성하면서 특정 부분 또는 세부 기능을 별도의 py 파일로 분리한 경우에 해당한다.

 

예제 프로젝트에서는 전달 받은 yml 파일의 경로로 내용을 읽고, 이메일 전송에 필요한 항목을 생성하기 때문에 emailclient.py로 지정했다.

보통 별도로 분리한 부분의 목적이나 기능을 표현할 수 있는 이름을 부여한다.

 

해당 파일은 패키지의 전체 기능을 정의하는 패키지이름.py나 main.py에서 import 하여 사용한다.

 

resources (또는 별도 이름 지정)

해당 프로그램의 기능을 작성하면서 사전에 정의한 기본 설정 값이나 데이터를 사용할 때 유용하다.

소스 코드와 설정 파일들을 분리하기 위해 resources라는 폴더로 분리한다.

 

일반적으로 실행에 필요한 설정 값은 명령창에서 --옵션 설정값 형태로 전달한다.

$ python train.py --epochs 50 --batch-size 64 --save-dir weights

 

하지만, 옵션이 많은 경우 모두 기입하기에는 불편한 경우가 많다.

 

resource에 기본 설정 값을 사전에 정의하여 사용하면 불편함을 최소화 할 수 있다.

  • 코드 작성할때 시작 부분에 실행 초기에 yml 파일에 정의한 값들을 기본 값으로 설정하도록 한다.
  • 사용자가 직접 입력한 설정 값들을 파싱하여 해당 항목의 값을 변경한다.

 

 

테스트 폴더

소스코드에 정의 된 기능들에 대한 단위(유닛) 테스트를 정의한 폴더이다.

 

다양한 유닛 테스트 라이브러리들이 있는데, 기존에 사용하던 pytest를 계속 사용한다.

pytest 사용법은 다음 사이트를 참고한다.

 

pytest: helps you write better programs — pytest documentation

pytest: helps you write better programs The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries. An example of a simple test: # content of test_sample.py def inc(x): return x

docs.pytest.org

 

보통 테스트 할 임의의 값을 사용하여 정의한 기능을 호출하고 결과에 이상이 없는지 판단한다.

  • 의도한 예외가 정상적으로 발생한다.
  • 의도한 값을 정상적으로 반환한다.
  • 의도하지 않은 값을 의도한 대로 처리한다.

 

테스트 코드 작성 방법은 사용하는 테스트 라이브러리마다 다르기 때문에 해당 기술 문서를 참고한다.

 

__init__.py

소스코드 폴더 항목의 __init__.py를 참고한다.

테스트 코드에서 소스 코드의 함수나 클래스를 import 하거나 호출하기 위해 필요하다.

 

test_패키지명.py (또는 test_main.py)

일반적으로 main 기능에 대해서는 테스트를 수행하지 않지만, 내부에 정의한 함수의 테스트가 필요할 경우 작성한다.

main 부분에서 수행할 테스트가 존재하지 않아 작성하지 않았다.

 

파일명은 test_를 prefix로 지정하고, 테스트 대상이 되는 파일의 이름을 추가한다.

 

test_부가기능.py

해당 프로그램의 기능을 작성하면서 특정 부분 또는 세부 기능을 별도의 py 파일로 분리한 경우에 해당한다.

별도로 분리한 기능이 정상적으로 작동하는지 테스트 하기 위한 파일이다.

 

 

파일명은 test_를 prefix로 지정하고, 테스트 대상이 되는 파일의 이름을 추가한다.

 

pytest의 경우 다음과 같은 형식으로 테스트 코드를 작성한다.

# -*- coding: utf-8 -*-#

import pytest
from freezegun import freeze_time  # 테스트를 위해 고정 된 시간 값 사용


class Test부가기능:
    @pytest.fixture(scope="function", autouse=True)
    def setup_teardown(self, request):
        # 테스트에 필요한 변수 (경로, 초기값 등) 정의
        # 아래는 예제 코드
        test_folder = os.path.dirname(os.path.realpath(__file__))
        self.test_conf_file = os.path.join(test_folder, "resources", "config.yml")
        with open(self.test_conf_file, 'r', encoding='utf-8') as yaml_file:
            self.yaml_dict = yaml.load(yaml_file, Loader=yaml.Loader)
        # 테스트가 종료 된 이후 수행 될 코드 정의 (teardown)
        # fixture의 scope가 function이므로 각 테스트 함수가 종료될때마다 실행
        # 테스트를 종료할 때 수행하는 작업이 없으면 포함하지 않아도 됨
        def teardown():
            print('Function Tear down - - - - ')
        request.addfinalizer(teardown)
        
    def test_함수1(self):
        # 정상 조건 또는 예외 조건에 대한 변수 정의
        # 정의한 변수를 부가기능의 해당 함수에 인자로 전달하여 호출
        # 결과값 비교 (논리연산자는 >, <, ==, >=, <= 등)
        assert 결과값 논리연산자 기대값
        
        # 정상 조건, 예외 조건 케이스별로 반복
        
    ### 테스트 하려는 함수에 대한 테스트 추가
    
    # 아래는 예제 코드
    @freeze_time(datetime(2020, 3, 25, 10, 25, 22))
    def test_set_curr_time(self):
        """
        set_curr_time을 통해 현재 시각을 제대로 기록하는지 확인한다.
        """
        test_cls = EmailClient()
        test_cls.set_curr_time()
       
        assert str(test_cls.curr_time)[0:19] == "2020-03-25 10:25:22"

 

resources

해당 프로그램의 기능을 테스트하면서 사용할 설정 값이나 데이터를 정의할때 사용한다.

보통 소스코드 하위의 resource에 정의한 기본 설정 값과 동일한 형태로 작성하고, 테스트에 필요한 값으로 변경한다.

 

테스트 코드와 설정 파일들을 분리하기 위해 resources라는 폴더로 분리한다.

 

나머지 사항은 소스코드 폴더의 resouces 항목과 동일하다.