zl程序教程

您现在的位置是:首页 >  Python

当前栏目

使用 Python 开发 CLI 工具并打包发布到 PyPI

2023-03-07 09:02:32 时间

引言

使用 Python 开发 CLI 工具并打包发布到 PyPI

  • 打包 Python 并发布到 PyPi
  • 使用 Python 开发 CLI 工具

打包并发布

方式1: setup.py

参考:

TODO:

方式2: 使用 poetry

参考:

安装 poetry

Windows 10

Windows (Powershell)

(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -

PS: 其实现在 PowerShell 也支持 curl , 其实好像就是 WebRequest 实现的

By default, Poetry is installed into a platform and user-specific directory:

  • ~/Library/Application Support/pypoetry on MacOS.
  • ~/.local/share/pypoetry on Linux/Unix.
  • %APPDATA%\pypoetry on Windows.

If you wish to change this, you may define the $POETRY_HOME environment variable:

这里我不想安装在默认路径

Windows (Powershell)

$env:APPDATA
$env:POETRY_HOME=“D:\Program Files\pypoetry”

现在可以安装了

(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -

注意最后有个 -

安装失败:

参考: - Poetry教程一(Poetry安装与卸载)_成都 - 阿木木的博客-CSDN博客_poetry安装

?  (Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
Retrieving Poetry metadata

# Welcome to Poetry!

This will download and install the latest version of Poetry,
a dependency and package manager for Python.

It will add the `poetry` command to Poetry's bin directory, located at:

D:\Program Files\pypoetry\bin

You can uninstall at any time by executing this script with the --uninstall option,
and these changes will be reverted.

Installing Poetry (1.3.1)
Installing Poetry (1.3.1): Creating environment
Installing Poetry (1.3.1): Installing Poetry
Installing Poetry (1.3.1): An error occurred. Removing partial environment.
Poetry installation failed.
See F:\Repos\notebook\poetry-installer-error-5nxluwjh.log for error logs.
?  cat poetry-installer-error-5nxluwjh.log
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple/poetry/

WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple/poetry/

WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple/poetry/

WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple/poetry/

WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError(0, 'Error'))': /simple/poetry/

ERROR: Could not find a version that satisfies the requirement poetry==1.3.1 (from versions: none)

ERROR: No matching distribution found for poetry==1.3.1

WARNING: There was an error checking the latest version of pip.


Traceback:

  File "<stdin>", line 919, in main
  File "<stdin>", line 550, in run
  File "<stdin>", line 572, in install
  File "<stdin>", line 675, in install_poetry
  File "<stdin>", line 367, in pip
  File "<stdin>", line 364, in python
  File "<stdin>", line 357, in run

指定使用 代理, 还是失败了

(Invoke-WebRequest -Proxy http://127.0.0.1:10808 -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
?  (Invoke-WebRequest -Proxy http://127.0.0.1:10808 -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -
Invoke-WebRequest: The response ended prematurely.

尝试另外一种方式 curl

curl -sSL https://install.python-poetry.org | python -

还是相同错误 失败

curl -sSL https://install.python-poetry.org | python3 - --git https://github.com/python-poetry/poetry.git@master

从 GitHub 安装失败

(Invoke-WebRequest -Proxy http://127.0.0.1:10808 -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python -

还是失败

离线安装 poetry

参考:

  1. 下载: https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py
  2. 下载: https://github.com/python-poetry/poetry/releases
  1. 将下载的压缩包存到与 install-poetry.py 文件 同级的文件夹下,不要解压
  1. 在此目录下运行安装
python install-poetry.py --file poetry-1.3.1.tar.gz
python install-poetry.py --path poetry-1.3.1.tar.gz

安装失败, 详细查看,还是 代理连接出错, 好奇怪的报错, pip.ini 代理等配置与代理工具均已关闭

尝试下设置 pip 国内镜像源, 发现上次电脑重装后, 还没有重新配置

安装过程极慢, 但总算是安装上了

Add Poetry to your PATH

The installer creates a poetry wrapper in a well-known, platform-specific directory:

  • $HOME/.local/bin on Unix.
  • %APPDATA%\Python\Scripts on Windows.
  • $POETRY_HOME/bin if $POETRY_HOME is set.

If this directory is not present in your $PATH, you can add it in order to invoke Poetry as poetry.

Alternatively, the full path to the poetry binary can always be used:

  • ~/Library/Application Support/pypoetry/venv/bin/poetry on MacOS.
  • ~/.local/share/pypoetry/venv/bin/poetry on Linux/Unix.
  • %APPDATA%\pypoetry\venv\Scripts\poetry on Windows.
  • $POETRY_HOME/venv/bin/poetry if $POETRY_HOME is set.

如下图

poetry --version
poetry self update

基础用法

poetry new --src my-package
my-package
├── pyproject.toml
├── README.md
├── src
│   └── my_package
│       └── __init__.py
└── tests
    └── __init__.py

打包并发布

参考:

poetry build
poetry publish
poetry config http-basic.pypi <username> <password>

发布成功

开发 CLI 工具

使用 Typer

参考:

poetry add "typer[all]"
# src/my_package/main.py

import typer


app = typer.Typer()


@app.callback()
def callback():
    """
    Awesome Portal Gun
    """


@app.command()
def shoot():
    """
    Shoot the portal gun
    """
    typer.echo("Shooting portal gun")


@app.command()
def load():
    """
    Load the portal gun
    """
    typer.echo("Loading portal gun")

Add a "script"

pyproject.toml

[tool.poetry.scripts]
my-package = "my_package.main:app"

my-package: 是 CLI 程序的名, 用于在 terminal 中呼叫

测试

poetry install

my-package

poetry build

# 从本地文件包安装, 注意替换文件路径
pip install --user /home/rock/code/rick-portal-gun/dist/rick_portal_gun-0.1.0-py3-none-any.whl

poetry install 安装后, 新开 Terimal 还是不能使用, 尝试 build 再 pip install

pip install --user dist/imaging-0.0.1-py3-none-any.whl

可在 main.py 最后添加 __main__ 用于启动测试

main.py

if __name__ == "__main__":
    app()

Q&A

Q: imaging 识别不到

[tool.poetry.scripts]
imaging = "imaging.main:app"

注意: 不是 src.imaging.main:app , 因为前面 packages 都已经 include

目测还必须将以下路径添加到环境变量 PATH 中,就如上面的图中 Warning 一样

C:\Users\yiyun\AppData\Roaming\Python\Python38\Scripts

发现若 Scripts 文件夹 已存在 imaging.exe 则反复安装并不会更新, 需要先卸载, 再安装即可成功

pip uninstall dist/pyimaging-0.0.1-py3-none-any.whl

pip install --user dist/pyimaging-0.0.1-py3-none-any.whl

PS: 很神奇, 目测有除包名外区分方法, 居然旧的包名(imaging)也一并卸载了

或者:

pip uninstall pyimaging

Q: UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 100: illegal multibyte sequence

参考:

pip install --user dist/pyimaging-0.0.1-py3-none-any.whl

ERROR: Exception:
Traceback (most recent call last):
  File "D:\anaconda3\lib\site-packages\pip\_internal\cli\base_command.py", line 188, in _main
    status = self.run(options, args)
  File "D:\anaconda3\lib\site-packages\pip\_internal\cli\req_command.py", line 185, in wrapper
    return func(self, options, args)
  File "D:\anaconda3\lib\site-packages\pip\_internal\commands\install.py", line 398, in run
    installed = install_given_reqs(
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\__init__.py", line 67, in install_given_reqs
    requirement.install(
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_install.py", line 804, in install
    install_wheel(
  File "D:\anaconda3\lib\site-packages\pip\_internal\operations\install\wheel.py", line 622, in install_wheel
    install_unpacked_wheel(
  File "D:\anaconda3\lib\site-packages\pip\_internal\operations\install\wheel.py", line 596, in install_unpacked_wheel
    rows = get_csv_rows_for_installed(
  File "D:\anaconda3\lib\site-packages\pip\_internal\operations\install\wheel.py", line 247, in get_csv_rows_for_installed
    for row in old_csv_rows:
UnicodeDecodeError: 'gbk' codec can't decode byte 0x80 in position 100: illegal multibyte sequence

修改下方路径文件

D:\anaconda3\Lib\site-packages\pip\_internal\operations\install\wheel.py

找到 get_csv_rows_for_installed, 发现此方法没有文件读取操作, 不是此方法, 搜索 open( , 为所有打开文件操作加上 下方 encoding 要求

, encoding="utf-8"
?  pip uninstall pyimaging
Found existing installation: pyimaging 0.2.0
ERROR: Exception:
Traceback (most recent call last):
  File "D:\anaconda3\lib\site-packages\pip\_internal\cli\base_command.py", line 188, in _main
    status = self.run(options, args)
  File "D:\anaconda3\lib\site-packages\pip\_internal\commands\uninstall.py", line 85, in run
    uninstall_pathset = req.uninstall(
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_install.py", line 675, in uninstall
    uninstalled_pathset = UninstallPathSet.from_dist(dist)
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_uninstall.py", line 535, in from_dist
    for path in uninstallation_paths(dist):
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_uninstall.py", line 67, in unique
    for item in fn(*args, **kw):
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_uninstall.py", line 85, in uninstallation_paths
    r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
  File "D:\anaconda3\lib\site-packages\pip\_vendor\pkg_resources\__init__.py", line 1432, in get_metadata_lines
    return yield_lines(self.get_metadata(name))
  File "D:\anaconda3\lib\site-packages\pip\_vendor\pkg_resources\__init__.py", line 1424, in get_metadata
    return value.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc7 in position 908: invalid continuation byte in RECORD file at path: c:\users\yiyun\appdata\roaming\python\python38\site-packages\pyimaging-0.2.0.dist-info\RECORD

此错误也是相同解决方法, encoding 问题 注意: 下方不要添加

# ValueError: binary mode doesn't take an encoding argument
with open(path, 'rb') as stream:

Q: Could not install packages due to an EnvironmentError: [Errno 2] No such file or directory

参考:

?  pip install --user dist/pyimaging-0.0.1-py3-none-any.whl
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple/, https://pypi.tuna.tsinghua.edu.cn/simple/, http://pypi.douban.com/simple/, http://pypi.mirrors.ustc.edu.cn/simple/
Requirement already satisfied: pyimaging==0.0.1 from file:///F:/Repos/imaging/dist/pyimaging-0.0.1-py3-none-any.whl in c:\users\yiyun\appdata\roaming\python\python38\site-packages (0.0.1)
WARNING: No metadata found in c:\users\yiyun\appdata\roaming\python\python38\site-packages
ERROR: Could not install packages due to an EnvironmentError: [Errno 2] No such file or directory: 'c:\\users\\yiyun\\appdata\\roaming\\python\\python38\\site-packages\\pyimaging-0.0.1.dist-info\\METADATA'

Q: FileNotFoundError: [Errno 2] No such file or directory: 'c:\\users\\yiyun\\appdata\\roaming\\python\\python38\\site-packages\\pyimaging-0.0.1.dist-info\\RECORD'

?  pip uninstall pyimaging
Found existing installation: pyimaging 0.0.1
ERROR: Exception:
Traceback (most recent call last):
  File "D:\anaconda3\lib\site-packages\pip\_internal\cli\base_command.py", line 188, in _main
    status = self.run(options, args)
  File "D:\anaconda3\lib\site-packages\pip\_internal\commands\uninstall.py", line 85, in run
    uninstall_pathset = req.uninstall(
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_install.py", line 675, in uninstall
    uninstalled_pathset = UninstallPathSet.from_dist(dist)
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_uninstall.py", line 535, in from_dist
    for path in uninstallation_paths(dist):
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_uninstall.py", line 67, in unique
    for item in fn(*args, **kw):
  File "D:\anaconda3\lib\site-packages\pip\_internal\req\req_uninstall.py", line 85, in uninstallation_paths
    r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
  File "D:\anaconda3\lib\site-packages\pip\_vendor\pkg_resources\__init__.py", line 1432, in get_metadata_lines
    return yield_lines(self.get_metadata(name))
  File "D:\anaconda3\lib\site-packages\pip\_vendor\pkg_resources\__init__.py", line 1420, in get_metadata
    value = self._get(path)
  File "D:\anaconda3\lib\site-packages\pip\_vendor\pkg_resources\__init__.py", line 1616, in _get
    with open(path, 'rb') as stream:
FileNotFoundError: [Errno 2] No such file or directory: 'c:\\users\\yiyun\\appdata\\roaming\\python\\python38\\site-packages\\pyimaging-0.0.1.dist-info\\RECORD'

经过测试, 直接删除 下方路径文件夹即可再次成功安装

C:\Users\yiyun\AppData\Roaming\Python\Python38\site-packages\pyimaging-0.0.1.dist-info

发现我反复安装后, 终于有了这两个在 install (METADATA) 与 uninstall(RECORD) 时会寻找的两个文件

C:\Users\yiyun\AppData\Roaming\Python\Python38\site-packages\pyimaging-0.0.1.dist-info

补充

目前 GitHub Package 不支持 Python 包

目前 GitHub Package 不支持 Python 包

Python 为图片加水印

参考:

import os
from PIL import Image, ImageDraw, ImageFont

def add_watermark(image_path, watermark_text):
  image = Image.open(image_path)
  font = ImageFont.truetype("arial.ttf", 32)
  fill_color = (255, 255, 255)
  width, height = image.size
  draw = ImageDraw.Draw(image)
  text_width, text_height = draw.textsize(watermark_text, font)
  x = (width - text_width) / 2
  y = (height - text_height) / 2
  draw.text((x, y), watermark_text, font=font, fill=fill_color)
  image.save(image_path)

def watermark_images_in_folder(folder_path, watermark_text):
  for root, dirs, files in os.walk(folder_path):
    for file in files:
      if file.endswith(('.jpg', '.jpeg', '.png')):
        image_path = os.path.join(root, file)
        add_watermark(image_path, watermark_text)

watermark_images_in_folder(".", "Powered by Python")

使用 .NET 为图片加水印

参考:

poetry 添加私有仓库源 (eg: 国内 PyPi 镜像源)

参考:

添加国内清华镜像源 - pypi | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

poetry source add tsinghua https://pypi.tuna.tsinghua.edu.cn/simple

对应配置文件 (pyproject.toml) 会自动添加上以下部分

[[tool.poetry.source]]
name = "tsinghua"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = false
secondary = false

PS:

# 添加 foo 源 为 次要(secondary) 源
poetry source add --secondary foo https://pypi.example.org/simple/
# 指定从 foo 源下载 private-package
poetry add --source foo private-package

参考

感谢帮助!