嗨,我是一个名为 GridCal 的开源程序的创建者。它由基于 Qt 的 GUI 和计算引擎组成。从一开始,出于实际原因,我就将 GUI 和引擎放在同一个包下。下面的结构运行良好,并且 pypi 的包已正确创建。这是因为
setup.py
文件与包 GirdCal
处于同一级别
repository_folder
|
|_ src
|_ GridCal
| |_ GUI
| |_ Core
| |_ IO
| |_ Simulations
| |_ __init__.py
|
|_ setup.py
|_ upload_to_pypi.py
最近,我将代码拆分为 2 个包(GridCal 和 GridCalEngine),看看是否可以获得适用于开发和部署到 pypi 的结构。
结构如下:
repository_folder
|
|_ src
|_ GridCal
| |_ GUI
| |_ __init__.py
| |_ setup.py
|
|_ GridCalEngine
| |_ Core
| |_ IO
| |_ Simulations
| |_ __init__.py
| |_ setup.py
|
|_ upload_to_pypi.py
这种拆分结构非常适合开发,因为 GUI 可以正确引用引擎,生成的包会被检查并上传到 pypi,但是在安装这些包时,安装失败,因为 文件不在包之外就像前面的例子一样。
我在SO中看到了像this和其他相关问题这样的资源,但它们没有解决生成可安装包的问题。
如何解决setup.py
文件位置问题?
setup.py
移出包裹,请移动包裹 在子目录内。这是推荐的扁平结构 布局:
repository_folder
|
|_ GridCal
| |_ GridCal
| | | |_ GUI
| | | |_ __init__.py
| |_ setup.py
|
|_ GridCalEngine
| |_ GridCalEngine
| | | |_ Core
| | | |_ IO
| | | |_ Simulations
| | | |_ __init__.py
| |_ setup.py
|
|_ upload_to_pypi.py
或 src 布局:
repository_folder
|
|_ GridCal
| _ src
| |_ GridCal
| | |_ GUI
| | |_ __init__.py
| |_ setup.py
|
|_ GridCalEngine
| _ src
| | |_ GridCalEngine
| | |_ Core
| | |_ IO
| | |_ Simulations
| | |_ __init__.py
| |_ setup.py
|
|_ upload_to_pypi.py
# This file is part of GridCal
#
# GridCal is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GridCal is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GridCal. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
import tarfile
import zipfile
from pathlib import Path
from typing import List, Tuple
from subprocess import call
def build_setup_cfg() -> str:
"""
Generate the content of setup.cgf
:return:
"""
val = '[egg_info]\n'
val += 'tag_build = \n'
val += 'tag_date = 0\n'
return val
def build_pkg_info(name: str,
version: str,
summary: str,
home_page: str,
author: str,
email: str,
license_: str,
keywords: str,
classifiers_list: List[str],
requires_pyhon: str,
description_content_type: str,
provides_extra: str,
long_description: str):
"""
Generate the content of PKG-INFO
:param name:
:param version:
:param summary:
:param home_page:
:param author:
:param email:
:param license_:
:param keywords:
:param classifiers_list:
:param requires_pyhon:
:param description_content_type:
:param provides_extra:
:param long_description:
:return:
"""
val = 'Metadata-Version: 2.1\n'
val += "Name: " + name + '\n'
val += "Version: " + version + '\n'
val += "Summary: " + summary + '\n'
val += "Home-page: " + home_page + '\n'
val += "Author: " + author + '\n'
val += "Author-email: " + email + '\n'
val += "License: " + license_ + '\n'
val += "Keywords: " + keywords + '\n'
for classifier in classifiers_list:
val += "Classifier: " + classifier + '\n'
val += "Requires-Python: " + requires_pyhon + '\n'
val += "Description-Content-Type: " + description_content_type + '\n'
val += "Provides-Extra: " + provides_extra + '\n'
val += '\n' + long_description + '\n'
return val
def check_ext(filename, ext_filter) -> bool:
"""
Check of the file complies with the list of extensions
:param filename: filename
:param ext_filter: list of extnsions
:return: true/false
"""
for ext in ext_filter:
if filename.endswith(ext):
return True
return False
def find_pkg_files(path: str, ext_filter=['.py']) -> List[Tuple[str, str]]:
"""
Get list
:param path: path to traverse
:param ext_filter: extensions of files to include
:return: list of [filename, complete path
"""
files_list = list()
for (dirpath, dirnames, filenames) in os.walk(path):
for fname in filenames:
if check_ext(filename=fname, ext_filter=ext_filter):
pth = os.path.join(dirpath, fname)
files_list.append((fname, pth))
return files_list
def build_tar_gz_pkg(pkg_name: str,
setup_path: str,
version: str,
summary: str,
home_page: str,
author: str,
email: str,
license_: str,
keywords: str,
classifiers_list: List[str],
requires_pyhon: str,
description_content_type: str,
provides_extra: str,
long_description: str,
folder_to_save='dist',
ext_filter=['py']):
"""
:param pkg_name:
:param setup_path:
:param version:
:param summary:
:param home_page:
:param author:
:param email:
:param license_:
:param keywords:
:param classifiers_list:
:param requires_pyhon:
:param description_content_type:
:param provides_extra:
:param long_description:
:param folder_to_save:
:param ext_filter:
:return:
"""
pkg_name2 = pkg_name + '-' + version
filename = pkg_name2 + '.tar.gz'
output_filename = os.path.join(folder_to_save, filename)
files = find_pkg_files(path=pkg_name,
ext_filter=ext_filter)
pkg_info = build_pkg_info(name=pkg_name,
version=version,
summary=summary,
home_page=home_page,
author=author,
email=email,
license_=license_,
keywords=keywords,
classifiers_list=classifiers_list,
requires_pyhon=requires_pyhon,
description_content_type=description_content_type,
provides_extra=provides_extra,
long_description=long_description)
pkg_info_path = 'pkg_info' + pkg_name
with open(pkg_info_path, 'w') as f:
f.write(pkg_info)
setup_cfg = build_setup_cfg()
setup_cfg_path = 'setup_cfg' + pkg_name
with open(setup_cfg_path, 'w') as f:
f.write(setup_cfg)
with tarfile.open(output_filename, "w:gz") as tar:
for name, file_path in files:
if not name.endswith('setup.py'):
tar.add(file_path, arcname=os.path.join(pkg_name2, file_path))
# add the setup where it belongs
tar.add(setup_path, arcname=os.path.join(pkg_name2, 'setup.py'))
# add
tar.add(pkg_info_path, arcname=os.path.join(pkg_name2, 'PKG-INFO'))
tar.add(setup_cfg_path, arcname=os.path.join(pkg_name2, 'setup.cfg'))
os.remove(pkg_info_path)
os.remove(setup_cfg_path)
return output_filename
def build_wheel(pkg_name: str,
setup_path: str,
version: str,
summary: str,
home_page: str,
author: str,
email: str,
license_: str,
keywords: str,
classifiers_list: List[str],
requires_pyhon: str,
description_content_type: str,
provides_extra: str,
long_description: str,
folder_to_save='dist',
ext_filter=['py']):
"""
:param pkg_name:
:param setup_path:
:param version:
:param summary:
:param home_page:
:param author:
:param email:
:param license_:
:param keywords:
:param classifiers_list:
:param requires_pyhon:
:param description_content_type:
:param provides_extra:
:param long_description:
:param folder_to_save:
:param ext_filter:
:return:
"""
pkg_name2 = pkg_name + '-' + version
filename = pkg_name2 + '.whl'
output_filename = os.path.join(folder_to_save, filename)
files = find_pkg_files(path=pkg_name,
ext_filter=ext_filter)
pkg_info = build_pkg_info(name=pkg_name,
version=version,
summary=summary,
home_page=home_page,
author=author,
email=email,
license_=license_,
keywords=keywords,
classifiers_list=classifiers_list,
requires_pyhon=requires_pyhon,
description_content_type=description_content_type,
provides_extra=provides_extra,
long_description=long_description)
pkg_info_path = 'pkg_info' + pkg_name
with open(pkg_info_path, 'w') as f:
f.write(pkg_info)
setup_cfg = build_setup_cfg()
setup_cfg_path = 'setup_cfg' + pkg_name
with open(setup_cfg_path, 'w') as f:
f.write(setup_cfg)
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED)as tar:
for name, file_path in files:
if not name.endswith('setup.py'):
tar.write(file_path, arcname=os.path.join(pkg_name2, file_path))
# add the setup where it belongs
tar.write(setup_path, arcname=os.path.join(pkg_name2, 'setup.py'))
# add
tar.writestr(os.path.join(pkg_name2, 'PKG-INFO'), data=pkg_info)
tar.writestr(os.path.join(pkg_name2, 'setup.cfg'), data=setup_cfg_path)
os.remove(pkg_info_path)
os.remove(setup_cfg_path)
return output_filename
def read_pypirc() -> Tuple[str, str]:
"""
Read the pypirc file located in home
:return: user, password
"""
home = Path.home()
path = os.path.join(home, 'pypirc')
with open(path) as file:
lines = [line.rstrip() for line in file]
user = ''
pwd = ''
for line in lines:
if '=' in line:
key, val = line.split('=')
if key.strip() == 'username':
user = val.strip()
elif key.strip() == 'password':
pwd = val.strip()
return user, pwd
def publish(pkg_name: str,
setup_path: str,
version: str,
summary: str,
home_page: str,
author: str,
email: str,
license_: str,
keywords: str,
classifiers_list: List[str],
requires_pyhon: str,
description_content_type: str,
provides_extra: str,
long_description: str):
"""
Publish package to Pypi using twine
:param pkg_name: name of the package (i.e GridCal)
:param setup_path: path to the package setup.py (i.e. GridCal/setup.py)
:param version: verison of the package (i.e. 5.1.0)
:param summary:
:param home_page:
:param author:
:param email:
:param license_:
:param keywords:
:param classifiers_list:
:param requires_pyhon:
:param description_content_type:
:param provides_extra:
:param long_description:
"""
# build the tar.gz file
fpath = build_tar_gz_pkg(pkg_name=pkg_name,
setup_path=setup_path,
version=version,
summary=summary,
home_page=home_page,
author=author,
email=email,
license_=license_,
keywords=keywords,
classifiers_list=classifiers_list,
requires_pyhon=requires_pyhon,
description_content_type=description_content_type,
provides_extra=provides_extra,
long_description=long_description,
folder_to_save='dist',
ext_filter=['.py', '.csv', '.txt'])
# check the tar.gz file
call([sys.executable, '-m', 'twine', 'check', fpath])
user, pwd = read_pypirc()
# upload the tar.gz file
call([sys.executable, '-m', 'twine', 'upload',
'--repository', 'pypi',
'--username', user,
'--password', pwd,
fpath])
这使我能够拥有完全符合我想要的结构,并仍然生成 pip 正确的包。
repository_folder
|
|_ src
|_ GridCal
| |_ GUI
| |_ __init__.py
| |_ setup.py
|
|_ GridCalEngine
| |_ Core
| |_ IO
| |_ Simulations
| |_ __init__.py
| |_ setup.py
|
|_ upload_to_pypi.py