본문 바로가기

SW/ETC

[Python] 파이썬 패키지 만들기, 테스트(unittest), 빌드, PyPI로 배포

1. 패키지 만들기

 

(0) 매뉴얼

www.packaging.python.org/tutorials/packaging-projects

를 참고하였다.

 

 

(1) 디렉토리 구조 설계

ROOT
├── setup.py
├── requirements.txt
├── LICENSE
├── README.md
└── package
   ├── __init__.py
   └── module.py

위와 같은 디렉토리 구조를 가진 패키지를 만들 었다고 가정하자.

 

 

(2) 코드 짜기

 

/package/__init__.py

# /package/__init__.py
"""
Description for Package
"""

from package.module import Example_class, method
# from package import * 로 써도 된다.

__all__ = ['module']

# 패키지 버전 정의
__version__ = '0.0.1' 

 

/package/module.py

# /package/module.py

class Example_class():
	def __init__(self):
		print("example_class instance loaded")
		self.attribute=False
    
	def example_method1(self):
		print("example_method executed")
 		self.attribute=True
		return self.attribute
    
	def example_method2(self):
		print("example_method2 executed")
		if self.attribute==True:
		 	raise Exception('example_method1 was already executed')
		else:
			return self.example_method1()
            
def method():
	print("method executed")

인스턴스를 생성하면 생성됨을 출력하고, attribute에 False를 넣는다.

인스턴스의 method1을 실행하면 실행됨을 출력하고, attribute에 True를 넣고, attribute를 반환한다.

인스턴스의 method2를 실행하면 실행됨을 출력하고, method1이 이미 실행되었다면 exception raise하고, 실행되지 않았다면 method1을 호출한다.

 

 

(3) Setup 하기

 

/setup.py

# /setup.py
import setuptools

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name="example-pkg-YOUR-USERNAME-HERE", # Replace with your own username
    version="0.0.1",
    author="Example Author",
    author_email="author@example.com",
    description="A small example package",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
)

이름, 버전, 저자, 저자메일, 짧은 설명, 긴 설명, url, 사용하는 파이썬 패키지, 구별자, 파이썬 최소 버전 를 등록한다.

 

setuptools.setup(
    ...,
    install_requires=["aiohttp", "another_project_name[extras_require_key]"],
    extras_require={
        'PDF':  ["ReportLab>=1.2", "RXP"],
        'reST': ["docutils>=0.3"],
    },
    ...
    )

install_requires는 프로젝트가 항상 필요로 하는 의존성 패키지 이므로 필수 설치 하고,

extras_require는 프로젝트가 특정 상황에 필요로 하는 의존성 패키지이므로 설치하지 않고 넘어간다.

install_requires에 다른 프로젝트의 이름을 넣고, 그 프로젝트의 extra 의존 패키지의 key를 넣으면 함께 다운로드 받는다.

 

 

setuptools.setup(
    ...,
    entry_points={
        'console_scripts': [
            'shortcut1 = package.module:func',
        ],
        'gui_scripts': [
            'shortcut2 = package.module:func',
        ]
    },
    ...
)

entry_points를 설정해주면 bash shell이나 exe에서 곧바로 실행할 수 있도록 shortcut을 자동으로 설정해준다.

보통은 가장 처음에 시작해야하는 main function이나, 옵션을 바꿀 때 사용하는 function을 이용한다.

 

setuptools.setup(
    ...,
    test_suite=['tests.test_module.suite'],
    ...
)
    

추후 unittest를 이용해 설정한 테스트 묶음 함수를 넣어주면 python setup.py test 옵션에서 동작한다.

 

 

 

/README.md

# Example Package

