Skip to content

Setuptools

Setuptools는 Python 패키지를 만들고 배포하는 데 필요한 도구 모음입니다. 이를 통해 패키지의 배포와 관리를 간소화할 수 있습니다.

Categories

Configuration

extras_require

확장 종속성을 설정할 수 있다.

extras_require={
    'test': tests_require,
    },

이렇게 지정해두면, 이런 명령으로 테스트에 필요한 패키지들을 미리 설치할 수 있습니다.

pip install -e .[test]

대표적으로 aiohttp 같은곳에서 pip install aiohttp[speedups]로 설치하는 내용이 extras_require 에 해당된다.

long_description_content_type

Set the value of long_description to the contents (not the path) of the README file itself. Set the long_description_content_type to an accepted Content-Type-style value for your README file’s markup, such as text/plain, text/x-rst (for reStructuredText), or text/markdown.

extra_compile_args

from distutils.core import setup
from distutils.extension import Extension

setup(
    name="myPy",
    ext_modules = [Extension(
        "MyPyExt",
        ["MyPyExt.cpp"],
        libraries = ["boost_python"],
        extra_compile_args = ["-g0"]
        )]
    )

setup.py

Minimal version

from setuptools import setup, find_packages

setup(
    name='MyPackageName',
    version='1.0.0',
    url='https://github.com/mypackage.git',
    author='Author Name',
    author_email='[email protected]',
    description='Description of my package',
    packages=find_packages(),    
    install_requires=['numpy >= 1.11.1', 'matplotlib >= 1.5.1'],
)

recc No-cython version

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

import os
from typing import List
from setuptools import setup, find_packages

SOURCE_PATH = os.path.abspath(__file__)
SOURCE_DIR = os.path.dirname(SOURCE_PATH)
RECC_DIR = os.path.join(SOURCE_DIR, "recc")
README_PATH = os.path.join(SOURCE_DIR, "README.md")
REQUIREMENTS_MAIN = os.path.join(SOURCE_DIR, "requirements.main.txt")
REQUIREMENTS_DOCS = os.path.join(SOURCE_DIR, "requirements.docs.txt")
REQUIREMENTS_TEST = os.path.join(SOURCE_DIR, "requirements.test.txt")
REQUIREMENTS_SETUP = os.path.join(SOURCE_DIR, "requirements.setup.txt")

# Make sure you're in the proper directory, whether you're using pip.
os.chdir(SOURCE_DIR)

from recc.util.version import normalize_version, version_text  # noqa


def read_file(path, encoding="utf-8") -> str:
    with open(path, encoding=encoding) as f:
        return f.read()


def read_packages(path, encoding="utf-8") -> List[str]:
    content = read_file(path, encoding)
    lines = content.split("\n")
    lines = map(lambda x: x.strip(), lines)
    lines = filter(lambda x: x and x[0] != "#", lines)
    return list(lines)


