在WxPython跨平台开发框架中,我们大多数情况下,数据记录通过wx.Grid的数据表格进行展示,其中表格的数据记录的显示和相关处理,通过在基类窗体 BaseListFrame 进行统一的处理,因此对于常规的数据记录打印,我们也可以在其中集成相关的打印处理,本篇随笔介绍如何利用WxPython内置的打印数据组件实现列表数据的自定义打印处理,以及对记录进行分页等常规操作。

1、WxPython内置的打印数据组件

wx.PrintPreview

wx.Printer

wx.Printout
是 wxPython 提供的用于打印功能的核心类。它们帮助开发者在程序中实现打印和打印预览功能。下面是关于这三个类的详细介绍及其使用方法。

1)
wx.Printout

wx.Printout
是一个抽象类,通常需要被子类化以便在打印时自定义页面内容。你可以通过继承
wx.Printout
类来实现自定义打印内容,重写
OnPrintPage
方法来决定每一页的打印内容。

常用方法:

  • OnPrintPage(page)
    : 该方法是打印过程中每一页的核心方法。你需要在这里绘制要打印的内容,例如文本、图片、图形等。每次页面需要绘制时,该方法会被调用。
  • GetDC()
    : 获取一个打印设备上下文(
    wx.DC
    对象),用于在页面上绘制内容。
  • HasPage(page)
    : 该方法用于检查是否有足够的内容来填充打印页面,返回
    True

    False

简单的wx.PrintOut的子类继承处理如下所示。

importwxclassMyPrintout(wx.Printout):def __init__(self):
super().
__init__()defOnPrintPage(self, page):
dc
=self.GetDC()
dc.SetFont(wx.Font(
10, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
dc.DrawText(
"Hello, this is a print test!", 100, 100)return True

一般情况下,我们肯定会添加很多相关的绘制逻辑处理,这里只是简单的介绍。

2)
wx.Printer

wx.Printer
是用来执行实际打印操作的类。它用于与打印机进行交互,发送页面内容到打印机。你可以使用
wx.Printer
来选择打印机、启动打印作业并发送打印内容。

常用方法:

  • Print(printout)
    : 用于启动打印操作,将
    printout
    对象中的内容发送到打印机。
  • GetCurrentPrinterName()
    : 获取当前选定的打印机名称。
  • SetPageSize(width, height)
    : 设置打印页面的大小。
  • SetPrintData(print_data)
    : 设置打印的数据,控制打印的纸张大小、打印质量等

3)
wx.PrintPreview

wx.PrintPreview
用于实现打印预览功能,它允许你查看打印内容的预览,而不是直接将内容发送到打印机。通过打印预览,你可以看到打印文档的外观,并进行调整(如分页、内容布局等)。

常用方法:

  • IsOk()
    : 检查打印预览是否成功创建。
  • Show()
    : 显示打印预览窗口。
  • SetPagesPerPreview(pages)
    : 设置每个预览页面显示的页面数。

通常,
wx.PrintPreview
是在打印机选择之前使用的,它会展示打印内容的预览。预览确认后,用户可以选择继续打印,程序则使用
wx.Printer
实际执行打印操作。

绘制二维表的打印预览可以通过在
wx.Printout
类的
OnPrintPage
方法中逐行逐列绘制表格内容。为了实现更复杂的表格布局,通常需要计算每一行和每一列的位置,确保文本不会超出页面边界,并在必要时分页。

如果需要在 wxPython 中实现打印预览,可以使用
wx.PrintPreview
类来生成打印预览。这个类允许你在显示打印预览时,不直接打印内容,而是显示一个打印的“模拟”视图,用户可以在此视图中查看打印效果,决定是否继续打印。

高级功能:表格样式

如果你需要更复杂的表格样式(例如,添加边框、背景色或其他格式化功能),你可以通过
dc.SetPen()

dc.SetBrush()
等方法来实现。

示例:给表格添加边框

defdraw_table_border(dc, x_pos, y_pos, width, height):
dc.SetPen(wx.Pen(wx.Colour(0, 0, 0)))
#设置边框颜色 dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255))) #设置背景颜色 dc.DrawRectangle(x_pos, y_pos, width, height)

你可以在
OnPrintPage
方法中调用此函数,为每个单元格添加边框。

2、在框架的基类窗体中实现自定义打印及分页处理

