pipx 源码解析【5】- pipx inject

pipx inject

pipx inject 命令用会往存在的虚拟环境中安装指定的包。语法是 pipx inject abc --dependencies depA depB。这个命令会把 depAdepB 安装到已经存在的 abc 虚拟环境中。

inject.py

和之前的命令一样,inject 会调用 command/inject.py 模块。最终调用的是 inject_dep 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
all_success = True

for dep in package_specs:
all_success &= inject_dep(
venv_dir,
None,
dep,
pip_args,
verbose=verbose,
include_apps=include_apps,
include_dependencies=include_dependencies,
force=force,
)

return EXIT_CODE_OK if all_success else EXIT_CODE_INJECT_ERROR

inject_dep 做了如下几步:

  1. 判断 inject 的目标虚拟环境是否存在。
  2. 运行 venv.install_package 安装 app
  3. 如果上面都没有出错,则执行 run_post_install_actions 方法处理安装后的收尾工作,如果有一步报错,则执行 venv.remove_venv 删除虚拟目录。

这里的 2,3 两步在上一篇的 pipx install 命令已经学习过了,所以从这个层面看,inject 和 install 的差别只是 inject 会指定一个存在的虚拟目录,而 install 是创建一个新的 venv

inject 命令不复杂,所涉及的知识点也基本上在之前 installrun 两个命令中提到过了。

pipx 源码解析【4】- pipx install

用 pipx 安装 app

上一篇学习了 run 命令, 这一篇主要学习下 install 命令的部分, 位置在 src/pipx/commands/install.py

这部分的代码相对较直接,从整个 install 函数看主要分如下步骤:

  1. package_spec 解析出 app 的 package_name
  2. 获取 venv 路径。假设我们的 app 名字是 cowsay, 获取的 venv_dir 路径就是 ~/.local/pipx/venvs/cowsay
  3. 判断 venv_dir 是否已经存在
  4. 如果已经存在,且有 force 选项,则提示用户 venv 目录已经存在,安装将覆盖原有目录内容。如果没有 force 选项,则提示用户后退出程序
  5. 运行 venv.create_venv 创建虚拟目录
  6. 运行 venv.install_package 安装 app
  7. 如果上面都没有出错,则执行 run_post_install_actions 方法处理安装后的收尾工作,如果有一步报错,则执行 venv.remove_venv 删除虚拟目录。

注意:上面 5, 6 两步在上一篇 run 命令中已经提到过了。只是 run 是在一个路径带 hash 值的临时虚拟目录里安装的(假设 hash 值是 abc 时:~/.local/pipx/.cache/abc),而 install 是以 package_name 命名的路径安装的: ~/.local/pipx/venvs/cowsay

Read More

pipx 源码解析【3】- pipx run

执行命令

上一篇学习了如何解析参数,这一篇主要学习如何执行命令。pipx 的命令(command)包括 run, install, inject, upgrade, upgrade-all, list, uninstall, uninstall-all, reinstall, reinstall-all

run_pipx_command 作为命令的主入口,将不同的 command 分配给不同的模块处理。处理 run 命令是 run.py

这篇就先学一个 run, 我也将通过记录知识点的方式学习。

知识点:下载网络文件

run.py 首先判断参数是否为 url,且以 .py 结尾,如果为真就直接从 url 下载这个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
if urllib.parse.urlparse(app).scheme:
if not app.endswith(".py"):
raise PipxError(
"""
pipx will only execute apps from the internet directly if they
end with '.py'. To run from an SVN, try pipx --spec URL BINARY
"""
)
logger.info("Detected url. Downloading and executing as a Python file.")

content = _http_get_request(app)
exec_app([str(python), "-c", content])

...

def _http_get_request(url: str) -> str:
try:
res = urllib.request.urlopen(url)
charset = res.headers.get_content_charset() or "utf-8"
return res.read().decode(charset)
except Exception as e:
logger.debug("Uncaught Exception:", exc_info=True)
raise PipxError(str(e))

其中下载网络文件用了 urllib.request.urlopen(url) 这个方法。下载完成后进入 exec_app 方法。

Read More

pipx 源码解析【2】- argparse 模块

入口函数 cli

第一篇说到 __main__ 函数,里面执行的就是入口函数 clicli 函数的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def cli() -> ExitCode:
"""Entry point from command line"""
try:
# 隐藏光标
hide_cursor()
# 初始化命令行参数解析器
parser = get_command_parser()
# 自动补全
argcomplete.autocomplete(parser)
# 解析命令行参数
parsed_pipx_args = parser.parse_args()
# 初始化准备工作(建立pipx 工作目录,配置日志)
setup(parsed_pipx_args)
# 检查参数准确性
check_args(parsed_pipx_args)
# 命令行参数是否有 command 参数
if not parsed_pipx_args.command:
parser.print_help()
return ExitCode(1)
# 执行命令
return run_pipx_command(parsed_pipx_args)
# 错误处理
except PipxError as e:
print(str(e), file=sys.stderr)
logger.debug(f"PipxError: {e}", exc_info=True)
return ExitCode(1)
except KeyboardInterrupt:
return ExitCode(1)
except Exception:
logger.debug("Uncaught Exception:", exc_info=True)
raise
finally:
logger.debug("pipx finished.")
# 显示光标
show_cursor()