def setup_main():
    long_description = read_file(README_PATH)
    install_requires = read_packages(REQUIREMENTS_MAIN)
    tests_require = read_packages(REQUIREMENTS_TEST)
    setup_requires = read_packages(REQUIREMENTS_SETUP)
    version = normalize_version(version_text)
    packages = find_packages(where=SOURCE_DIR, exclude=("tester*",))

    setup_config = {
        "name": "recc",
        "version": version,
        "description": "The Answer, No-Code Development Platform.",
        "long_description": long_description,
        "long_description_content_type": "text/markdown",
        "license": "MIT License",
        "keywords": [
            "Answer",
            "No-code",
            "Visual Graph",
            "Visual Programming",
            "Machine Learning",
            "Deep Learning",
        ],
        "url": "https://answerdoc.bogonets.com",
        "project_urls": {
            "Documentation": "https://answerdoc.bogonets.com",
            "Source": "https://github.com/bogonets/answer",
        },
        "packages": packages,
        "package_dir": {"recc": "recc"},
        "include_package_data": True,
        "install_requires": install_requires,
        "setup_requires": setup_requires,
        "python_requires": ">=3.8",
        "zip_safe": False,
        "test_suite": "test",
        "tests_require": tests_require,
        "classifiers": [
            "Development Status :: 4 - Beta",
            "Environment :: Web Environment",
            "Environment :: GPU :: NVIDIA CUDA",
            "Framework :: AsyncIO",
            "Intended Audience :: Developers",
            "Intended Audience :: System Administrators",
            "Operating System :: OS Independent",
            "Programming Language :: Python",
            "Programming Language :: Python :: 3.8",
            "Programming Language :: JavaScript",
            "Topic :: Scientific/Engineering :: Image Processing",
            "Topic :: Scientific/Engineering :: Image Recognition",
            "Topic :: Scientific/Engineering :: Information Analysis",
            "Topic :: Scientific/Engineering :: Visualization",
            "Topic :: Software Development",
            "Topic :: Utilities",
            "Topic :: Internet :: WWW/HTTP",
            "License :: OSI Approved :: MIT License",
        ],
        "entry_points": {
            "console_scripts": [
                "recc = recc.app.entrypoint:main",
            ]
        },
        "author": "yourname",
        "author_email": "[email protected]",
        "maintainer": "yourname",
        "maintainer_email": "[email protected]",
    }

    setup(**setup_config)


if __name__ == "__main__":
    setup_main()

Editable

다음과 같이 -e 옵션을 추가하면

pip install -e .

전체 파일을 site-packages에 설치하지 않고, 원본 소스가 있는 디렉토리로 연결된다. 바로바로 편집(editable)할 수 있으니, 개발자 모드처럼 사용할 수 있다.

데이터 파일 지원

파이썬 패키지에 일반 데이터 파일을 포함시키는 방법에 대한 내용. 요약하면:

include_package_data
True일 경우 MANIFEST.in에 정의된 내용대로 포함한다.
package_data
추가 패턴을 지정할 수 있다.
exclude_package_data
패키지가 설치될 때 포함되지 않아야 하는 데이터 파일 및 디렉터리에 대한 패턴을 지정합니다.

MANIFEST

관련 공식 문서 목록
4. 소스 배포판 만들기 — Python 3.10.0a3 문서
4. 소스 배포판 만들기 — 파이썬 설명서 주석판
4. Creating a Source Distribution — Python 3.10.6 documentation
[추천] 8. 명령 레퍼런스 — Python 3.10.0b3 문서 - 매니페스트(manifest) 템플릿 명령에 대한 공식 문서 위치.

매니페스트(manifest) 템플릿 명령은 다음과 같습니다:

명령

설명

include pat1 pat2 ...

나열된 패턴 중 어느 하나와 일치하는 모든 파일을 포함합니다

exclude pat1 pat2 ...

나열된 패턴 중 어느 하나와 일치하는 모든 파일 제외합니다

recursive-include dir pat1 pat2 ...

나열된 패턴 중 어느 하나와 일치하는 dir 아래의 모든 파일을 포함합니다

recursive-exclude dir pat1 pat2 ...

나열된 패턴 중 어느 하나와 일치하는 dir 아래의 모든 파일을 제외합니다

global-include pat1 pat2 ...

소스 트리에서 나열된 패턴 중 어느 하나와 일치하는 모든 파일을 포함합니다

global-exclude pat1 pat2 ...

소스 트리에서 나열된 패턴 중 어느 하나와 일치하는 모든 파일을 제외합니다

prune dir

dir 아래의 모든 파일을 제외합니다

graft dir

dir 아래의 모든 파일을 포함합니다

여기에서 패턴은 유닉스 스타일 glob 패턴입니다:

  • *는 일반 파일명 문자의 모든 시퀀스와 일치하고,
  • ?는 하나의 일반 파일명 문자와 일치하며,
  • [range]는 range의 문자(예를 들어, a-z, a-zA-Z, a-f0-9_.)와 일치합니다.
  • 《일반 파일명 문자》의 정의는 플랫폼에 따라 다릅니다:
    • 유닉스에서는 슬래시를 제외한 모든 것입니다.
    • 윈도우에서는 역 슬래시나 콜론을 제외한 모든 것입니다.