在我的WxPython跨平台开发框架中,我们对于常规窗体列表界面做了抽象处理,一般绝大多数的逻辑封装在基类上,基类提供一些可重写的函数给子类实现弹性化的处理。

如下是基类窗体和其他窗体之间的集成关系。

一般列表界面,如下所示。

我们打印模块的处理,需要把这些列表的记录显示在打印预览界面上,并决定是否继续打印即可。

前面介绍了wx.PrintOut的子类处理,只是简单的输出一些文本信息,一般对于更复杂的打印需求(例如表格或多页文档),你可以在
OnPrintPage
中使用更复杂的绘制逻辑。例如,绘制多个表格行或根据页数拆分内容等等。

我们先来看看实现后的打印预览界面效果,有一个感官认识后再继续探寻它的实现机制。

打印预览入口,在列表界面上右键,弹出打印菜单,如下界面所示。

打印预览界面如下所示。

我们来看看,对于列表数据,我们是如何一步步通过自定义的方式实现打印的内容绘制的。

打印有几个注意的地方,由于不同的操作系统或者不同的设置可能会导致打印面板的尺寸有所差异,因此最好统一转换为一个逻辑的尺寸处理;另外分页的处理是关键,需要根据字体文字计算出你绘制一行所需要的高度,然后根据操作的页面高度计算可以绘制的行数,然后在表格行记录中循环处理,判断达到每页的行数就换行即可。

在我们的WxPython跨平台框架中列表入口菜单的实现如下代码所示,主要就是调用MyPrintOut的自定义对象,然后调用PrintPreview进行打印预览窗体的显示即可,如下代码所示。

    def OnPrint(self, event: wx.Event) ->None:"""打印表格"""

        #创建打印数据对象
        pageData =wx.PrintData()
pageData.SetPaperId(wx.PAPER_A4)
#设置纸张类型为 A4 pageData.SetOrientation(wx.LANDSCAPE) #设置纸张方向为纵向 #将打印数据设置到打印机对象 data =wx.PrintDialogData(pageData)#窗口标题 page_title =self.GetTitle()#创建打印输出对象 printout =ctrl.MyPrintout(self.grid, page_title)
printout2
=ctrl.MyPrintout(self.grid, page_title)#创建打印机对象,并将打印数据传递给打印机 #printer = wx.Printer(data) preview = wx.PrintPreview(printout, printout2, data=data)ifpreview.IsOk():
preview.SetZoom(
100)
frame
=wx.PreviewFrame(
preview, self,
"打印预览", pos=self.GetPosition(), size=self.GetSize()
)
frame.Initialize()
frame.Show(True)

wx.PrintPreview需要接受两个PrintOut对象,如代码:printout, printout2

wx.PrintData 主要就是指定页面尺寸及布局等相关信息,具体的逻辑还是在自定义的 MyPrintout 类里面,它接受当前的wx.Grid对象和当前页面的标题。

而ctrl.MyPrintOut是我们根据需要打印的二维表内容进行的逻辑封装,它们的类初始化代码如下。

classMyPrintout(wx.Printout):"""创建一个自定义的 wx.Printout 类,在其中定义如何打印 wx.Grid 的内容。"""

    def __init__(
self,
grid: gridlib.Grid,
title: str
= "打印标题",
margins
=(wx.Point(10, 10), wx.Point(10, 10)),
):
super().
__init__()
self.grid
=grid
self.title
=title
self.margins
=margins#获取网格的行列数 self.row_count =self.grid.GetNumberRows()
self.col_count
=self.grid.GetNumberCols()
self.current_row
= 0 #记录当前打印的行数

我们记录表格对象,以及表格的行数和列数,以及当前打印的行数,默认从0开始,在每次计算分页的时候,需要知道当前的记录才能接着绘制剩余的记录。

我们通过转换逻辑单位,获得绘制设备的页面高度和宽度,然后每行记录的高度,我们根据内容的实际高度+一定的空白间距,如下代码

        font =wx.Font(
FONTSIZE, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL
)
dc.SetFont(font)
#计算每行的高度 self.lineHeight = dc.GetCharHeight() + 5 #加上5像素的间距