里面每个函数主要的功能都做了注释,最主要的就是 get_command_parserrun_pipx_command 两个函数。

这一篇这里先看 get_command_parser 函数,该只有一个作用,就是解析命令行参数。主要涉及到了 argparse 模块的使用,下面详细说明。

Read More

pipx 源码解析【1】

pipx 是什么?

pipx 是一个 python 包管理工具。但和 pip 又有区别: pip 管理 python 的类库(libraries)和应用(apps),而 pipx 专门管理 python 的应用(apps),另外在此基础上添加了环境隔离的能力。总体上来说有点像 Mac 系统上的 brew

类库和应用怎么区分?我个人理解这里的应用主要指的是可以直接在命令行里执行的脚本,也就是发布时会在 setup.py 写入 console_scripts 字段的 python 库( 安装后生成了可执行脚本并放入 bin 文件夹)。举个 🌰 ,比如 pip, virtualenv 这类可以直接在命令行执行的就是应用,而 numpy, pandas 这类在代码中 import 的则属于类库。另外 pipx 自己本身也是一个应用。

为什么要用隔离环境来单独管理应用呢?我能想到很明显的一个原因是各个应用依赖的版本冲突问题。举个 🌰 ,需要全局安装应用 A 和 应用 B, A 和 B 都依赖某一个类库 C,但依赖的版本不一样,A 依赖 C=1.0,B 依赖 C=2.0。如果先装了 A,在没有环境隔离的情况下,全局的 site-package 下面会有 C=1.0 的版本,这时如果我再安装 B,因为 B 依赖 C=2.0,所以安装完成后会将全局 site-package 里的 C=1.0 替换为 C=2.0。这可能会导致应用 A 无法正常工作。

更详细的可以参照 pipx文档

Read More

python records 源码解析

总览

records 库是 python 大神 kennethreitz 写的一个 SQL for Human 的 python 库,意在让书写 sql 更加的便捷和人性化。

从 README 里可以看出 records 是对 sqlalchemytablist 的一层包装,所以阅读源码主要是学习作者 pythonic 的代码编写思维。

所以本文不会纠结于源码的具体逻辑,重点是学习一些知识点,下面的每一段都会针对我觉得值得借鉴和学习的某一个知识点来做记录

1. 如何让代码既能用做 module 又可以被直接执行

如果是直接执行 records.py, 则会进入下面的函数,在 cli() 方法内部使用了 docopt 来解析命令行参数。docopt 的使用可以参考之前的的文章 envoy 源码解析

1
2
3
# Run the CLI when executed directly.
if __name__ == '__main__':
cli()

Read More

什么是反向代理

正向代理

首先回顾下我们最常见的代理: 正向代理。

我们常说的代理也就是指正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求,某些科学上网工具扮演的就是典型的正向代理角色。用浏览器访问

正向代理的实现

Read More

正则表达式匹配

题目

给你一个字符串  s  和一个字符规律  p,请你来实现一个支持 ‘.’  和  ‘*‘  的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*‘ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖   整个   字符串  s 的,而不是部分字符串。

思路

首先状态 dp 一定能自己想出来。
dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配

  1. 如果 p.charAt(j) == s.charAt(i) : dp[i][j] = dp[i-1][j-1];

  2. 如果 p.charAt(j) == ‘.’ : dp[i][j] = dp[i-1][j-1];

  3. 如果 p.charAt(j) == ‘*‘:

    • 如果 p.charAt(j-1) != s.charAt(i) : dp[i][j] = dp[i][j-2] // in this case, a* only counts as empty
    • 如果 p.charAt(j-1) == s.charAt(i) or p.charAt(i-1) == ‘.’:
      • dp[i][j] = dp[i-1][j] //in this case, a* counts as multiple a
      • or dp[i][j] = dp[i][j-1] // in this case, a* counts as single a
      • or dp[i][j] = dp[i][j-2] // in this case, a* counts as empty

Read More

字符串转换整数 (atoi)

题目

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数 myAtoi(string s) 的算法如下:

读入字符串并丢弃无用的前导空格

检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。

读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。

将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。

如果整数数超过 32 位有符号整数范围 [−231,  231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为 −231 ,大于 231 − 1 的整数应该被固定为 231 − 1 。

返回整数作为最终结果。

Read More

整数反转

题目

这是 leetcode 上一道难度为简单的算法题:

给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−231,  231 − 1] ,就返回 0。

虽然是一个简单题,但如果你对 python 的负数取余和整数除法不熟悉的话,很容易踩坑🙂。

Read More