때로는 이것으로 충분하지만, 일반적으로 배포할 추가 파일을 지정하려고 합니다. 이를 수행하는 일반적인 방법은 기본적으로 MANIFEST.in이라는 매니페스트 템플릿(manifest template)을 작성하는 것입니다. 매니페스트 템플릿은 소스 배포에 포함할 정확한 파일 목록인 매니페스트 파일 MANIFEST를 생성하는 방법에 대한 지침 목록일 뿐입니다. sdist 명령은 이 템플릿을 처리하고 해당 지침과 파일 시스템에서 찾은 내용을 기반으로 매니페스트를 생성합니다.

여러분 자신의 매니페스트 파일을 만드는 것을 선호한다면 형식은 간단합니다: 한 줄에 파일 이름 하나, 일반 파일 (또는 이들에 대한 심볼릭 링크)만 가능합니다. 여러분 자신의 MANIFEST를 제공하면, 모든 것을 지정해야 합니다: 위에서 설명한 기본 파일 집합은 이 경우 적용되지 않습니다.

Example

include doc/*
include requirements.*.txt
include README.md

include pyproject.toml

# Include the README
include *.md

# Include the license file
include LICENSE.txt

# Include setup.py
include setup.py

# Include the data files
recursive-include data *

시험판 버전 관리

기본 버전 관리 체계에 관계없이 특정 최종 릴리스의 시험판은 다음과 같이 게시 될 수 있습니다.

  • 0 개 이상의 개발 릴리스 (.devN접미사로 표시)
  • 0 개 이상의 알파 릴리스 (.aN접미사로 표시)
  • 0 개 이상의 베타 릴리스 (.bN접미사로 표시)
  • 0 개 이상의 릴리스 후보 (.rcN접미사로 표시)

pip 다른 최신 Python 패키지 설치 프로그램은 설치할 종속성 버전을 결정할 때 기본적으로 시험판을 무시합니다.

setup.cfg

[metadata]
name = type-serialize
description = Serialize with type annotations
version = attr: type_serialize.__version__
url = https://github.com/yourid/type-serialize
author = yourname
author_email = [email protected]
maintainer = yourname
maintainer_email = [email protected]
project_urls =
    GitHub: issues = https://github.com/yourid/type-serialize/issues
    GitHub: repo = https://github.com/yourid/type-serialize
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT License
license_files = LICENSE
keywords =
    serialize
classifiers =
    Development Status :: 4 - Beta
    Intended Audience :: Developers
    Operating System :: POSIX
    Operating System :: MacOS :: MacOS X
    Operating System :: Microsoft :: Windows
    Programming Language :: Python
    Programming Language :: Python :: 3
    Programming Language :: Python :: 3.8
    Programming Language :: Python :: 3.9
    Programming Language :: Python :: 3.10
    Topic :: Software Development
    Topic :: Utilities
    License :: OSI Approved :: MIT License

[options]
python_requires = >=3.8
packages = find:
tests_require =
    pytest

[options.extras_require]
full =
    numpy
    orjson
    msgpack

[options.packages.find]
exclude =
    test*

[options.entry_points]
console_scripts =
    type_serialize = type_serialize.app.cli:main

Specifying values

  • options.extras_require - Optional dependencies.
    • pip install fastapi[all]과 같이 뒤에 [all]를 붙이면 선택적 종속성 패키지가 설치된다.
    • 예제는 다음과 같다:

[metadata] name = Package-A

[options.extras_require] PDF =

ReportLab>=1.2
RXP

</syntaxhighlight>

  • classifiers: - PyPI:Classifiers 항목 참조.
  • attr: - 모듈 속성에서 값을 읽습니다. attr:Callable과 Iterable을 지원합니다. 지원되지 않는 유형은 str()을 사용하여 캐스트됩니다.
    • (직간접적으로) 타사 가져오기를 포함하는 모듈의 변수에 할당된 리터럴 값의 일반적인 경우를 지원하기 위해 attr:는 먼저 모듈의 AST를 검사하여 모듈에서 값 읽기를 시도합니다. 실패하면 attr:은 모듈 가져오기로 대체합니다.
  • file: - Value is read from a list of files and then concatenated

INFORMATION

file: 지시문은 샌드박스 처리되어 프로젝트 디렉터리(즉, setup.cfg/pyproject.toml이 포함된 디렉터리) 외부에는 도달하지 않습니다.

install_requires file error

다음과 같은 file: 문법이 있는데

install_requires = file: requirements.txt

file: 읽기 요구 사항에 대한 지시문은 버전 62.6부터 지원됩니다. 파일 형식은 requirements.txt 파일과 유사하지만 주석이 아닌 모든 행은 PEP 508을 준수해야 합니다 (pip 지정 구문, 예: -c/-r/-e 플래그는 지원되지 않음). 라이브러리 개발자는 종속성을 특정 버전을 단단히 고정(Tightly Pinning)하는 것을 피해야 합니다 (e.g. "Locked" requirements file)

정상적으로 작동하지 않을 경우 직접 setup.py에 적어야 한다:

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

import os
from setuptools import setup
from typing import List

SOURCE_PATH = os.path.abspath(__file__)
SOURCE_DIR = os.path.dirname(SOURCE_PATH)
REQUIREMENTS_MAIN = os.path.join(SOURCE_DIR, "requirements.main.txt")


def install_requires(encoding="utf-8") -> List[str]:
    with open(REQUIREMENTS_MAIN, encoding=encoding) as f:
        content = f.read()
    lines = content.split("\n")
    lines = map(lambda x: x.strip(), lines)
    lines = filter(lambda x: x and x[0] != "#", lines)
    return list(lines)


if __name__ == "__main__":
    setup(install_requires=install_requires())

또는 distutils.text_file.TextFile를 사용하면 된다:

install_requires = distutils.text_file.TextFile(filename="./requirements.txt").readlines()

recc eample

Cython을 사용하여 -g0 옵션으로, 공유 라이브러리를 생성하고 wheel을 만든다.

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

import os
import re
import sys
import codecs

from setuptools import setup, find_packages
from setuptools.command.build_py import build_py
from Cython.Build import cythonize


SOURCE_PATH = os.path.abspath(__file__)
SOURCE_DIR = os.path.dirname(SOURCE_PATH)
RECC_DIR = os.path.join(SOURCE_DIR, "recc")
README_PATH = os.path.join(SOURCE_DIR, "README.md")
REQUIREMENTS_MAIN = os.path.join(SOURCE_DIR, "requirements.main.txt")
REQUIREMENTS_DOCS = os.path.join(SOURCE_DIR, "requirements.docs.txt")
REQUIREMENTS_TEST = os.path.join(SOURCE_DIR, "requirements.test.txt")
REQUIREMENTS_SETUP = os.path.join(SOURCE_DIR, "requirements.setup.txt")

SOURCE_FILTERS = (
    re.compile(r".*__init__\.py$"),
    re.compile(r".*__main__\.py$"),
)

# Ensure we're in the proper directory whether or not we're being used by pip.
os.chdir(SOURCE_DIR)

from recc.util.version import normalize_version, version_text  # noqa


def ignore_filter(sources, filters=SOURCE_FILTERS):
    result = sources
    for f in filters:
        result = list(filter(lambda x: f.match(x) is None, result))
    return result


def is_match_filter(source, filters=SOURCE_FILTERS):
    for f in filters:
        if f.match(source) is not None:
            return True
    return False


def get_children(path):
    result = []
    for parent, _, files in os.walk(path):
        for name in files:
            result.append(os.path.join(parent, name))
    return result


def get_children_with_match(path, regexp=".*"):
    result = []
    for cursor in get_children(path):
        if re.match(regexp, cursor) is not None:
            result.append(cursor)
    return result


def read_file(path, encoding="utf-8"):
    with codecs.open(filename=path, encoding=encoding) as f:
        return f.read()


def find_fist_positional_argument(args):
    # args[0] is script
    for a in args[1:]:
        if a[0] != "-":
            return a


def is_bdist_wheel():
    return find_fist_positional_argument(sys.argv) == "bdist_wheel"


class NoPythonBuildPy(build_py):
    def find_package_modules(self, package, package_dir):
        # ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
        modules = super().find_package_modules(package, package_dir)
        filtered_modules = []
        for pkg, mod, filepath in modules:
            if is_match_filter(filepath):
                filtered_modules.append((pkg, mod, filepath))
        return filtered_modules


def setup_main():
    long_description = read_file(README_PATH)
    install_requires = read_file(REQUIREMENTS_MAIN).split("\n")
    tests_require = read_file(REQUIREMENTS_TEST).split("\n")
    setup_requires = read_file(REQUIREMENTS_SETUP).split("\n")

    files = get_children_with_match(path=RECC_DIR, regexp=r".*\.py$")
    files = ignore_filter(files)

    compiler_directives = {"language_level": 3}
    cython_modules = cythonize(
        module_list=files, compiler_directives=compiler_directives,
    )
    for ext in cython_modules:
        ext.extra_compile_args = ["-g0"]

    setup_config = {
        "name": "recc",
        "version": normalize_version(version_text),
        "description": "A Python library for the Docker Engine API.",
        "long_description": long_description,
        "long_description_content_type": "text/markdown",
        "url": "https://answerdoc.bogonets.com",
        "project_urls": {
            "Documentation": "https://answerdoc.bogonets.com",
            "Source": "https://github.com/bogonets",
        },
        "packages": find_packages(where=SOURCE_DIR, exclude=("test",)),
        "package_dir": {"recc": "recc"},
        "package_data": {"recc": ["node/*.json"]},
        "install_requires": install_requires,
        "setup_requires": setup_requires,
        "python_requires": ">=3.7",
        "zip_safe": False,
        "test_suite": "test",
        "tests_require": tests_require,
        "classifiers": [
            "Development Status :: 4 - Beta",
            "Environment :: Web Environment",
            "Intended Audience :: Developers",
            "Intended Audience :: System Administrators",
            "Operating System :: OS Independent",
            "Programming Language :: Python",
            "Programming Language :: Python :: 3.7",
            "Topic :: Software Development",
            "Topic :: Utilities",
            "Topic :: Internet :: WWW/HTTP",
        ],
        "entry_points": {
            "console_scripts": [
                "recc = recc.app.entrypoint:main",
            ]
        },
        "maintainer": "yourname",
        "maintainer_email": "[email protected]",
    }

    if is_bdist_wheel():
        setup_config["ext_modules"] = cython_modules
        setup_config["cmdclass"] = {"build_py": NoPythonBuildPy}

    setup(**setup_config)


if __name__ == "__main__":
    setup_main()

파이썬 패키지를 만드는법

간단한 파이썬 유틸리티를 만들어서 공유할 때 패키지로 만드는 법:

  1. 기능 작성
  2. flit 을 이용해 기본 패키지 형태를 작성하고 TestPyPI 에 등록해서 테스트
  3. flit 로 PyPI 에 실제 등록
  4. README 와 CHANGELOG 추가
  5. tox 로 포매팅(black), 테스트 커버리지(coverage), 코드 퀄리티(flake8, pylint, mccabe), 정적 분석(mypy), 등 적용하기
  6. GitHub Actions 로 자동 빌드 설정하고, 커밋할 때 마다 Codecov로 커버리지 & Clode Climate 로 코드 품질 테스트
  7. make 로 pylint, coverage 등만 빠르게 실행하게 만들기
  8. GitHub Actions 에 flit 로 자동 publish 적용

twine 을 사용해 업로드 하는법

twine upload dist/*

See also

Favorite site

Guide

Samples