计算每页可以绘制多少行,需要记录摒除标题内容、表格标题行以及相关空白,剩下的页面高度除以一行的高度,进行计算获得即可。如下代码所示。

        #标题高度 + 标题底部空白行高度 + 表头高度
        y_offset = 20 + 20 + 20
        #计算每页可以显示的行数
        self.linesPerPage = int((self.pageHeight - y_offset) // self.lineHeight)

然后,我们在PrintOut的子类实现中重写 OnPreparePrinting(self) 函数,这个函数在绘制页面前执行一次,我们需要在其中计算出页面的数量。

        #计算页面数
        self.numPages = self.row_count //self.linesPerPageif self.row_count % self.linesPerPage !=0:
self.numPages
+= 1

然后在重写两个函数,决定是否存在下一页,以及页码信息,如下所示的函数实现。

    defHasPage(self, page):return page <=self.numPagesdefGetPageInfo(self):return (1, self.numPages, 1, self.numPages)

剩下的就是实现

def OnPrintPage(self, page: int) -> bool:

这个函数就是主要控制绘制内容和分页标识的处理的。

绘制的内容,主要根据起始位置,并设置相关的字体大小实现绘制即可,我们简单介绍一下,如下是在页面顶部的中间绘制标题。

        #绘制标题,居中显示
        text_width, text_height =dc.GetTextExtent(self.title)
title_x
= (width - text_width) // 2 #计算居中的X位置 dc.DrawText(self.title, int(title_x), int(y_offset))

线条的绘制也是类似,确定位置和颜色等,画线绘制即可。

        #设置线条颜色
        dc.SetPen(wx.Pen(wx.Colour(0, 0, 0)))  #黑色线条
        #将所有的坐标转换为整数
        dc.DrawLine(line_x, line_y, line_x + line_width, line_y)

绘制表头的时候,我们切换会正常的字体大小,然后遍历绘制

        #绘制表头
font.SetPointSize(FONTSIZE)
dc.SetFont(font)
for col inrange(self.col_count):
dc.DrawText(
self.grid.GetColLabelValue(col),
int(x_offset
+sum(col_widths[:col])),
int(y_offset),
)

其中的 col_widths 是我们前面根据表格的列数量进行计算的宽度集合

col_widths = [self.grid.GetColSize(col) for col in range(self.col_count)]

以上内容是我们每页都需要绘制的常规信息,因此先绘制报表表头、线条、表格标题行这些。

下面我们需要根据当前page的页码来计算当前开始的行记录,page为1的时候,那么当前的开始行号是0,否则就是根据页码计算数值。

self.current_row = (page - 1) * self.linesPerPage  #计算当前页面的起始行数

下面就是对数据进行分页的处理了,如果需要分页,在OnPrintPage函数中返回False,否则返回True,如下代码所示。

        #绘制表格的内容
        lines_on_page = 0  #记录当前页面的行数
        for row inrange(self.current_row, self.row_count):
y_offset
+= self.lineHeight #增加行的高度 for col inrange(self.col_count):
cell_value
=self.grid.GetCellValue(row, col)
dc.DrawText(
cell_value,
x_offset
+sum(col_widths[:col]),
y_offset,
)
lines_on_page
+= 1 #如果当前页面的行数已经达到最大值,则绘制页脚,并返回False,继续打印下一页 if lines_on_page >=self.linesPerPage:
self.draw_footer(dc, page)
#绘制页脚,底部的页码 return False #打印完一页后返回False,继续打印下一页 self.draw_footer(dc, page)return True

而底部的页码信息,我们简单绘制当前页面和页面总数即可,如下函数。

    defdraw_footer(self, dc: wx.DC, page: int):#绘制当前页码
        page_num_text = f"当前页码: {page} / {self.numPages}"page_num_width, page_num_height=dc.GetTextExtent(page_num_text)
dc.DrawText(
page_num_text,
int((self.pageWidth
- page_num_width) // 2),
int(self.y2),
)

最后获得随笔开始前介绍的效果。

如果没有分页信息,那么底部空白一些,还是会绘制页码信息,如下是没有更多记录的时候打印预览的界面效果

以上就是整个实现的过程,我们在WxPython开发框架中自定义PrintOut对象,实现WxPython跨平台开发框架之列表数据的通用打印处理过程。

一、前言

随着 Solon 3.0 和 Solon-Rx 3.0 发布,又迎来了的
RxSqlUtils 扩展插件
,用于“响应式”操作数据库。RxSqlUtils 是基于 R2dbc 和 Reactor 接口构建。极简风格,就像个工具类,故名:RxSqlUtils。

尤其在 solon-web-rx 和
Solon Cloud Gateway(基于纯响应式构建)
场景开发时,RxSqlUtils 会是最好的良配。

二、RxSqlUtils 使用

1、引入依赖

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-data-rx-sqlutils</artifactId>
</dependency>

2、新建数据库表(for H2)

CREATE TABLE `user`  (
  `id` bigint(20) not null,
  `name` varchar(255)  DEFAULT NULL,
  `title` varchar(255)  DEFAULT NULL,
  PRIMARY KEY (`id`)
);

3、定义实体类

使用了 lombok 的注解。

@Data
public class User {
    private Long id;
    private String name;
    private String title;
}

4、添加数据源配置

solon.dataSources:
  user!: # ‘!’结尾表示默认数据源
    class: "org.noear.solon.data.datasource.R2dbcConnectionFactory"
    r2dbcUrl: "r2dbc:h2:mem:///test;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;IGNORECASE=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE"

5、注入 RxSqlUtils 并使用

注入(这样就可以用了)

@Component
public class UserDao {
    @Inject
    private RxSqlUtils sqlUtils;
}

查询操作

public Flux<User> getAllUsers() {
    return sqlUtils.sql("select * from user")
                   .queryRowList(User.class);
}

新增操作

public Mono<Long> addUser(User user) {
    return sqlUtils.sql("INSERT INTO user (name , title) VALUES (?,?)", user.getName(), user.getTitle())
                   .updateReturnKey(Long.class);
}

更新操作

public Mono<Long> updateUser(User user) {
    return sqlUtils.sql("UPDATE user SET name=?, title=? WHERE id=?", user.getName(), user.getTitle(), user.getId())
                   .update();
}

总结

使用 RxSqlUtils 可以完成数据库的响应式操作,也有更好的透明性,使用简单和直接。

无论何时当我们要使用一个多值对象时,我们必须要清楚两个问题,一是这些值的顺序重要吗?二是允许重复值的存在吗?在编程语言中还会有其他的明确的信息,在UML中,只需明确这两个问题的答案即可确定对应的集合类型。

1.Set

Set是一个不允许存在重复值且未排序的集合。
例如一个骑行活动中,有许多骑手参加。每个骑手在一个骑行活动中只可能出现一次,即任一个特定的骑手在骑行活动中不可能重复出现;每个参与骑行活动的骑手之间没有什么特定或要求的顺序,他们是平等的。所以参与一个骑行活动的骑手不重复且无序,这些骑手就构成了一个Set。在类图中可表示如下:

在UML中,Set并没有专有的表示符号,它通过在多重性后添加表示不允许重复只能唯一的约束“{unique}”来表达。“unique”用于对集合中元素的唯一性进行约束,“{unique}”还有等价形式“{isunique=true}”。

2.Ordered Set

Ordered Set是一个不允许存在重复值且排序的集合。
例如一本书的作者可能有多人,而每位作者对一本书的贡献是不同的,所以书籍在出版时,一方面不会将一个作者列出来两次,另一方面作者的排序是非常重要的,即一本书的作者是不重复且有序的集合,他们构成了一个Ordered Set。以下类图中的属性author表达了这种特性。

在UML中,Ordered Set没有专有的表示符号,它通过在多重性后添加表示不允许重复并排序的的约束“{ordered, unique}”来表达。其中“ordered”表示集合是有序的,“{order}”也有一个等价形式“{isorder=true}”。

3.Bag

Bag是一个允许存在重复值且未排序的集合。
例如一个购物车中的商品,同一件商品可能会有多个,并且这些商品之间没有顺序可言,这些商品构成了一个Bag。
在UML中,Bag也没有专有的表示符号,它通过在多重性后添加表示允许重复(不唯一)、无序(不排序)的约束“{nonUnique, unordered}”来表达。在一些UML工具中,通常只支持添加“{unique}”和“{order}”约束,而缺省情况下,即未添加相关约束则表示相反的情况。故对于一个多重性条目,如果它是Bag,则不添加“{unique}”和“{order}”约束即可。
以下类图中属性items体现了它是一个Bag。

4.Sequence

Sequence是一个允许存在重复值且排序的集合,又称为Ordered Bag。
例如一个骑行活动的线路规划由多个点构成,而线路并不排斥两次甚至多次经过同一地点,但在线路上这些地点是有严格顺序的,所以骑行线路上的这些点构成了一个Sequence。在以下类图中属性itinerary描述了骑行线路。

在UML中,Sequence也没有专有的表示符号,它通过在多重性后添加表示不唯一且有序的约束来表达,如前所述,不标明唯一约束则表示不唯一,故仅使用约束“{ordered}”即可。或者,也可直接使用约束“{sequence}”或“{seq}”,在某些情况下,“{stream}”表达了相似的含义。
可以看到,在UML中集合类型的两个关键特性就是唯一性与有序性,这两个特性的不同组合对应了不同的集合类型。我们可以归纳如下表:
image

前言

大学的时候也写过和
Ingram
差不多的工具,不过那时候已经玩到没有兴致了,代码已不知道哪里去。没想到在Github看到了这个工具,实现思路和我的几乎一样,互联网就是这么神奇。

Ingram的Github:
https://github.com/jorhelp/Ingram

本着不能让服务器闲着的想法,开始吧

安装
Ingram

git clone https://github.com/jorhelp/Ingram.git
cd Ingram
pip3 install virtualenv
python3 -m virtualenv venv
source venv/bin/activate
pip3 install -r requirements.txt

安装后需要一个目标文件,这里用
masscan
来搞一波

安装
masscan

安装
masscan
的坑不少,有报错丢gpt就好了

安装步骤

sudo yum groupinstall "Development Tools"
git clone https://github.com/robertdavidgraham/masscan
cd masscan
make
make install

不出意外这里make失败,报错信息

src/massip-addr.c: In function ‘ipv6address_selftest’:
src/massip-addr.c:292:7: warning: excess elements in union initializer [enabled by default]
       {NULL, {{0, 0}, 0}}
       ^
src/massip-addr.c:292:7: warning: (near initialization for ‘tests[8].ip_addr.<anonymous>’) [enabled by default]
src/massip-addr.c:299:3: error: ‘for’ loop initial declarations are only allowed in C99 mode
   for (int i = 0; tests[i].name != NULL; i++) {
   ^
src/massip-addr.c:299:3: note: use option -std=c99 or -std=gnu99 to compile your code
src/massip-addr.c:296:13: warning: unused variable ‘ip’ [-Wunused-variable]
   ipaddress ip;
             ^
make: *** [tmp/massip-addr.o] Error 1

根据提示
note: use option -std=c99 or -std=gnu99 to compile your code
,修改 Makefile 来加入
-std=c99

-std=gnu99
选项。在 Makefile 中找到 CFLAGS 定义部分,并添加以下内容:

CFLAGS += -std=gnu99

然后重新
make
,这次就成功了

2024-12-27T09:08:58.png

完成之后,查看软件帮助信息:

masscan -h

获取活动主机

我这边ip段用的是纯真ip数据库,随便搜了一个地区

masscan x.x.0.0/16 -p80,8000-8008 -oL results --max-rate 8000

或者通过指定扫描文件

masscan -p80,8000-8008 -iL 目标文件 -oL 结果文件 --rate 8000

很快不用5分钟就扫描完了。扫描全网可以用下面的命令

masscan 0.0.0.0/0 -p0-65535 -oL all-results --max-rate 100000

masscan 运行完之后,将结果文件整理一下:
grep 'open' 结果文件 | awk '{printf"%s:%s\n", $4, $3}' > targets.txt

结果文件整理成
ip:port
的格式

开始最终的扫描

之后对这些主机进行扫描:
python run_ingram.py -i targets.txt -o out

2024-12-27T09:20:58.png

接下来就放在服务器慢慢跑吧


image

Python
包管理生态中存在多种工具,如
pip

pip-tools

poetry

conda
等,各自具备一定功能。

而今天介绍的
uv

Astral
公司推出的一款基于
Rust
编写的
Python
包管理工具,旨在成为 “
Python 的 Cargo
”。

它提供了快速、可靠且易用的包管理体验,在性能、兼容性和功能上都有出色表现,为
Python
项目的开发和管理带来了新的选择。

1. 为什么用uv

与其他
Python
中的包管理工具相比,
uv
更像是一个全能选手,它的优势在于:

  1. 速度快
    :得益于
    Rust

    uv
    工具的速度让人惊艳,比如安装依赖,速度比其他工具快很多
  2. 功能全面

    uv
    是“
    一站式服务
    ”的工具,从安装 Python、管理虚拟环境,到安装和管理包,再到管理项目依赖,它统统都能处理得很好
  3. 前景光明
    :背后有风投公司
    Astral
    支持,且采用了
    MIT
    许可,即使未来出现问题,社区也有应对的办法

使用
uv
,也可以像
NodeJS
或者
Rust
项目那样方便的管理依赖。

2. 如何安装

安装
uv
非常简单,可以使用官方提供的安装脚本,也可以通过
pip
来安装。

# On macOS and Linux.
curl -LsSf https://astral.sh/uv/install.sh | sh

# On Windows.
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# With pip.
pip install uv

安装之后,可以通过
uv help
命令检查是否安装成功:

3. 如何使用

下面演示如何使用
uv
来管理
Python
项目。

使用
uv
之前,创建一个
Python
项目对我来说就是创建一个文件夹而已。

使用
uv
之后,终于有了一些项目的感觉,对于
uv
,我使用时间也不长,疏漏或错误的地方欢迎指正!

接下来,从创建一个项目开始,演示我使用
uv
时常用的一些功能。

首先,介绍
uv
工具主要使用的两个文件:

  • pyproject.toml
    :定义项目的主要依赖,包括项目名称、版本、描述、支持的
    Python
    版本等信息
  • uv.lock
    :记录项目的所有依赖,包括依赖的依赖,且跨平台,确保在不同环境下安装的一致性。这个文件由
    uv
    自动管理,不要手动编辑

3.1. 创建项目

接下来,创建一个项目,使用
uv init <project dir>
命令。

$  uv init myproject
Initialized project `myproject` at `D:\projects\python\myproject`

$  cd .\myproject\

$  ls


    目录: D:\projects\python\myproject


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2024/12/27  12:06:08            109 .gitignore
-a----        2024/12/27  12:06:08              5 .python-version
-a----        2024/12/27  12:06:08             87 hello.py
-a----        2024/12/27  12:06:08            155 pyproject.toml
-a----        2024/12/27  12:06:08              0 README.md

通过
init
创建项目之后,
uv
工具贴心地帮助我们生成了一些默认文件。

其中
hello.py
只是一段演示用的代码,

随后我们可以根据实际的项目需要删除这个代码文件,换成自己的实际代码。

$  cat .\hello.py
def main():
    print("Hello from myproject!")


if __name__ == "__main__":
    main()

pyproject.toml
中是一些项目信息:

$  cat .\pyproject.toml
[project]
name = "myproject"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

注意

uv init
创建项目之后,会自动将项目使用
Git
来管理。

3.2. 操作环境

创建项目之后,我们进入项目根文件夹的第一件事就是同步项目依赖。

$  uv sync
Using CPython 3.12.4 interpreter at: D:\miniconda3\envs\databook\python.exe
Creating virtual environment at: .venv
Resolved 1 package in 15ms
Audited in 0.05ms

同步之后,会自动查找或下载合适的
Python
版本,创建并设置项目的虚拟环境,构建完整的依赖列表并写入

uv.lock
文件,最后将依赖同步到虚拟环境中。

我们这个是新创建的项目,没有什么依赖,所以
uv.lock
文件中的内容也比较简单。

$  ls


    目录: D:\projects\python\myproject


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2024/12/27  12:12:39                .venv
-a----        2024/12/27  12:06:08            109 .gitignore
-a----        2024/12/27  12:06:08              5 .python-version
-a----        2024/12/27  12:06:08             87 hello.py
-a----        2024/12/27  12:06:08            155 pyproject.toml
-a----        2024/12/27  12:06:08              0 README.md
-a----        2024/12/27  12:12:39            116 uv.lock

$  cat .\uv.lock
version = 1
requires-python = ">=3.12"

[[package]]
name = "myproject"
version = "0.1.0"
source = { virtual = "." }

uv sync
同步之后,就可以运行项目的代码了。

既然使用
uv
管理项目的话,我们就使用
uv
的命令来运行代码,不要像以前那样使用
python xxx.py
来运行。

我们可以试着运行项目创建时自动生成的代码。

$  uv run .\hello.py
Hello from myproject!

3.3. 管理依赖

管理依赖是我使用
uv
工具的主要目的,使用
uv
添加依赖非常简单,和
npm

cargo
差不多。

$  uv add pandas
Resolved 7 packages in 3.41s
Prepared 6 packages in 4.63s
Installed 6 packages in 1.80s
 + numpy==2.2.1
 + pandas==2.2.3
 + python-dateutil==2.9.0.post0
 + pytz==2024.2
 + six==1.17.0
 + tzdata==2024.2

尝试安装了一个
pandas
依赖(pandas依赖的包也自动安装了),从上面日志可以看出速度非常快。

这时再看看
uv.lock
文件的变化。

$  cat .\uv.lock
version = 1
requires-python = ">=3.12"

[[package]]
name = "myproject"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
    { name = "pandas" },
]

[package.metadata]
requires-dist = [{ name = "pandas", specifier = ">=2.2.3" }]

[[package]]
name = "pandas"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "numpy" },
    { name = "python-dateutil" },
    { name = "pytz" },
    { name = "tzdata" },
]