This is a simple example package. You can use
[Github-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
to write your content.

Github 등 에서 메인에 보일 페이지에 쓰일 내용을 적는다.

 

 

/LICENSE

Copyright (c) 2018 The Python Packaging Authority

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

사이트 링크  에서 라이센스를 골라 적는다.

위는 MIT 라이센스의 예시다.

 

/requirments.txt

 

현재 패키지를 실행하는데 필요한 패키지들을 나열한다.

setup.py에서 install_requires를 설정했을 경우에는 필요 없다.

만약 requirments.txt로 필요 의존성 패키지를 설정했을경우, 추후 다음 명령어로 설치할 수 있다.

$ pip install -r requirements.txt

 

 

2. 패키지 테스트

 

(1) TDD란?

Test Driven Development의 약자.

개발자가 직접 하는 테스트라는 뜻.

 

(2) unittest란?

TDD를 도와주는 패키지. 내장되어있으므로 따로 설치할 필요 없다.

 

https://docs.python.org/ko/3/library/unittest.html

위 매뉴얼을 참고하면서 썼음.

 

(3) 테스트 코드 작성

ROOT
├── setup.py
├── requirements.txt
├── License
├── README.md
└── package
   ├── __init__.py
   ├── module.py
└── tests
   ├── __init__.py
   └── test_module.py

tests 디렉토리와, 그 안의 __init__.py 파일, test_module.py를 작성했다.

 

 

/tests/__init__.py

 

아무 내용도 적지 않아도 된다.

__init__.py 파일이 존재하는 것 만으로 패키지 형태로 인식되기 때문.

 

 

/tests/test_module.py

# /tests/test_module.py
import unittest
from package import module

class TestClass(unittest.TestCase):
    def SetUp(self):
        self.instance = module.Example_class()
        
    def tearDown(self):
        self.instance.dispose()
        # del self.instance
        
    def test_result(self):
        result=self.instance.example_method1()
        self.assertEqual(result, True)
        
    def test_raise_exception(self):
        self.assertRaises(Exception, lambda: self.instance.example_method2())

SetUp과 tearDown 메서드는 unittest 패키지에서 자체적으로 호출하는 메서드로,

테스트 메서드 호출 이전에 SetUP, 호출 이후에 tearDown를 호출하도록 되어있다.

메서드보다 큰 단위로, 테스크 클래스 호출 이전/이후에 호출되는 SetUpClass(), tearDownClass() 가 있다.

 

test_result 메서드는

module의 Example_class의 example_method 결과로 받은 반환값 result를

TestCase 클래스의 assertEqual 메서드를 이용하여 테스트한다.

 

test_raise_exception 메서드는

module의 Example_class의 example_method2를 실행하는 중 Exception이 일어나는지를

TestCase 클래스의 assertRaises 메서드를 이용하여 테스트한다.

 

테스트시 테스트 메서드의 실행 순서는 코드 순서가 아니라 메서드 이름 정렬 순서에 의해 결정된다.

위의 코드는 SetUp>test_raise_exception>test_result>tearDown 순으로 실행된다.

 

test_raise_exception이 Exception을 정상적으로 일으키기 위해선

SetUp>test_result>test_raise_exception>tearDown이 되어야 하므로,

테스트 메서드의 이름을 변경해야한다.

 

unittest 패키지(모듈)의 TestCase 클래스가 제공하는 boolean 관련 assert Methods는 아래와 같다.

unittest 패키지(모듈)의 TestCase 클래스가 제공하는 Exception 관련 assert Methods는 아래와 같다.

unittest 패키지(모듈)의 TestCase 클래스가 제공하는 대소비교 관련 assert Methods는 아래와 같다.

unittest 패키지(모듈)의 TestCase 클래스가 제공하는 기타 자료형 관련 assert Methods는 아래와 같다.

 

(4) 테스트

 

test_module.py를 구현한 뒤, 다음 명령어를 실행함으로써 쉽게 테스트하고, 오류의 위치와 원인을 알 수 있다.

$ python -m unittest # 전체 테스트
$ python -m unittest tests/test_module.py # 특정 모듈 테스트
$ python -m unittest tests/test_module.Test # 특정 클래스 테스트
$ python -m unittest tests/test_module.Test.test_method # 특정 메서드 테스트
$ python -m unittest -v test_module # 상세정보(verbose) 모드

$ python -m unittest tests/test_module1 tests/test_module2 # 여러개의 모듈 테스트

 

또는 단순하게 test_module.py 를 실행해서 테스트하고싶다면, test_module.py의 마지막에 다음과 같이 코드를 써주면 된다.

# /tests.test_module.py

class Test(unittest.TestCase):
  """
  생략
  """


if __name__ == "__main__":
    unittest.main()
    #unittest.main(verbosity=2)

verbosity 옵션을 넣으면 verbose 모드로 실행할 수 있다.

 

$ python tests/test_module.py

-m unittest 라는 명령어를 생략할 수 있다.

 

 

만약 여러 번의 테스트를 따로 실행하고 싶으면, TestSuite를 쓰면 좋다.

def suite():
    suite = unittest.TestSuite()
    suite.addTest(TestClass('test_result'))
    suite.addTest(TestClass('test_raise_exception'))
    return suite

if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(suite())

TestSuite와 관련하여 SetUp을 쓰고 싶다면 SetUpClass, SetUpModule의 사용을 고려해봐야한다.

자세한 내용은 매뉴얼을 살펴보자.

 

 

3. 패키지 빌드, 배포

 

(1) 설치

 

$ python setup.py --help-commands

Standard commands:
  build             build everything needed to install
  build_py          "build" pure Python modules (copy to build directory)
  build_ext         build C/C++ and Cython extensions (compile/link to build directory)
  build_clib        build C/C++ libraries used by Python extensions
  build_scripts     "build" scripts (copy and fixup #! line)
  clean             clean up temporary files from 'build' command
  install           install everything from build directory
  ...

루트 디렉토리에서 help옵션으로 커맨드를 살펴보면 Standard commands를 볼 수 있다.

그 중 가장 자주 사용하는 것은 다음 세 가지다.

$ python setup.py install
$ python setup.py build
$ python setup.py test

install 옵션을 사용하면, 로컬에서 제작한 패키지를 환경변수 설정되어있는 패키지 디렉토리에 설치할 수 있다.

만약 기본 패키지 디렉토리가 아닌 로컬에 설치하고싶다면, develop 옵션을 사용할 시 egg-info 파일로 생성할 수 있다.

*pip install -e ~~~ 로하면 루트에 생성된다.

test 옵션은 setup.py에서 설정했던 test_suite의 함수를 실행하는 내용이다.

 

빌드에 관해선 build 옵션을 이용하기 보다는 다음을 참고하자.

 

 

 

(2) 빌드

 

setup.py가 있는 루트 디렉토리에서 다음을 실행하자

$ python3 setup.py sdist bdist_wheel

sdist는 Source Distribution의 약자로, 현재 루트 디렉토리를 통채로 압축한 tar.gz 포맷으로 배포하겠다는 의미다.

bdist는 Build Distributionn의 약자로, 빌드(컴파일)을 완료한 파일을 배포하겠다는 의미다. 해당 파일은 바이너리일 수도, 아닐 수도 있으나 최소한 바로 실행가능한 파일상태로 나타난다.

bdist의 옵션으로 wheel과 egg가 있는데, 요즘엔 wheel이 국룰이라고 한다.

 

dist/
  example_pkg_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
  example_pkg_YOUR_USERNAME_HERE-0.0.1.tar.gz

dist 디렉토리에 위와 같이 .whl 파일과 tar.gz이 생긴다.

dist 의 경로는 다음과 같다.

 

리눅스 : /usr/local/lib/파이썬버전/site-packages/

맥 : /Library/Frameworks/Python.framework/Versions/버전/lib/파이썬버전/site-packages/

 

 

(3) PyPI로 패키지 배포하기

 

1) pip 업그레이드

$ pip --version // 버전 확인

$ pip install -U pip // 맥, 리눅스
$ python3 -m pip install --user --upgrade setuptools wheel // 윈도우

pip가 안되면 pip3로 하자.

 

 

2) Test PyPI 가입, 토큰 얻기

 