[[package]]
name = "pytz"
version = "2024.2"
source = { registry = "https://pypi.org/simple" }

上面的日志中我删除了很多内容,因为整体内容太多,详细记录了每个包以及它依赖的包的情况。

uv.lock
这个文件我们不要手动去编辑它,使用
uv
工具去管理它。

引入了pandas之后,我们看看是否可以在
hello.py
中使用。

$  cat .\hello.py
import pandas as pd


def main():
    print("Hello from myproject!")
    df = pd.DataFrame(
        {
            "A": [1, 2, 3],
            "B": [4, 5, 6],
        }
    )
    print(df)


if __name__ == "__main__":
    main()

$  uv run .\hello.py
Hello from myproject!
   A  B
0  1  4
1  2  5
2  3  6

可以正常使用安装的包
pandas
,下面在试试删除依赖会怎么样。

$  uv remove pandas
Resolved 1 package in 12ms
Uninstalled 6 packages in 1.18s
 - numpy==2.2.1
 - pandas==2.2.3
 - python-dateutil==2.9.0.post0
 - pytz==2024.2
 - six==1.17.0
 - tzdata==2024.2

$  cat .\uv.lock
version = 1
requires-python = ">=3.12"

[[package]]
name = "myproject"
version = "0.1.0"
source = { virtual = "." }

使用
uv remove
命令删除
pandas
包之后,也会自动删除
pandas
依赖的其他包,

我们看到
uv.lock
文件也恢复到最初的内容。

再试试运行
hello.py
看看。

$  uv run .\hello.py
Traceback (most recent call last):
  File "D:\projects\python\myproject\hello.py", line 1, in <module>
    import pandas as pd
ModuleNotFoundError: No module named 'pandas'

果然,无法运行了。

3.4. 区分开发和生产环境

还有一个比较常用的功能是区分
开发环境

生产环境
的依赖,这个功能在
NodeJS

Rust
中很常见。

比如,我们想把
pandas
安装到开发环境中,而把
requests
安装到生产环境中。

$  uv add --group dev pandas
Resolved 7 packages in 1.72s
Installed 6 packages in 1.39s
 + numpy==2.2.1
 + pandas==2.2.3
 + python-dateutil==2.9.0.post0
 + pytz==2024.2
 + six==1.17.0
 + tzdata==2024.2

$  uv add --group production requests
Resolved 12 packages in 2.72s
Prepared 5 packages in 1.31s
Installed 5 packages in 68ms
 + certifi==2024.12.14
 + charset-normalizer==3.4.1
 + idna==3.10
 + requests==2.32.3
 + urllib3==2.3.0

安装之后,
uv.lock
文件自动添加了各个包及其依赖,这里不再赘述。

从项目的
pyproject.toml
中可以看出不同环境的包依赖。

$  cat .\pyproject.toml
[project]
name = "myproject"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "pandas>=2.2.3",
]
production = [
    "requests>=2.32.3",
]

4. 未来发展

uv
也可以构建和发布
Python
包到
PyPi
,具体细节本篇就不展开了。

uv
自从发布后,团队一直致力于优先提升其跨平台的兼容性、性能和稳定性,帮助用户顺利将项目过渡到使用
uv
来管理。

长远来看,
uv
将发展成为一个完整的
Python
项目和包管理器,提供一站式的开发体验,涵盖从
Python
安装到项目管理的各个环节,进一步简化
Python
项目的开发流程,提高开发效率。