가입 링크

API 토큰 링크

Don’t close the page until you have copied and saved the token — you won’t see that token again.

토큰은 두번 다시 못보니 꼭 저장하고서 닫아라

 

시키는대로 하면 토큰이 나온다.

 

3) Twine 설치

$ python3 -m pip install --user --upgrade twine // 윈도우
$ pip install twine // 맥, 리눅스

 

4) dist 등록

$ python3 -m twine upload --repository testpypi dist/*
$ twine upload --repository testpypi dist/*

ID와 PW를 입력하라고 할 텐데,

ID는 토큰을

PW는 pypi-를 붙인 토큰을 입력하면 된다.

 

5) 등록이 잘 되었는지 확인

$ python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps example-pkg-YOUR-USERNAME-HERE
$ pip install --index-url https://test.pypi.org/simple/ --no-deps example-pkg-YOUR-USERNAME-HERE // 맥, 리눅스

--index-url은 정식 pip 서버가 아닌 test 서버에 올렸기 때문에 붙였으며,

--no-deps는 다른 의존성 문제와 충돌하더라도 강제 설치하는 옵션이다.

 

7) 정식 pip에 올리려면?

 

 - 이름을 독특하게 잘 짓자.

 - https://pypi.org 에 가입하자

 - twine upload dist/* 로 업로드하자. (--repository testpypi 옵션 생략)

 - pip install 패키지 이름 으로 설치 가능.