mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-03-21 02:10:11 +08:00
Merge branch 'master' of https://github.com/LCTT/TranslateProject into translating
This commit is contained in:
commit
5a3202bff1
@ -3,27 +3,26 @@
|
||||
[#]: author: "Jim Hall https://opensource.com/users/jim-hall"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: "perfiffer"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
[#]: reviewer: "wxy"
|
||||
[#]: publisher: "wxy"
|
||||
[#]: url: "https://linux.cn/article-14938-1.html"
|
||||
|
||||
我是如何使用 Linux sed 命令自动进行文件编辑
|
||||
如何使用 Linux sed 命令自动进行文件编辑
|
||||
======
|
||||
以下是从 Linux 命令行自动编辑文件的一些提示和技巧。
|
||||
|
||||
![computer screen][1]
|
||||

|
||||
|
||||
Image by: Opensource.com
|
||||
> 以下是从 Linux 命令行自动编辑文件的一些提示和技巧。
|
||||
|
||||
当我使用 Linux 命令行时,无论是在台式机上编写新程序还是在 Web 服务器上管理网站,我经常需要处理文本文件。Linux 提供了强大的工具,我可以利用这些工具来完成我的工作。我经常使用 `sed`,一个可以根据模式修改文本的编辑器。
|
||||
|
||||
`sed` 代表 *<ruby>流编辑器 <rt><rp>(</rp>stream editor<rp>)</rp></rt></ruby>*,它编辑文件中的文本并打印结果。使用 `sed` 的一种方法是识别一个字符串在文件中出现的次数,并将它们替换为不同的字符串。你可以使用 `sed` 来处理文本文件,其程度似乎无穷无尽,但我想分享一些使用 `sed` 来帮助我管理文件的方法。
|
||||
`sed` 代表 <ruby>流编辑器<rt>Stream EDitor</rt></ruby>,它编辑文件中的文本并打印结果。使用 `sed` 的一种方法是识别一个字符串在文件中的几次出现,并将它们替换为不同的字符串。使用 `sed` 来处理文本文件的方式似乎是无穷无尽的,但我想分享一些使用 `sed` 来帮助我管理文件的方法。
|
||||
|
||||
### 在 Linux 上搜索和替换文件中的文本
|
||||
|
||||
要使用 `sed`,你需要使用一个*正则表达式*。正则表达式是定义模式的一组特殊字符。我最常使用 `sed` 的例子是替换文件中的文本。替换文本的语法如下:`s/originaltext/newtext`。`s` 告诉 `sed` 执行文本替换或交换出现的文本。在斜线之间提供原始文本和新文本。
|
||||
|
||||
此语法将仅替换每行中第一次出现的 *<ruby>原始文本 <rt><rp>(</rp>originaltext<rp>)</rp></rt></ruby>*。要替换每个匹配项,即使在一行中原始文本出现了不止一次,也要将 `g` 追加到表达式的末尾。例如:`s/originaltext/newtext/g`。
|
||||
此语法将仅替换每行中第一次出现的 `originaltext`。要替换每个匹配项,即使在一行中原始文本出现了不止一次,要将 `g` 追加到表达式的末尾。例如:`s/originaltext/newtext/g`。
|
||||
|
||||
要在 `sed` 中使用此表达式,请使用 `-e` 选项指定此正则表达式:
|
||||
|
||||
@ -31,7 +30,7 @@ Image by: Opensource.com
|
||||
$ sed -e 's/originaltext/newtext/g'
|
||||
```
|
||||
|
||||
例如,假设我有一个名为 **game** 的 Makefile 文件,它模拟了 Conway 的生命游戏:
|
||||
例如,假设我有一个名为 `game` 程序的 Makefile 文件,该程序模拟了康威的《生命游戏》:
|
||||
|
||||
```
|
||||
.PHONY: all run clean
|
||||
@ -50,7 +49,7 @@ clean:
|
||||
$(RM) game
|
||||
```
|
||||
|
||||
**game** 这个名字并不是很有描述性,所以我可能会把它改名为 **life**。将 `game.c` 源文件重命名为 `life.c` 非常简单,但现在我需要修改 Makefile 以使用新名称。我可以使用 `sed` 来将所有的 **game** 更改为 **life**:
|
||||
`game` 这个名字并不是很有描述性,所以我想会把它改名为 `life`。将 `game.c` 源文件重命名为 `life.c` 非常简单,但现在我需要修改 Makefile 以使用新名称。我可以使用 `sed` 来将所有的 `game` 更改为 `life`:
|
||||
|
||||
```
|
||||
$ sed -e 's/game/life/g' Makefile
|
||||
@ -77,7 +76,7 @@ $ cp Makefile Makefile.old
|
||||
$ sed -e 's/game/life/g' Makefile.old > Makefile
|
||||
```
|
||||
|
||||
如果你确信你的更改正是你想要的,请使用 `-i` 或 `--in-place` 选项来编辑文件。但是,我建议添加一个备份文件后缀,类似于 `--in-place=.old`,用来备份原始文件,以备日后需要恢复时使用。它看起来像这样:
|
||||
如果你确信你的更改正是你想要的,请使用 `-i` 或 `--in-place` 选项来编辑文件。但是,我建议添加一个备份文件后缀,如 `--in-place=.old`,用来备份原始文件,以备日后需要恢复时使用。它看起来像这样:
|
||||
|
||||
```
|
||||
$ sed --in-place=.old -e 's/game/life/g' Makefile
|
||||
@ -87,9 +86,9 @@ Makefile Makefile.old
|
||||
|
||||
### 在 Linux 上使用 sed 引用文件
|
||||
|
||||
你可以使用正则表达式的其它功能来匹配特定的文本实例。例如,你可能需要替换出现在行首的文本。使用 `sed`,你可以将行的开头与插入字符 **^** 匹配。
|
||||
你可以使用正则表达式的其它功能来匹配特定的文本实例。例如,你可能需要替换出现在行首的文本。使用 `sed`,你可以用上尖号 `^` 来匹配行的开头。
|
||||
|
||||
我使用“行首”来替换文本的一种方式是当我需要在电子邮件中引用一个文件时。假设我想在电子邮件中共享我的 Makefile,但我不想将其作为文件附件包含在内。相反,我更喜欢在电子邮件正文中“引用”文件,在每行之前使用 **>**。我可以使用以下 `sed` 命令将编辑后的版本打印到我的终端,并将其复制粘贴到新的电子邮件中:
|
||||
我使用“行首”来替换文本的一种方式是当我需要在电子邮件中引用一个文件时。假设我想在电子邮件中共享我的 Makefile,但我不想将其作为文件附件包含在内。相反,我更喜欢在电子邮件正文中“引用”文件,在每行之前使用 `>`。我可以使用以下 `sed` 命令将编辑后的版本打印到我的终端,并将其复制粘贴到新的电子邮件中:
|
||||
|
||||
```
|
||||
$ sed -e 's/^/>/' Makefile
|
||||
@ -109,7 +108,7 @@ $ sed -e 's/^/>/' Makefile
|
||||
> $(RM) life
|
||||
```
|
||||
|
||||
`s/^/>/` 正则表达式匹配每行的开头(**^**),并在那里放置一个 **>**。实际上,这相当于每行都以 **>** 符号开始。
|
||||
`s/^/>/` 正则表达式匹配每行的开头(`^`),并在那里放置一个 `>`。实际上,这相当于每行都以 `>` 符号开始。
|
||||
|
||||
制表符可能无法在电子邮件中正确显示,但我可以通过添加另一个正则表达式将 Makefile 中的所有制表符替换为几个空格:
|
||||
|
||||
@ -133,7 +132,7 @@ $ sed -e 's/^/>/' -e 's/\t/ /g' Makefile
|
||||
|
||||
`\t` 表示文字制表符,因此 `s/\t/ /g` 告诉 `sed` 用输出中的两个空格替换输入中的所有制表符。
|
||||
|
||||
如果你需要对文件进行大量编辑,你可以将 `-e` 命令保存在文件中并使用 `-f` 选项来告诉 `sed` 将该文件用作"脚本"。如果你需要经常进行相同的编辑,这种方法特别有用。我已经准备了 `quotemail.sed` 的脚本文件来在我的电子邮件中引用 Makefile:
|
||||
如果你需要对文件进行大量编辑,你可以将 `-e` 命令保存在文件中,并使用 `-f` 选项来告诉 `sed` 将该文件用作“脚本”。如果你需要经常进行相同的编辑,这种方法特别有用。我已经准备了 `quotemail.sed` 的脚本文件来在我的电子邮件中引用 Makefile:
|
||||
|
||||
```
|
||||
$ cat quotemail.sed
|
||||
@ -167,7 +166,7 @@ via: https://opensource.com/article/22/8/automate-file-edits-sed-linux
|
||||
作者:[Jim Hall][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[perfiffer](https://github.com/perfiffer)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
@ -3,16 +3,16 @@
|
||||
[#]: author: "Sagar Sharma https://itsfoss.com/author/sagar/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: "geekpi"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
[#]: reviewer: "wxy"
|
||||
[#]: publisher: "wxy"
|
||||
[#]: url: "https://linux.cn/article-14939-1.html"
|
||||
|
||||
Sunamu:在 Linux 桌面上显示当前播放音乐的歌词
|
||||
======
|
||||
|
||||
作为一个吸睛的**音乐小部件**(或控制器)。
|
||||

|
||||
|
||||
这是 Sunamu 的唯一专注,它工作得很好。
|
||||
作为一个吸睛的**音乐小部件**(或控制器) —— 这是 Sunamu 唯一专注的事情,它工作得很好。
|
||||
|
||||
Sunamu 是一个有趣的工具。它不是音乐播放器,但可让你显示正在播放的音乐并对其进行控制。
|
||||
|
||||
@ -24,19 +24,19 @@ Sunamu 是一个有趣的工具。它不是音乐播放器,但可让你显示
|
||||
|
||||
![playing music with sunamu][1]
|
||||
|
||||
正如你在上面的截图中所注意到的,它看起来是一种非常好的方式来显示正在播放的音乐,带有歌词,同时具有基本的控件。
|
||||
正如你在上面的截图中所注意到的,它看起来是一种显示正在播放的音乐的非常好的方式,带有歌词,同时具有基本的控件。
|
||||
|
||||
你可以播放/暂停、转到下一首/上一首曲目、随机播放和启用循环。
|
||||
|
||||
Sunamu 支持多种音频平台,包括 Spotify。它还检测本地收藏中的音乐,支持一些可用于 Linux 的[最佳音乐播放器][2]。
|
||||
Sunamu 支持多种音频平台,包括 Spotify。它还可以检测本地收藏中的音乐,支持一些可用于 Linux 的 [最佳音乐播放器][2]。
|
||||
|
||||
此外,它还支持 Windows。因此,如果你通过 Windows 上的 Microsoft Edge 浏览器流式传输某些内容,它应该可以正常工作。
|
||||
此外,它还支持 Windows。因此,如果你通过 Windows 上的 Edge 浏览器流式传输某些内容,它应该可以正常工作。
|
||||
|
||||
你可以查看其 GitHub 页面上的[兼容性列表][3]以了解有关支持的播放器和浏览器的更多信息。
|
||||
你可以查看其 GitHub 页面上的 [兼容性列表][3] 以了解有关支持的播放器和浏览器的更多信息。
|
||||
|
||||
幸运的是,你不必受限于它默认提供的功能。它提供了一种调整配置文件的简单方法(在其 [GitHub 页面][4]上了解更多信息)。这使得新手可以调整一些设置并获得乐趣。
|
||||
幸运的是,你不必受限于它默认提供的功能。它提供了一种调整配置文件的简单方法(在其 [GitHub 页面][4] 上可以了解更多信息)。这使得新手可以调整一些设置并获得乐趣。
|
||||
|
||||
我将在本文的后面部分提到一些关于它的提示。
|
||||
我将在本文的后面部分提到一些关于它的技巧。
|
||||
|
||||
### Sunamu 的特点
|
||||
|
||||
@ -45,7 +45,7 @@ Sunamu 支持多种音频平台,包括 Spotify。它还检测本地收藏中
|
||||
Sunamu 具有一些不错的特性,其中一些是:
|
||||
|
||||
* 检测并显示当前正在播放的歌曲。
|
||||
* 从专辑封面中获取配色方案并使用相同的调色板以获得更好的视觉效果。
|
||||
* 从专辑封面中获取配色方案,并使用相同的调色板以获得更好的视觉效果。
|
||||
* 可通过配置文件进行定制。
|
||||
* 与 Discord 完美集成。
|
||||
* 消耗最少的系统资源。
|
||||
@ -56,7 +56,7 @@ Sunamu 具有一些不错的特性,其中一些是:
|
||||
|
||||
它提供 AppImage、deb 和 rpm 包,以便在各种 Linux 发行版中轻松安装。我使用 AppImage 进行测试,并且非常好用。
|
||||
|
||||
如果你是 Linux 新手,你还可以从我们关于[如何使用 AppImage][7] 或[安装 deb 包][8]和 [rpm 包][9]的指南中受益。
|
||||
如果你是 Linux 新手,你还可以从我们关于 [如何使用 AppImage][7] 或 [安装 deb 包][8]、[rpm 包][9] 的指南中得到帮助。
|
||||
|
||||
有趣的是,Sunamu 是少数为基于 ARM 的机器提供直接支持的开源音乐工具之一。
|
||||
|
||||
@ -64,7 +64,7 @@ Sunamu 具有一些不错的特性,其中一些是:
|
||||
|
||||
**让我通过终端向你展示基于 Debian 的发行版的快速安装方法**。只需按照给定的说明进行操作,你就可以开始使用了:
|
||||
|
||||
首先,让我们使用 wget 命令下载 .deb 包,如下所示:
|
||||
首先,让我们使用 `wget` 命令下载 .deb 包,如下所示:
|
||||
|
||||
```
|
||||
wget https://github.com/NyaomiDEV/Sunamu/releases/download/v2.0.0/sunamu_2.0.0_amd64.deb
|
||||
@ -78,11 +78,11 @@ sudo dpkg -i sunamu_2.0.0_amd64.deb
|
||||
|
||||
![install sunamu in ubuntu][11]
|
||||
|
||||
### 提示:调整配置文件
|
||||
### 技巧:调整配置文件
|
||||
|
||||
默认情况下,Sunamu 不会从专辑封面中获取颜色,而是显示每首歌曲的歌词。和许多其他人一样,我喜欢避免阅读歌词。
|
||||
默认情况下,Sunamu 不会从专辑封面中获取颜色,而是显示每首歌曲的歌词。和许多其他人一样,我喜欢不看歌词。
|
||||
|
||||
Sunamu 的配置文件通常位于**\~/.config/sunamu/config.json5**。
|
||||
Sunamu 的配置文件通常位于 `~/.config/sunamu/config.json5`。
|
||||
|
||||
要打开 Sunamu 配置文件,请输入给定的命令:
|
||||
|
||||
@ -90,7 +90,7 @@ Sunamu 的配置文件通常位于**\~/.config/sunamu/config.json5**。
|
||||
nano ~/.config/sunamu/config.json5
|
||||
```
|
||||
|
||||
如下所示在 electron 部分进行更改(启用颜色和禁用歌词):
|
||||
如下所示在 `electron` 部分进行更改(启用颜色并禁用歌词):
|
||||
|
||||
```
|
||||
electron: {
|
||||
@ -107,9 +107,9 @@ electron: {
|
||||
|
||||
![modify config file of sunamu][12]
|
||||
|
||||
### 最后的想法
|
||||
### 总结
|
||||
|
||||
除非你是避免使用基于 electron 应用的人,否则 Sunamu 是一款足以增强你在 Linux 上的音乐体验的应用。继 [Amberol][13] 之后,这是我最近喜欢的第二款音乐相关应用。
|
||||
除非你是避免使用基于 Electron 应用的人,否则 Sunamu 是一款足以增强你在 Linux 上的音乐体验的应用。继 [Amberol][13] 之后,这是我最近喜欢的第二款音乐相关应用。
|
||||
|
||||
如果你尝试过,请不要忘记在评论部分分享你的经验。
|
||||
|
||||
@ -120,7 +120,7 @@ via: https://itsfoss.com/sunamu-music-widget/
|
||||
作者:[Sagar Sharma][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[geekpi](https://github.com/geekpi)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
@ -0,0 +1,100 @@
|
||||
[#]: subject: "Deepin 23 is Introducing a New Package Format and Repository, Sounds Interesting!"
|
||||
[#]: via: "https://news.itsfoss.com/deepin-23/"
|
||||
[#]: author: "Ankush Das https://news.itsfoss.com/author/ankush/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
Deepin 23 is Introducing a New Package Format and Repository, Sounds Interesting!
|
||||
======
|
||||
deepin 23 will be an interesting upgrade with a couple of fundamental changes. What do you think?
|
||||
|
||||
![deepin 23][1]
|
||||
|
||||
Deepin remains one of the most [beautiful Linux distributions][2] out there.
|
||||
|
||||
While the entire user experience may not be hassle-free, it certainly looks good. The developers of deepin have experimented with intriguing customizations that make it stand out from the crowd.
|
||||
|
||||
If you are learning about it for the first time, you should know that it is one of the [interesting distributions based on Debian Linux][3].
|
||||
|
||||
With [deepin 20][4] and its recent point releases, we had a nice list of upgrades. Now, it looks like **deepin 23** will be the next major upgrade.
|
||||
|
||||
### Deepin 23 Preview: What’s New?
|
||||
|
||||
![deepin 23][5]
|
||||
|
||||
Deepin 23 preview has been made available for testing. Unlike its previous version, deepin 23 includes some fundamental upgrades that impact the user experience on several levels.
|
||||
|
||||
The key highlights include:
|
||||
|
||||
* A new package format.
|
||||
* A new idea for system updates.
|
||||
* A new repository.
|
||||
* New wallpapers.
|
||||
* [Linux Kernel 5.18][6].
|
||||
|
||||
### New Package Format: Linglong
|
||||
|
||||
![][7]
|
||||
|
||||
**Linglong** is the new package format developed by deepin.
|
||||
|
||||
It aims at solving various compatibility problems caused by complex dependencies of traditional package formats under Linux. Furthermore, reducing security risks by supporting sandboxing along with incremental updates and privacy protection.
|
||||
|
||||
You can find the packages on its [Linglong store][8] as of now.
|
||||
|
||||
Do we need another package format? I don’t think so.
|
||||
|
||||
With Flatpak and Snap around, it does not sound like anything that’s never been done before.
|
||||
|
||||
To me, it looks like deepin simply wants to have its own package format as part of its offerings.
|
||||
|
||||
### Atomic Updates
|
||||
|
||||
The system updates will be treated as atomic operations, i.e., that when packages are successfully installed, the update completes. If the installation fails, the system can be reverted to the previous version with no changes.
|
||||
|
||||
So, you get system rollback support after an upgrade and can avoid difficulties with partial upgrades.
|
||||
|
||||
### Independent Upstream
|
||||
|
||||
While it is based on Debian, deepin aims to have a separate repository for the core packages and some optional components.
|
||||
|
||||
For the preview release, the developers mention that they intend to learn from the upstream like Debian and Arch Linux to improve their repository.
|
||||
|
||||
### New Wallpapers
|
||||
|
||||
![deepin 23][9]
|
||||
|
||||
Like every other major release, deepin 23 adds some refreshing wallpapers and new default wallpaper.
|
||||
|
||||
### Download Deepin 23 Preview
|
||||
|
||||
Note that Deepin 23 stable is not yet available. So, if you want to experiment on your test system, and experience what’s in store for you, try the preview version.
|
||||
|
||||
You can find the download links to the ISO file in the [official announcement post][10].
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://news.itsfoss.com/deepin-23/
|
||||
|
||||
作者:[Ankush Das][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://news.itsfoss.com/author/ankush/
|
||||
[b]: https://github.com/lkxed
|
||||
[1]: https://news.itsfoss.com/wp-content/uploads/2022/08/deepin-23-release.jpg
|
||||
[2]: https://itsfoss.com/beautiful-linux-distributions/
|
||||
[3]: https://itsfoss.com/debian-based-distros/
|
||||
[4]: https://itsfoss.com/deepin-20-review/
|
||||
[5]: https://news.itsfoss.com/wp-content/uploads/2022/08/deepin-23-preview-screenshot.jpg
|
||||
[6]: https://news.itsfoss.com/linux-kernel-5-18-release/
|
||||
[7]: https://news.itsfoss.com/wp-content/uploads/2022/08/deepin-23-linglong.png
|
||||
[8]: https://store.linglong.dev/
|
||||
[9]: https://news.itsfoss.com/wp-content/uploads/2022/08/deepin-23-wallpapers-preview.jpg
|
||||
[10]: https://www.deepin.org/en/linux-system-distribution-deepin-23-preview-released/
|
@ -2,11 +2,10 @@
|
||||
[#]: via: "https://opensource.com/article/22/5/cloud-service-providers-open"
|
||||
[#]: author: "Seth Kenlon https://opensource.com/users/seth"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: translator: "FelixYFZ "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
Cloud service providers: How to keep your options open
|
||||
======
|
||||
No matter what level of openness your cloud service operates on, you have choices for your own environment.
|
||||
|
@ -2,7 +2,7 @@
|
||||
[#]: via: "https://opensource.com/article/22/8/coding-advice-new-programmers"
|
||||
[#]: author: "Sachin Samal https://opensource.com/users/sacsam005"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: translator: "lkxed"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
@ -2,11 +2,10 @@
|
||||
[#]: via: (https://opensource.com/article/21/7/what-security-policy)
|
||||
[#]: author: (Chris Collins https://opensource.com/users/clcollins)
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: ( )
|
||||
[#]: translator: (FelixYFZ )
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
|
||||
What you need to know about security policies
|
||||
======
|
||||
Learn about protecting your personal computer, server, and cloud systems
|
||||
|
@ -1,513 +0,0 @@
|
||||
[#]: subject: "3 ways to test your API with Python"
|
||||
[#]: via: "https://opensource.com/article/21/9/unit-test-python"
|
||||
[#]: author: "Miguel Brito https://opensource.com/users/miguendes"
|
||||
[#]: collector: "lujun9972"
|
||||
[#]: translator: "Yufei-Yan"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
3 ways to test your API with Python
|
||||
======
|
||||
Unit testing can be daunting, but these Python modules will make your
|
||||
life much easier.
|
||||
![Puzzle pieces coming together to form a computer screen][1]
|
||||
|
||||
In this tutorial, you'll learn how to unit test code that performs HTTP requests. In other words, you'll see the art of API unit testing in Python.
|
||||
|
||||
Unit tests are meant to test a single unit of behavior. In testing, a well-known rule of thumb is to isolate code that reaches external dependencies.
|
||||
|
||||
For instance, when testing a code that performs HTTP requests, it's recommended to replace the real call with a fake call during test time. This way, you can unit test it without performing a real HTTP request every time you run the test.
|
||||
|
||||
The question is, _how can you isolate the code?_
|
||||
|
||||
Hopefully, that's what I'm going to answer in this post! I'll not only show you how to do it but also weigh the pros and cons of three different approaches.
|
||||
|
||||
Requirements:
|
||||
|
||||
* [Python 3.8][2]
|
||||
* pytest-mock
|
||||
* requests
|
||||
* flask
|
||||
* responses
|
||||
* VCR.py
|
||||
|
||||
|
||||
|
||||
### Demo app using a weather REST API
|
||||
|
||||
To put this problem in context, imagine that you're building a weather app. This app uses a third-party weather REST API to retrieve weather information for a particular city. One of the requirements is to generate a simple HTML page, like the image below:
|
||||
|
||||
![web page displaying London weather][3]
|
||||
|
||||
The weather in London, OpenWeatherMap. Image is the author's own.
|
||||
|
||||
To get the information about the weather, you must find it somewhere. Fortunately, [OpenWeatherMap][2] provides everything you need through its REST API service.
|
||||
|
||||
_Ok, that's cool, but how can I use it?_
|
||||
|
||||
You can get everything you need by sending a `GET` request to: `https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={api_key}&units=metric`. For this tutorial, I'll parameterize the city name and settle on the metric unit.
|
||||
|
||||
### Retrieving the data
|
||||
|
||||
To retrieve the weather data, use `requests`. You can create a function that receives a city name as a parameter and returns a JSON. The JSON will contain the temperature, weather description, sunset, sunrise time, and so on.
|
||||
|
||||
The example below illustrates such a function:
|
||||
|
||||
|
||||
```
|
||||
def find_weather_for(city: str) -> dict:
|
||||
"""Queries the weather API and returns the weather data for a particular city."""
|
||||
url = API.format(city_name=city, api_key=API_KEY)
|
||||
resp = requests.get(url)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
The URL is made up of two global variables:
|
||||
|
||||
|
||||
```
|
||||
BASE_URL = "<https://api.openweathermap.org/data/2.5/weather>"
|
||||
API = BASE_URL + "?q={city_name}&appid={api_key}&units=metric"
|
||||
```
|
||||
|
||||
The API returns a JSON in this format:
|
||||
|
||||
|
||||
```
|
||||
{
|
||||
"coord": {
|
||||
"lon": -0.13,
|
||||
"lat": 51.51
|
||||
},
|
||||
"weather": [
|
||||
{
|
||||
"id": 800,
|
||||
"main": "Clear",
|
||||
"description": "clear sky",
|
||||
"icon": "01d"
|
||||
}
|
||||
],
|
||||
"base": "stations",
|
||||
"main": {
|
||||
"temp": 16.53,
|
||||
"feels_like": 15.52,
|
||||
"temp_min": 15,
|
||||
"temp_max": 17.78,
|
||||
"pressure": 1023,
|
||||
"humidity": 72
|
||||
},
|
||||
"visibility": 10000,
|
||||
"wind": {
|
||||
"speed": 2.1,
|
||||
"deg": 40
|
||||
},
|
||||
"clouds": {
|
||||
"all": 0
|
||||
},
|
||||
"dt": 1600420164,
|
||||
"sys": {
|
||||
"type": 1,
|
||||
"id": 1414,
|
||||
"country": "GB",
|
||||
"sunrise": 1600407646,
|
||||
"sunset": 1600452509
|
||||
},
|
||||
"timezone": 3600,
|
||||
"id": 2643743,
|
||||
"name": "London",
|
||||
"cod": 200
|
||||
```
|
||||
|
||||
The data is returned as a Python dictionary when you call `resp.json()`. In order to encapsulate all the details, you can represent them as a `dataclass`. This class has a factory method that gets the dictionary and returns a `WeatherInfo` instance.
|
||||
|
||||
This is good because you keep the representation stable. For example, if the API changes the way it structures the JSON, you can change the logic in just one place, the `from_dict` method. Other parts of the code won't be affected. You can even get information from different sources and combine them in the `from_dict` method!
|
||||
|
||||
|
||||
```
|
||||
@dataclass
|
||||
class WeatherInfo:
|
||||
temp: float
|
||||
sunset: str
|
||||
sunrise: str
|
||||
temp_min: float
|
||||
temp_max: float
|
||||
desc: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "WeatherInfo":
|
||||
return cls(
|
||||
temp=data["main"]["temp"],
|
||||
temp_min=data["main"]["temp_min"],
|
||||
temp_max=data["main"]["temp_max"],
|
||||
desc=data["weather"][0]["main"],
|
||||
sunset=format_date(data["sys"]["sunset"]),
|
||||
sunrise=format_date(data["sys"]["sunrise"]),
|
||||
)
|
||||
```
|
||||
|
||||
Now, you'll create a function called `retrieve_weather`. You'll use this function to call the API and return a `WeatherInfo` so you can build your HTML page.
|
||||
|
||||
|
||||
```
|
||||
def retrieve_weather(city: str) -> WeatherInfo:
|
||||
"""Finds the weather for a city and returns a WeatherInfo instance."""
|
||||
data = find_weather_for(city)
|
||||
return WeatherInfo.from_dict(data)
|
||||
```
|
||||
|
||||
Good, you have the basic building blocks for our app. Before moving forward, unit test those functions.
|
||||
|
||||
### 1\. Testing the API using mocks
|
||||
|
||||
[According to Wikipedia][4], a mock object is an object that simulates the behavior of a real object by mimicking it. In Python, you can mock any object using the `unittest.mock` lib that is part of the standard library. To test the `retrieve_weather` function, you can then mock `requests.get` and return static data.
|
||||
|
||||
#### pytest-mock
|
||||
|
||||
For this tutorial, you'll use `pytest` as your testing framework of choice. The `pytest` library is very extensible through plugins. To accomplish our mocking goals, use `pytest-mock`. This plugin abstracts a bunch of setups from `unittest.mock` and makes your testing code very concise. If you are curious, I discuss more about it in [another blog post][5].
|
||||
|
||||
_Ok, enough talking, show me the code._
|
||||
|
||||
Here's a complete test case for the `retrieve_weather` function. This test uses two fixtures: One is the `mocker` fixture provided by the `pytest-mock` plugin. The other one is ours. It's just the static data you saved from a previous request.
|
||||
|
||||
|
||||
```
|
||||
@pytest.fixture()
|
||||
def fake_weather_info():
|
||||
"""Fixture that returns a static weather data."""
|
||||
with open("tests/resources/weather.json") as f:
|
||||
return json.load(f)
|
||||
|
||||
[/code] [code]
|
||||
|
||||
def test_retrieve_weather_using_mocks(mocker, fake_weather_info):
|
||||
"""Given a city name, test that a HTML report about the weather is generated
|
||||
correctly."""
|
||||
# Creates a fake requests response object
|
||||
fake_resp = mocker.Mock()
|
||||
# Mock the json method to return the static weather data
|
||||
fake_resp.json = mocker.Mock(return_value=fake_weather_info)
|
||||
# Mock the status code
|
||||
fake_resp.status_code = HTTPStatus.OK
|
||||
|
||||
mocker.patch("weather_app.requests.get", return_value=fake_resp)
|
||||
|
||||
weather_info = retrieve_weather(city="London")
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
If you run the test, you get the following output:
|
||||
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
...[omitted]...
|
||||
tests/test_weather_app.py::test_retrieve_weather_using_mocks PASSED [100%]
|
||||
============================== 1 passed in 0.20s ===============================
|
||||
Process finished with exit code 0
|
||||
```
|
||||
|
||||
Great, your tests pass! But... Life is not a bed of roses. This test has pros and cons. I'll take a look at them.
|
||||
|
||||
#### Pros
|
||||
|
||||
Well, one pro already discussed is that by mocking the API's return, you make your tests easier. Isolate the communication with the API and make the test predictable. It will always return what you want.
|
||||
|
||||
#### Cons
|
||||
|
||||
As for cons, the problem is, what if you don't want to use `requests` anymore and decide to go with the standard library's `urllib`. Every time you change the implementation of `find_weather_for`, you will have to adapt the test. A good test doesn't change when your implementation changes. So, by mocking, you end up coupling your test with the implementation.
|
||||
|
||||
Also, another downside is the amount of setup you have to do before calling the function—at least three lines of code.
|
||||
|
||||
|
||||
```
|
||||
...
|
||||
# Creates a fake requests response object
|
||||
fake_resp = mocker.Mock()
|
||||
# Mock the json method to return the static weather data
|
||||
fake_resp.json = mocker.Mock(return_value=fake_weather_info)
|
||||
# Mock the status code
|
||||
fake_resp.status_code = HTTPStatus.OK
|
||||
...
|
||||
```
|
||||
|
||||
_Can I do better?_
|
||||
|
||||
Yes, please, follow along. I'll see now how to improve it a bit.
|
||||
|
||||
### Using responses
|
||||
|
||||
Mocking `requests` using the `mocker` feature has the downside of having a long setup. A good way to avoid that is to use a library that intercepts `requests` calls and patches them. There is more than one lib for that, but the simplest to me is `responses`. Let's see how to use it to replace `mock`.
|
||||
|
||||
|
||||
```
|
||||
@responses.activate
|
||||
def test_retrieve_weather_using_responses(fake_weather_info):
|
||||
"""Given a city name, test that a HTML report about the weather is generated
|
||||
correctly."""
|
||||
api_uri = API.format(city_name="London", api_key=API_KEY)
|
||||
responses.add(responses.GET, api_uri, json=fake_weather_info, status=HTTPStatus.OK)
|
||||
|
||||
weather_info = retrieve_weather(city="London")
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
Again, this function makes use of our `fake_weather_info` fixture.
|
||||
|
||||
Next, run the test:
|
||||
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
...
|
||||
tests/test_weather_app.py::test_retrieve_weather_using_responses PASSED [100%]
|
||||
============================== 1 passed in 0.19s ===============================
|
||||
```
|
||||
|
||||
Excellent! This test pass too. But... It's still not that great.
|
||||
|
||||
#### Pros
|
||||
|
||||
The good thing about using libraries like `responses` is that you don't need to patch `requests` ourselves. You save some setup by delegating the abstraction to the library. However, in case you haven't noticed, there are problems.
|
||||
|
||||
#### Cons
|
||||
|
||||
Again, the problem is, much like `unittest.mock`, your test is coupled to the implementation. If you replace `requests`, your test breaks.
|
||||
|
||||
### 2\. Testing the API using an adapter
|
||||
|
||||
_If by using mocks I couple our tests, what can I do?_
|
||||
|
||||
Imagine the following scenario: Say that you can no longer use `requests`, and you'll have to replace it with `urllib` since it comes with Python. Not only that, you learned the lesson of not coupling test code with implementation, and you want to avoid that in the future. You want to replace `urllib` and not have to rewrite the tests.
|
||||
|
||||
It turns out you can abstract away the code that performs the `GET` request.
|
||||
|
||||
_Really? How?_
|
||||
|
||||
You can abstract it by using an adapter. The adapter is a design pattern used to encapsulate or wrap the interface of other classes and expose it as a new interface. This way, you can change the adapters without changing our code. For example, you can encapsulate the details about `requests` in our `find_weather_for` and expose it via a function that takes only the URL.
|
||||
|
||||
So, this:
|
||||
|
||||
|
||||
```
|
||||
def find_weather_for(city: str) -> dict:
|
||||
"""Queries the weather API and returns the weather data for a particular city."""
|
||||
url = API.format(city_name=city, api_key=API_KEY)
|
||||
resp = requests.get(url)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
Becomes this:
|
||||
|
||||
|
||||
```
|
||||
def find_weather_for(city: str) -> dict:
|
||||
"""Queries the weather API and returns the weather data for a particular city."""
|
||||
url = API.format(city_name=city, api_key=API_KEY)
|
||||
return adapter(url)
|
||||
```
|
||||
|
||||
And the adapter becomes this:
|
||||
|
||||
|
||||
```
|
||||
def requests_adapter(url: str) -> dict:
|
||||
resp = requests.get(url)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
Now it's time to refactor our `retrieve_weather` function:
|
||||
|
||||
|
||||
```
|
||||
def retrieve_weather(city: str) -> WeatherInfo:
|
||||
"""Finds the weather for a city and returns a WeatherInfo instance."""
|
||||
data = find_weather_for(city, adapter=requests_adapter)
|
||||
return WeatherInfo.from_dict(data)
|
||||
```
|
||||
|
||||
So, if you decide to change this implementation to one that uses `urllib`, just swap the adapters:
|
||||
|
||||
|
||||
```
|
||||
def urllib_adapter(url: str) -> dict:
|
||||
"""An adapter that encapsulates urllib.urlopen"""
|
||||
with urllib.request.urlopen(url) as response:
|
||||
resp = response.read()
|
||||
return json.loads(resp)
|
||||
|
||||
[/code] [code]
|
||||
|
||||
def retrieve_weather(city: str) -> WeatherInfo:
|
||||
"""Finds the weather for a city and returns a WeatherInfo instance."""
|
||||
data = find_weather_for(city, adapter=urllib_adapter)
|
||||
return WeatherInfo.from_dict(data)
|
||||
```
|
||||
|
||||
_Ok, how about the tests?_
|
||||
|
||||
To test r`etrieve_weather`, just create a fake adapter that is used during test time:
|
||||
|
||||
|
||||
```
|
||||
@responses.activate
|
||||
def test_retrieve_weather_using_adapter(
|
||||
fake_weather_info,
|
||||
):
|
||||
def fake_adapter(url: str):
|
||||
return fake_weather_info
|
||||
|
||||
weather_info = retrieve_weather(city="London", adapter=fake_adapter)
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
If you run the test you get:
|
||||
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
tests/test_weather_app.py::test_retrieve_weather_using_adapter PASSED [100%]
|
||||
============================== 1 passed in 0.22s ===============================
|
||||
```
|
||||
|
||||
#### Pros
|
||||
|
||||
The pro for this approach is that you successfully decoupled your test from the implementation. Use [dependency injection][6] to inject a fake adapter during test time. Also, you can swap the adapter at any time, including during runtime. You did all of this without changing the behavior.
|
||||
|
||||
#### Cons
|
||||
|
||||
The cons are that, since you're using a fake adapter for tests, if you introduce a bug in the adapter you employ in the implementation, your test won't catch it. For example, say that we pass a faulty parameter to `requests`, like this:
|
||||
|
||||
|
||||
```
|
||||
def requests_adapter(url: str) -> dict:
|
||||
resp = requests.get(url, headers=<some broken headers>)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
This adapter will fail in production, and the unit tests won't catch it. But truth to be told, you also have the same problem with the previous approach. That's why you always need to go beyond unit tests and also have integration tests. That being said, consider another option.
|
||||
|
||||
### 3\. Testing the API using VCR.py
|
||||
|
||||
Now it's finally the time to discuss our last option. I have only found about it quite recently, frankly. I've been using mocks for a long time and always had some problems with them. `VCR.py` is a library that simplifies a lot of the tests that make HTTP requests.
|
||||
|
||||
It works by recording the HTTP interaction the first time you run the test as a flat YAML file called a _cassette_. Both the request and the response are serialized. When you run the test for the second time, `VCR.py` will intercept the call and return a response for the request made.
|
||||
|
||||
Now see how to test `retrieve_weather` using `VCR.py below:`
|
||||
|
||||
|
||||
```
|
||||
@vcr.use_cassette()
|
||||
def test_retrieve_weather_using_vcr(fake_weather_info):
|
||||
weather_info = retrieve_weather(city="London")
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
_Wow, is that it? No setup? What is that `@vcr.use_cassette()`?_
|
||||
|
||||
Yes, that's it! There is no setup, just a `pytest` annotation to tell VCR to intercept the call and save the cassette file.
|
||||
|
||||
_What does the cassette file look like?_
|
||||
|
||||
Good question. There's a bunch of things in it. This is because VCR saves every detail of the interaction.
|
||||
|
||||
|
||||
```
|
||||
interactions:
|
||||
\- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- python-requests/2.24.0
|
||||
method: GET
|
||||
uri: [https://api.openweathermap.org/data/2.5/weather?q=London\&appid=\][7]<YOUR API KEY HERE>&units=metric
|
||||
response:
|
||||
body:
|
||||
string: '{"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":800,"main":"Clear","description":"clearsky","icon":"01d"}],"base":"stations","main":{"temp":16.53,"feels_like":15.52,"temp_min":15,"temp_max":17.78,"pressure":1023,"humidity":72},"visibility":10000,"wind":{"speed":2.1,"deg":40},"clouds":{"all":0},"dt":1600420164,"sys":{"type":1,"id":1414,"country":"GB","sunrise":1600407646,"sunset":1600452509},"timezone":3600,"id":2643743,"name":"London","cod":200}'
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Methods:
|
||||
- GET, POST
|
||||
Access-Control-Allow-Origin:
|
||||
- '*'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '454'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 18 Sep 2020 10:53:25 GMT
|
||||
Server:
|
||||
- openresty
|
||||
X-Cache-Key:
|
||||
- /data/2.5/weather?q=london&units=metric
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
```
|
||||
|
||||
_That's a lot!_
|
||||
|
||||
Indeed! The good thing is that you don't need to care much about it. `VCR.py` takes care of that for you.
|
||||
|
||||
#### Pros
|
||||
|
||||
Now, for the pros, I can list at least five things:
|
||||
|
||||
* No setup code.
|
||||
* Tests remain isolated, so it's fast.
|
||||
* Tests are deterministic.
|
||||
* If you change the request, like by using incorrect headers, the test will fail.
|
||||
* It's not coupled to the implementation, so you can swap the adapters, and the test will pass. The only thing that matters is that you request is the same.
|
||||
|
||||
|
||||
|
||||
#### Cons
|
||||
|
||||
Again, despite the enormous benefits compared to mocking, there are still problems.
|
||||
|
||||
If the API provider changes the format of the data for some reason, the test will still pass. Fortunately, this is not very frequent, and API providers usually version their APIs before introducing such breaking changes. Also, unit tests are not meant to access the external API, so there isn't much to do here.
|
||||
|
||||
Another thing to consider is having end-to-end tests in place. These tests will call the server every time it runs. As the name says, it's a more broad test and slow. They cover a lot more ground than unit tests. In fact, not every project will need to have them. So, in my view, `VCR.py` is more than enough for most people's needs.
|
||||
|
||||
### Conclusion
|
||||
|
||||
This is it. I hope you've learned something useful today. Testing API client applications can be a bit daunting. Yet, when armed with the right tools and knowledge, you can tame the beast.
|
||||
|
||||
You can find the full app on [my GitHub][8].
|
||||
|
||||
* * *
|
||||
|
||||
_This article was originally published on the [author's personal blog][9] and has been adapted with permission._
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://opensource.com/article/21/9/unit-test-python
|
||||
|
||||
作者:[Miguel Brito][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://opensource.com/users/miguendes
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/puzzle_computer_solve_fix_tool.png?itok=U0pH1uwj (Puzzle pieces coming together to form a computer screen)
|
||||
[2]: https://miguendes.me/how-i-set-up-my-python-workspace
|
||||
[3]: https://opensource.com/sites/default/files/sbzkkiywh.jpeg
|
||||
[4]: https://en.wikipedia.org/wiki/Mock_object
|
||||
[5]: https://miguendes.me/7-pytest-plugins-you-must-definitely-use
|
||||
[6]: https://stackoverflow.com/questions/130794/what-is-dependency-injection
|
||||
[7]: https://api.openweathermap.org/data/2.5/weather?q=London\&appid=\
|
||||
[8]: https://github.com/miguendes/tutorials/tree/master/testing_http
|
||||
[9]: https://miguendes.me/3-ways-to-test-api-client-applications-in-python
|
@ -2,7 +2,7 @@
|
||||
[#]: via: "https://www.opensourceforu.com/2022/05/the-basic-concepts-of-shell-scripting/"
|
||||
[#]: author: "Sathyanarayanan Thangavelu https://www.opensourceforu.com/author/sathyanarayanan-thangavelu/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: translator: "FYJNEVERFOLLOWS"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
@ -179,7 +179,7 @@ via: https://www.opensourceforu.com/2022/05/the-basic-concepts-of-shell-scriptin
|
||||
|
||||
作者:[Sathyanarayanan Thangavelu][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
译者:[FYJNEVERFOLLOWS](https://github.com/FYJNEVERFOLLOWS)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
@ -0,0 +1,145 @@
|
||||
[#]: subject: "4 cool new projects to try in Copr for August 2022"
|
||||
[#]: via: "https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/"
|
||||
[#]: author: "Jiri Kyjovsky https://fedoramagazine.org/author/nikromen/"
|
||||
[#]: collector: "lujun9972"
|
||||
[#]: translator: " "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
4 cool new projects to try in Copr for August 2022
|
||||
======
|
||||
|
||||
![4 packages to try from the Copr repos][1]
|
||||
|
||||
[Copr][2] is a build system for anyone in the Fedora community. It hosts thousands of projects for various purposes and audiences. Some of them should never be installed by anyone, some are already being transitioned to the official Fedora Linux repositories, and the rest are somewhere in between. Copr gives you the opportunity to install third-party software that is not available in Fedora Linux repositories, try nightly versions of your dependencies, use patched builds of your favorite tools to support some non-standard use cases, and just experiment freely.
|
||||
|
||||
If you don’t know [how to enable a repository][3] or if you are concerned about whether [it is safe to use Copr][4], please consult the [project documentation][5].
|
||||
|
||||
This article takes a closer look at interesting projects that recently landed in Copr.
|
||||
|
||||
### **Ntfy**
|
||||
|
||||
[Ntfy][6] is a simple HTTP-based notification service that allows you to send notifications to your devices using scripts from any computer. To send notifications ntfy uses PUT/POST commands or it is possible to send notifications via ntfy __CLI without any registration or login_._ For this reason, choose a hard-to guess topic name, as this is essentially a password.
|
||||
|
||||
In the case of sending notifications, it is as simple as this:
|
||||
|
||||
```
|
||||
|
||||
$ ntfy publish beer-lovers "Hi folks. I love beer!"
|
||||
{"id":"4ZADC9KNKBse", "time":1649963662, "event":"message", "topic":"beer-lovers", "message":"Hi folks. I love beer!"}
|
||||
|
||||
```
|
||||
|
||||
And a listener who subscribes to this topic will receive:
|
||||
|
||||
```
|
||||
|
||||
$ ntfy subscribe beer-lovers
|
||||
{"id":"4ZADC9KNKBse", "time":1649963662, "event":"message", "topic":"beer-lovers", "message":"Hi folks. I love beer!"}
|
||||
|
||||
```
|
||||
|
||||
If you wish to receive notifications on your phone, then ntfy also has a [mobile app][7] for Android so you can send notifications from your laptop to your phone.
|
||||
|
||||
![][8]
|
||||
|
||||
#### **Installation instructions**
|
||||
|
||||
The [repo][9] currently provides _ntfy_ for Fedora Linux 35, 36, 37, and Fedora Rawhide. To install it, use these commands:
|
||||
|
||||
```
|
||||
|
||||
sudo dnf copr enable cyqsimon/ntfysh
|
||||
sudo dnf install ntfysh
|
||||
|
||||
```
|
||||
|
||||
### Koi
|
||||
|
||||
If you use light mode during the day but want to protect your eyesight overnight and switch to dark mode, you don’t have to do it manually anymore. [Koi][10] will do it for you!
|
||||
|
||||
Koi provides KDE Plasma Desktop functionality to automatically switch between light and dark mode according to your preferences. Just set the time and themes.
|
||||
|
||||
![][11]
|
||||
|
||||
#### **Installation instructions**
|
||||
|
||||
The [repo][12] currently provides _Koi_ for Fedora Linux 35, 36, 37, and Fedora Rawhide. To install it, use these commands:
|
||||
|
||||
```
|
||||
|
||||
sudo dnf copr enable birkch/Koi
|
||||
sudo dnf install Koi
|
||||
|
||||
```
|
||||
|
||||
### **SwayNotificationCenter**
|
||||
|
||||
[SwayNotificationCenter][13] provides a simple and nice looking GTK GUI for your desktop notifications.
|
||||
|
||||
You will find some key features such as do-not-disturb mode, a panel to view previous notifications, track pad/mouse gestures, support for keyboard shortcuts, and customizable widgets. SwayNotificationCenter also provides a good way to [configure and customize][14] via JSON and CSS files.
|
||||
|
||||
More information on <https://github.com/ErikReider/SwayNotificationCenter> with screenshots at the bottom of the page.
|
||||
|
||||
#### **Installation instructions**
|
||||
|
||||
The [repo][15] currently provides _SwayNotificationCenter_ for Fedora Linux 35, 36, 37, and Fedora Rawhide. To install it, use these commands:
|
||||
|
||||
```
|
||||
|
||||
sudo dnf copr enable erikreider/SwayNotificationCenter
|
||||
sudo dnf install SwayNotificationCenter
|
||||
|
||||
```
|
||||
|
||||
### **Webapp Manager**
|
||||
|
||||
Ever want to launch your favorite websites from one place? With [WebApp][16] manager, you can save your favorite websites and run them later as if they were an apps.
|
||||
|
||||
You can set a browser in which you want to open the website and much more. For example, with Firefox, all links are always opened within the WebApp.
|
||||
|
||||
![][17]
|
||||
|
||||
#### **Installation instructions**
|
||||
|
||||
The [repo][18] currently provides _WebApp_ for Fedora Linux 35, 36, 37, and Fedora Rawhide. To install it, use these commands:
|
||||
|
||||
```
|
||||
|
||||
sudo dnf copr enable perabyte/webapp-manager
|
||||
sudo dnf install webapp-manager
|
||||
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/
|
||||
|
||||
作者:[Jiri Kyjovsky][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://fedoramagazine.org/author/nikromen/
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://fedoramagazine.org/wp-content/uploads/2017/08/4-copr-945x400.jpg
|
||||
[2]: https://copr.fedorainfracloud.org/
|
||||
[3]: https://docs.pagure.org/copr.copr/how_to_enable_repo.html#how-to-enable-repo
|
||||
[4]: https://docs.pagure.org/copr.copr/user_documentation.html#is-it-safe-to-use-copr
|
||||
[5]: https://docs.pagure.org/copr.copr/user_documentation.html
|
||||
[6]: https://github.com/binwiederhier/ntfy
|
||||
[7]: https://play.google.com/store/apps/details?id=io.heckel.ntfy
|
||||
[8]: https://fedoramagazine.org/wp-content/uploads/2022/08/beer.jpg
|
||||
[9]: https://copr.fedorainfracloud.org/coprs/cyqsimon/ntfysh/
|
||||
[10]: https://github.com/baduhai/Koi
|
||||
[11]: https://fedoramagazine.org/wp-content/uploads/2022/08/Screenshot_20220813_133028.png
|
||||
[12]: https://copr.fedorainfracloud.org/coprs/birkch/Koi/
|
||||
[13]: https://github.com/ErikReider/SwayNotificationCenter
|
||||
[14]: https://github.com/ErikReider/SwayNotificationCenter#scripting
|
||||
[15]: https://copr.fedorainfracloud.org/coprs/erikreider/SwayNotificationCenter/
|
||||
[16]: https://github.com/linuxmint/webapp-manager
|
||||
[17]: https://fedoramagazine.org/wp-content/uploads/2022/08/Screenshot_20220810_182415.png
|
||||
[18]: https://copr.fedorainfracloud.org/coprs/perabyte/webapp-manager/
|
@ -0,0 +1,68 @@
|
||||
[#]: subject: "Desktop Linux Market Share: August 2022"
|
||||
[#]: via: "https://itsfoss.com/linux-market-share/"
|
||||
[#]: author: "Ankush Das https://itsfoss.com/author/ankush/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
Desktop Linux Market Share: August 2022
|
||||
======
|
||||
|
||||
Every year, we discuss the year of the Linux desktop. It can’t be helped when we see an increase in the operating system’s market share in the consumer space.
|
||||
|
||||
![linux desktop market share][1]
|
||||
|
||||
Of course, Linux dominates the entire cloud industry (Web host, cloud computing, data warehouse, etc.). Here, we focus only on the desktop Linux market share.
|
||||
|
||||
**If you’re new to the Linux world**, Linux is not an operating system, it is a kernel. But, we tend to term “Linux” as an OS to keep things simple.You can learn [what Linux is in our explainer article][2].
|
||||
|
||||
One day, we hope that Linux distributions dominate the desktop operating market share in the future. But, what do the current trends say? Is it the year of the Linux desktop yet?
|
||||
|
||||
The trends change every month. Last year, Linux could have a better grip over the market share compared to this year. So, it is vital to track the latest reports.
|
||||
|
||||
Here, we try to keep track of the latest trends in the form of monthly updated reports from different sources.
|
||||
|
||||
### Operating System Market Share: July 2022
|
||||
|
||||
We update the available information every month. Note that the information available for the last month gets published next month. So, for example, when we update the report in the month of August, it will include the statistics for July.
|
||||
|
||||
Among the desktop operating systems available (Windows, macOS, and Chrome OS), Linux usually tends to occupy the **third position** overall.
|
||||
|
||||
Some of the latest stats include:
|
||||
|
||||
* [Net Marketshare][3]: The current Linux market share is 1.68% compared to 6.09% for macOS and 91.40% for Windows.
|
||||
* [Statcounter][4]: Linux occupies 2.76% of the market share compared to 14.51% for macOS, and 75.21% for Windows.
|
||||
* [W3Schools][5] (last updated on May 2022): Linux has a grip on 4.2% of the market share, compared to 9.2% of macOS and 70% of Windows.
|
||||
* [Steam Survey][6]: In terms of desktop gaming, Linux has a market share of 1.23% when compared to 1.74% for macOS, and 97.03% for Windows.
|
||||
* [Statista][7] (last updated on June 2022): The Linux desktop market share was 2.42% when compared to 14.64% for macOS, and 76.33% for Windows.
|
||||
* [Stack Overflow Survey][8]: Among the developers who participate in the Stack Overflow survey, 40.23% of users use the Linux-based operating system for personal use and 39.89% of those use it for professional use.
|
||||
|
||||
Every source utilizes a different method of data collection. The market share constantly changes, which is why we decided to update this report regularly, instead of making separate posts on slight changes to the market share.
|
||||
|
||||
**Overall**, it looks like Linux as a desktop operating system is popular among developers, and is eventually influencing gamers, and other consumers as an alternative operating system.
|
||||
|
||||
*What do you think about the trends? Do you see Linux overtake macOS in terms of desktop market share? Share your thoughts in the comments below.*
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://itsfoss.com/linux-market-share/
|
||||
|
||||
作者:[Ankush Das][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://itsfoss.com/author/ankush/
|
||||
[b]: https://github.com/lkxed
|
||||
[1]: https://itsfoss.com/wp-content/uploads/2017/09/linux-desktop-market-share.jpg
|
||||
[2]: https://itsfoss.com/what-is-linux/
|
||||
[3]: https://www.netmarketshare.com/operating-system-market-share.aspx?options=%7B%22filter%22%3A%7B%22%24and%22%3A%5B%7B%22deviceType%22%3A%7B%22%24in%22%3A%5B%22Desktop%2Flaptop%22%5D%7D%7D%5D%7D%2C%22dateLabel%22%3A%22Custom%22%2C%22attributes%22%3A%22share%22%2C%22group%22%3A%22platform%22%2C%22sort%22%3A%7B%22share%22%3A-1%7D%2C%22id%22%3A%22platformsDesktop%22%2C%22dateInterval%22%3A%22Monthly%22%2C%22dateStart%22%3A%222021-12%22%2C%22dateEnd%22%3A%222022-07%22%2C%22segments%22%3A%22-1000%22%7D
|
||||
[4]: https://gs.statcounter.com/os-market-share/desktop/worldwide
|
||||
[5]: https://www.w3schools.com/browsers/browsers_os.asp
|
||||
[6]: https://store.steampowered.com/hwsurvey/Steam-Hardware-Software-Survey-Welcome-to-Steam?platform=linux
|
||||
[7]: https://www.statista.com/statistics/218089/global-market-share-of-windows-7/
|
||||
[8]: https://survey.stackoverflow.co/2022/#technology-most-popular-technologies
|
@ -0,0 +1,211 @@
|
||||
[#]: subject: "GNOME 43: Top New Features and Release Wiki"
|
||||
[#]: via: "https://www.debugpoint.com/gnome-43/"
|
||||
[#]: author: "Arindam https://www.debugpoint.com/author/admin1/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
GNOME 43: Top New Features and Release Wiki
|
||||
======
|
||||
An extensive feature analysis of the GNOME 43 desktop environment bringing impactful changes to your day-to-day needs and workflow.
|
||||
|
||||
![GNOME 43 Running via GNOME OS][1]
|
||||
|
||||
This article summarises all necessary information about GNOME 43, including features, release schedule and more. The GNOME 43 release (upcoming) is probably the most impactful release since the GNOME 40 in terms of the features and their impact on your workflow.
|
||||
|
||||
The feature includes updated and faster Shell performance, wrapping up GTK4 and libadwaita conversion, renovated Files and fantastic Web changes.
|
||||
|
||||
All these necessary changes were long overdue and will change your traditional workflow in the GNOME desktop to make you more productive.
|
||||
|
||||
### Schedule
|
||||
|
||||
The BETA is out. The release candidate is expected on September 3rd, 2022. GNOME 43 finally releases on September 21, 2022.
|
||||
|
||||
* GNOME 43 Beta: August 31, 2022 (Complete)
|
||||
* GNOME 43 RC: September 3, 2022
|
||||
* GNOME 43 final release: September 21, 2022
|
||||
|
||||
### GNOME 43: The Features
|
||||
|
||||
#### 1. Core Shell Changes
|
||||
|
||||
* Finally, the high-resolution scroll wheel support lands in GNOME thanks to recent work in Wayland. So, if you have a high-resolution display, scrolling with an advanced mouse (such as Logitech MX Master 3) should be a treat for you.
|
||||
* In addition to the above, the direct scanout support in GNOME 43 will help with multi-monitor setup situations.
|
||||
* The server-side decorations get essential colour support.
|
||||
* Shell also implemented a feature where the notifications get away when the focus changes, and it doesn’t wait for the timeout.
|
||||
* Like every release, you experience better animation performance across the desktop, improved grid and overview navigation and critical updates, which gives you a seamless experience.
|
||||
|
||||
So, that are the key summaries of the core changes. Now, let’s talk about the Quick settings.
|
||||
|
||||
#### 2. New Quick Settings Menu
|
||||
|
||||
The quick settings in the system tray are entirely changed. The quick settings items and menus now feature pill-shaped toggle buttons with vibrant colours to show what is happening in your system. The menu is also dynamic and makes way for cascading menu items. In addition, you can choose the audio devices in the quick settings.
|
||||
|
||||
Here’s a quick demo, and for additional screenshots and write-up – read our exclusive coverage: [GNOME 43 Quick settings][2].
|
||||
|
||||
![Quick Settings Demo in GNOME 43][3]
|
||||
|
||||
#### 3. Files
|
||||
|
||||
GNOME Files gets the most features in GNOME 43 release. The list of improvements in this application is enormous. The file manager is the most used app in any desktop environment. Hence the changes in Files are the most impactful across the user base.
|
||||
|
||||
For the first time, Files with GTK4 arrive (it was not ready during GNOME 42 release), and it will change your workflow for good.
|
||||
|
||||
I will try to explain most of them in a brief list. Otherwise, this will be a lengthy post. I will push out another separate article on the File features.
|
||||
|
||||
##### Adaptive sidebar
|
||||
|
||||
So the sidebar of Files which gives you access to navigations, favourites, network drives, etc. – is not responsive. And it [autohides][4] itself when Files window size reaches a point. A familiar and handy feature if you work with many open windows and have smaller displays.
|
||||
|
||||
Another exciting feature is that when the sidebar is wholly hidden, an icon appears at the left top for you to make it visible.
|
||||
|
||||
![Files 43 with autohide sidebar][5]
|
||||
|
||||
##### Emblems
|
||||
|
||||
We had the emblems in GNOME long back, and they went away. So, Emblems make a comeback with GNOME 43 with small icons beside the files and directories. These icons imply the type, such as symbolic link, read-only, etc. Moreover, the icons change their colour based on your theme, and multiple emblems are also available for a single file.
|
||||
|
||||
![Emblems in GNOME 43][6]
|
||||
|
||||
##### Rubberband Selection
|
||||
|
||||
Next up is the much-awaited rubberband selection feature, which [finally arrived][7]. Now you can select the files and folders by drag-selection mechanism. One of the most requested features from the users.
|
||||
|
||||
![Rubberband Selection Feature][8]
|
||||
|
||||
##### GtkColumnView replacing GtkTreeView
|
||||
|
||||
When you mouse over the items in the column view, you see a focused row which is another critical feature of Files in GNOME 43. But the [tree view could not make it][9] and probably planned for the next iteration.
|
||||
|
||||
![GtkColumnView enables row focus][10]
|
||||
|
||||
##### Redesigned properties window with interactive permission and executable detection
|
||||
|
||||
The properties window is [wholly changed][11], thanks to the adaptation of GTK4. The window is now much cleaner and well-designed, showing essential items only when required.
|
||||
|
||||
Furthermore, the properties dialog can determine the file type and provide suitable options. For example, if you view the properties of a shell script or text file, you will get an option to make it executable. In contrast, the properties of an image file do not give you an executable option.
|
||||
|
||||
![Intelligent properties window][12]
|
||||
|
||||
**Tabbed View improvements**
|
||||
|
||||
The tabbed view of Files gets some [additional updates][13]. The most noteworthy ones are the proper focus when dragging a file to tag, the creation of tabs after the current focussed tab and so on.
|
||||
|
||||
**Redesigned Right-click menu**
|
||||
|
||||
The primary right-click context menu on files or folders is restricted. Firstly, the OPEN option is clubbed under a submenu. Secondly, the copy/paste/cut options are consolidated in a group. And finally, the Trash, Rename and Compress options are grouped.
|
||||
|
||||
In addition, the Open in terminal option is available for all files and folders. However, create a new file option is still missing (which I expected in this release).
|
||||
|
||||
![Various Context Menu][14]
|
||||
|
||||
##### Other changes in Files
|
||||
|
||||
Other prominent changes in Files are the Trash icon, and other locations (network drive, disk) gets the properties option in right-click context menu.
|
||||
|
||||
Finally, the Files preference window was revamped to show you more essential items. The redesign makes it easy for the average user to find the proper Files settings.
|
||||
|
||||
#### 4. Web
|
||||
|
||||
Let’s spare some moments to talk about our beloved Epiphany, a.k.a. GNOME Web, the WebKit-based native web browser for the GNOME desktop.
|
||||
|
||||
The updates were long overdue and finally started landing from this release onwards.
|
||||
|
||||
First and foremost, GNOME Web now supports WebExtension API. It lets you download and install the Firefox and Google Chrome extensions inside the Web. Here’s how you can do it.
|
||||
|
||||
* Download any extension file (xpi or crx file) from Firefox Add-on or Google Chrome extension page.
|
||||
* Click on the hamburger menu and select Extensions.
|
||||
* Finally, click add to install them.
|
||||
|
||||
WebExtension support is a crucial step for making the Web usable soon.
|
||||
|
||||
Secondly, the Firefox Sync option is available, which lets you log in to the Web via a Firefox account to sync bookmarks and other browser items.
|
||||
|
||||
![Login to the Web using a Firefox account][15]
|
||||
|
||||
Other noteworthy changes in the Web include support for “view source”, GTK4 porting work and an updated PDF library (PDF.js 2.13.216).
|
||||
|
||||
One of the critical components which are still missing in Web is the [WebRTC support via GStreamer][16]. Once that feature arrives, it will be a decent browser for daily usage.
|
||||
|
||||
Hopefully, one day, we all have a decent alternative browser which is non-Firefox or non-Chromium.
|
||||
|
||||
#### 5. Settings
|
||||
|
||||
In the settings window, mostly improvements and visual fine-tuning arrive in this release. The important change includes the “Dog Bark” sound in Alert is gone now after a long and [interesting conversation][17].
|
||||
|
||||
In addition, a new device security panel is introduced, and the TImezone map in the Date & Time panel is revamped.
|
||||
|
||||
The settings sidebar is also responsive and gives you autohide features like the Files shown above.
|
||||
|
||||
#### 6. Software
|
||||
|
||||
Two crucial changes are coming to GNOME Software. These changes enable you to view more information on a single page for any application.
|
||||
|
||||
Firstly, a new section, “Other apps by Author”, gives you a list of apps by the author of the current app. This helps in discovery and tells you how popular the creator is.
|
||||
|
||||
Secondly, GNOME 43 Software now provides you with a detailed list of permission required by the Flatpak apps in a separate window. Hence, you can verify the app before you install them.
|
||||
|
||||
Another crucial visual change is a new “Available for Fedora/any distro” section on the application main overview page, which requires configuration.
|
||||
|
||||
![Other APPS by developer section in Software][18]
|
||||
|
||||
#### 7. Climate Change Wallpaper
|
||||
|
||||
I am not sure whether this feature landed. Because I could not find it, but I heard about it. So, I though I should mention it here.
|
||||
|
||||
The feature is that GNOME 43 brings a background wallpaper showing how the global temperature has risen over the decades from [ocean stripes][19]. The wallpaper contains vertical colour-coded bars denoting low and high temperatures. I think it’s a nice touch and an effort to raise awareness. Here’s the [commit][20] in GitLab.
|
||||
|
||||
In addition, a couple of new [“days and nights”][21] fresh wallpapers are available.
|
||||
|
||||
That’s all about the essential changes I could find and summarise here. Besides those, a vast list of bug fixes, performance improvements and code clean up lands in GNOME 43.
|
||||
|
||||
Fedora 37 will feature GNOME 43 when released, and some parts of it should be in Ubuntu 22.10, due in October.
|
||||
|
||||
### Wrapping Up
|
||||
|
||||
GNOME 43 is an iconic release because it changes several basic designs and impacts millions of users’ workflow. The quick settings transformation is fantastic and long overdue. In addition, the necessary changes in Files, Web and Settings will improve your productivity.
|
||||
|
||||
Furthermore, the new features arrive while keeping the design guideline and aesthetics in mind. A good user interface requires a well-thought-out process, and the devs did a perfect job in this release.
|
||||
|
||||
So, that’s pretty much it. That’s GNOME 43 for you. Let me know if you plan to get this update and want to hop from KDE to GNOME!
|
||||
|
||||
Do let me know your favourite feature in the comment section below.
|
||||
|
||||
Cheers.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://www.debugpoint.com/gnome-43/
|
||||
|
||||
作者:[Arindam][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://www.debugpoint.com/author/admin1/
|
||||
[b]: https://github.com/lkxed
|
||||
[1]: https://www.debugpoint.com/wp-content/uploads/2022/08/GNOME-43-Running-via-GNOME-OS.jpg
|
||||
[2]: https://www.debugpoint.com/gnome-43-quick-settings/
|
||||
[3]: https://www.debugpoint.com/?attachment_id=10682
|
||||
[4]: https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/877
|
||||
[5]: https://www.debugpoint.com/?attachment_id=10684
|
||||
[6]: https://www.debugpoint.com/?attachment_id=10685
|
||||
[7]: https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/817
|
||||
[8]: https://www.debugpoint.com/wp-content/uploads/2022/08/Rubberband-Selection-Feature.gif
|
||||
[9]: https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/817
|
||||
[10]: https://www.debugpoint.com/wp-content/uploads/2022/08/GtkColumnView-enables-row-focus.gif
|
||||
[11]: https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/745
|
||||
[12]: https://www.debugpoint.com/wp-content/uploads/2022/08/Intelligent-properties-window.jpg
|
||||
[13]: https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/595
|
||||
[14]: https://www.debugpoint.com/?attachment_id=10689
|
||||
[15]: https://www.debugpoint.com/wp-content/uploads/2022/08/Login-to-Web-using-Firefox-account.jpg
|
||||
[16]: https://twitter.com/_philn_/status/1490391956970684422
|
||||
[17]: https://discourse.gnome.org/t/dog-barking-error-message-sound/9529/2
|
||||
[18]: https://www.debugpoint.com/wp-content/uploads/2022/08/Other-APPS-by-developer-section-in-Software.jpg
|
||||
[19]: https://showyourstripes.info/s/globe/
|
||||
[20]: https://gitlab.gnome.org/GNOME/gnome-backgrounds/-/commit/a142d5c88702112fae3b64a6d90d10488150d8c1
|
||||
[21]: https://www.debugpoint.com/custom-light-dark-wallpaper-gnome/
|
@ -0,0 +1,168 @@
|
||||
[#]: subject: "How to Iron Out IDE Issues with Auto Stub Generation"
|
||||
[#]: via: "https://www.opensourceforu.com/2022/08/how-to-iron-out-ide-issues-with-auto-stub-generation/"
|
||||
[#]: author: "Vladimir Losev https://www.opensourceforu.com/author/vladimir-losev/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
How to Iron Out IDE Issues with Auto Stub Generation
|
||||
======
|
||||
*Automatic stub generation can significantly improve code analysis. Read on to find out why you should consider this, and how to go about it.*
|
||||
|
||||
A good project takes a desired shape only due to the rigorous effort and time that developers put into it. But all this effort can go straight down the drain if the key components that are essential for development present any problems. During the development of the Toloka-Kit library at Toloka, the team encountered a wide range of such issues with integrated development environment (IDE) support. This article will help you understand the issues faced and how these were dealt with, so you can cross similar hurdles easily.
|
||||
To put things into perspective, let us first understand what IDE hinting is.
|
||||
|
||||
### IDE hinting
|
||||
|
||||
Let us say we want to implement a module called sleepy (Figure 1). This sleepy module will contain a sleep_for function between the standard library and time sleep function. Our sleep_for function will accept a time unit, so we will be able to make it sleep for different time periods, say half an hour, a day, or maybe even for weeks.
|
||||
|
||||
![Figure 1: Sleepy module in IDE][1]
|
||||
|
||||
The team at Toloka implemented this module, and we saw that if we used the script_for function anywhere in our code, then the IDE showed us the signature. Upon trying to print the argument name it got autocompleted. If for some reason we passed an argument that was not expected in the signature, then the IDE highlighted this argument and we would see that we were doing something wrong before running the code.
|
||||
|
||||
Let us write a simple decorator called log_function_call, as shown in Figure 2. This function does exactly what the name suggests. It will write a line of code in the login stream every time we run the function, and every time the function is complete we will write another message to our login form.
|
||||
|
||||
![Figure 2: A simple decorator][2]
|
||||
|
||||
Now, if we configure our login and run our sleeper function, we can see that everything works just fine (Figure 3). We can clearly see these lines of code. If we start to use this function in our code, we can see that neither do we have the signature highlighted any longer nor are the arguments autocompleted. If we pass an argument that shouldn’t be there, it is not highlighted and IDE does not indicate that we are doing something wrong.
|
||||
|
||||
![Figure 3: Function output][3]
|
||||
|
||||
“What are the reasons for this?” you may ask. Technically, it seems perfectly fine behaviour because decorators are substituting one function for another. Here, we defined a function called ‘wrapper’ and substituted our original function with the wrapper function. Upon importing the inspect module, when we used the inspect.signature function to just view the signature, we could see that the signature is still there (Figure 4).
|
||||
|
||||
![Figure 4: Screen with signature][4]
|
||||
|
||||
The trick is that functools.wrap actually preserves the signature in a specific attribute and it is there at runtime. And that begs the question: “Why could our IDE not pick the information up?”
|
||||
|
||||
### Static code analysis
|
||||
|
||||
The reason for this is exactly what has been mentioned above. The information about the original signature is stored in an attribute and is available at runtime. But IDE does not run our code. Instead, it uses static code analysis; so it does not have access to runtime objects and there are plenty of reasons for that.
|
||||
|
||||
* Side effects: If you are running the code without proper knowledge of it, you may end up damaging your hard drive.
|
||||
* Code may raise an exception or run for an infinite time: There could also be an exception in the code that can be raised while it’s being run.
|
||||
* Syntactically incorrect code: The code may not be syntactically correct code.
|
||||
In static code analysis, IDEs understand the code itself without actually running it, which is a much more difficult task to accomplish.
|
||||
|
||||
### So what can we do?
|
||||
|
||||
Generally, it is the best practice to provide the IDE with as much information as possible in the code itself. But in this particular case, this is what can be done. Figure 5 shows what our original decorator looked like; we can provide additional information in the form of typing. We can tell our IDE that the log function call is accepting and returning something of the same type. In our case, if the log_function_call accepts a function with some signature, then it should return the function with the same signature.
|
||||
|
||||
Let us see how this works. If we start typing the argument name, we can see that there is a pop-up window above, which lists the whole signature (Figure 5). So actually the information about the signature is in our IDE but is not used to the fullest extent for some reason. The same is happening with the additional argument. The reason it does not highlight anything is due to some minor bug.
|
||||
|
||||
![Figure 5: Pop-up window with list][5]
|
||||
|
||||
Even a simple decorator can confuse IDE hinting, but in real life we may want to implement some more difficult decorators that we would not be able to annotate properly to help IDE to cope with this.
|
||||
|
||||
Let us go back to our sleeping module for a second, and say that we got some feedback from our users to ensure our function is good. The users do not want to import time units every time we use the module. They want to be able to use both TimeUnit enum and the string literal. So, we write another decorator called autoenum, which will take the function, analyse the signature, and pick the arguments that are annotated as enum types. For those particular arguments, we will try to cast all the arguments that pass to the enum value. The code will look something like what is shown in Figure 6.
|
||||
|
||||
![Figure 6: The code][6]
|
||||
|
||||
The process is quite complicated, but let us go through it. First, we take the original signature of our function. Next, we bind our arbitrary arguments args and kwargs to the original signature in order to understand which values are passed to which argument name. Then we traverse the pairs of argument names and values passed to this argument. In case the argument was annotated as enum in the original signature, and if for some reason the value passed to this argument is not an enum, then we can assign a new argument, which is an argument annotation of original value. It takes an enum class and creates an enum instance from a string with a row name.
|
||||
|
||||
There is a big chunk of code that preserves the signatures. If we want our new function to accept both enums and string literals, then we must update the annotations so that every enum annotation is replaced with a union of enums and some string literals. We can recreate a signature object and assign it to a signature attribute.
|
||||
|
||||
![Figure 7: Updated IDE][7]
|
||||
|
||||
In Figure 7, we can see that our IDE highlights the MINUTE string as an invalid argument, but if we run the code we can see that both versions of our code, i.e., the one with the time unit and the one with the string, are actually working. And when we try to get the output from the signature, we can see that this signature is actually updated.
|
||||
|
||||
As a unit we should be able to pass both the sleepy time unit, which is an enum, or one of the following with literal seconds, minutes, hours, days or weeks. For some reason, IDE does not take this and we know why.
|
||||
|
||||
In this particular case the code is so complicated that it is quite tough to understand which signature should be deduced from it without running it, because it is then almost equivalent to actually running the code. This is a very difficult problem, and we cannot think of annotations that will simplify the work for our IDE.
|
||||
|
||||
This is why stub files are proposed.
|
||||
|
||||
### What are stub files?
|
||||
|
||||
Stub files are special files containing PEP-84. The main purpose of these files is to hold the typing information for modules. They are just Python files with the extension ‘pyi’. If an IDE finds a corresponding stub file for a module, it will analyse the stub file instead of the original file for syntax highlighting.
|
||||
|
||||
*What to add to stub files*
|
||||
|
||||
* Public members definitions
|
||||
* Annotations
|
||||
* Docstrings
|
||||
* Final function signatures without decorators
|
||||
* Constants where possible (otherwise use `…` instead)
|
||||
* Necessary import
|
||||
|
||||
*What can be omitted?*
|
||||
|
||||
* Private members
|
||||
* Decorators
|
||||
* Function implementations (use docstring, `…` or pass)
|
||||
|
||||
Now we have PEP-561, which says that any package maintainer that wants to support stub files has to put a marker file named py.typed in the package. We create an empty file called py.typed. This is an indicator for the static analytics tool for our IDE that it should look for the stub files in the package. Let’s see what happens to our IDE hinting after we add a py.typed empty file.
|
||||
|
||||
We can once again see the signature and the autocomplete. We typed an argument that is not there and it is still highlighted. If we pass a string instead of the Timeunit.num, you can see it is not highlighted as an error in Figure 8. But there is still a problem. The reason for using all those decorators was to eliminate the boilerplate so that our coding process could be simplified and there would be no need to write a new piece of code every time. We could put this logic to decorators, but to support the IDE correctly we need to maintain stub files all by ourselves. This is a huge burden and defeats the purpose of using the decorators in the code. This is where automatic stub generation comes in.
|
||||
|
||||
### Automatic stub generation
|
||||
|
||||
To cut the long story short, the IDE is using static code analysis because it is afraid to run our code and does not have access to runtime objects to examine them. But if we write our code correctly while ensuring that it does not contain any side effects, and we can safely import it, then we can help the IDE to import these modules. From the imported modules we can inspect the module and create stub files without decorators, but the IDE will be able to pick it.
|
||||
|
||||
Let us write some code to see how this can be implemented:
|
||||
|
||||
```
|
||||
import importlib.util
|
||||
from inspect import isfunction, signature
|
||||
from io import StringIO
|
||||
from types import ModuleType
|
||||
def load_module_from_file (name: str, path: str):
|
||||
spec = importlib.util.spec_from_file_location (name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec. loader.exec_module(module)
|
||||
return module
|
||||
def generate_stub_content (module: ModuleType) -> str:
|
||||
sio = StringIO()
|
||||
for name, value in module.__dict_.items():
|
||||
if isfunction(value) and value.__module__== module.__name__:
|
||||
sio.write(f’def {name}{signature (value)}:\n pass\n’)
|
||||
return sio.getvalue()
|
||||
def main(name, path):
|
||||
with open (path + ‘i’, ‘w’) as stub_file:
|
||||
module = load_module_from_file(name, path)
|
||||
stub_file.write(generate_stub_content(module))
|
||||
if __name__== ‘__main__’:
|
||||
main(‘sleepy’, ‘../sleepy/__init__.py’)
|
||||
```
|
||||
|
||||
First, we have a function that receives the name of a module and a path to a source code. It imports the source code from the path as if it was a module called name. We’ll see how this works later.
|
||||
|
||||
![Figure 8: Screen with highlighting error][8]
|
||||
|
||||
Next, we need to create a special function that generates the stub files so that this function will accept the module object. This is a simplified version but will only generate stub files for functions. We will take our module and traverse all the attributes of our modules. If the value that is stored in the attribute is a function, and this function was defined in a specified module (some modules might be imported from somewhere else), then we only want to create the stub files to create the functions that are defined in our module. We will write the keyword, the name of the function, the signature of the function, and an empty body. The only thing ‘Function main’ does is that it takes a path to a module and imports it, creates a stub file, and then writes this file into the file that ends with an ‘I’. Now if we take a sleepy dot ‘py’ module, it will write the stub file to sleepy.pyi and we can run the code for our sleepy module.
|
||||
|
||||
We want to import the sleepy module as a sleeper module, and it is located at sleepy/youNeedThatFile. We can get a long line of code by running it, but if we refactor the code we will see something like what is shown in Figure 9.
|
||||
|
||||
![Figure 9: Result of refactoring the code][9]
|
||||
|
||||
We now have the sleep_for function and a signature that is pretty much similar to what we expected but not quite. From this simple example we can see all the difficulties we will encounter while trying to take this approach.
|
||||
|
||||
This code was simple and did not generate a syntactically correct Python file. This was because there was a problem with the default value and the required files were also not imported for it to be a valid Python file. We did not import literal and union, or define sleepy.TimeUnit. But since the IDE is clear, with a little more effort we can easily make this code work.
|
||||
|
||||
In this case, we used the Stubmaker tool. This tool accepts the name and path to the package and output directories to generate stub files. This allows us to get automatically generated stub files that are created after introspecting the runtime objects.
|
||||
|
||||
It is essential to understand the need for proper code analysis while working on a project. Try to seal as many loopholes as possible, so that problems like low cohesion are overcome. Developer time is valuable, and should be spent on improving the project rather than fixing routine problems. So, go for automatic stub generation to make their life easier too.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://www.opensourceforu.com/2022/08/how-to-iron-out-ide-issues-with-auto-stub-generation/
|
||||
|
||||
作者:[Vladimir Losev][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://www.opensourceforu.com/author/vladimir-losev/
|
||||
[b]: https://github.com/lkxed
|
||||
[1]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-1-Sleepy-module-in-IDE.png
|
||||
[2]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-2-A-simple-decorator.png
|
||||
[3]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-3-Function-outputs.png
|
||||
[4]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-4-Screen-with-signature.png
|
||||
[5]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-5-Pop-up-window-with-list.png
|
||||
[6]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-6-The-code.png
|
||||
[7]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-7-Updated-IDE-1.png
|
||||
[8]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-8-Screen-with-highlighting-error.png
|
||||
[9]: https://www.opensourceforu.com/wp-content/uploads/2022/06/Figure-9-Result-of-refactoring-the-code.png
|
@ -0,0 +1,183 @@
|
||||
[#]: subject: "How to Setup HAProxy SSL Termination on Ubuntu 22.04"
|
||||
[#]: via: "https://www.linuxtechi.com/how-to-setup-haproxy-ssl-termination-ubuntu/"
|
||||
[#]: author: "James Kiarie https://www.linuxtechi.com/author/james/"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: " "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
How to Setup HAProxy SSL Termination on Ubuntu 22.04
|
||||
======
|
||||
|
||||
In our previous guide, we demonstrated how to install and configure HAProxy as a load balancer on Ubuntu 22.04. This guide is a continuation of that guide and, we will go a step further and demonstrate how to setup HAPrpxy SSL termination on Ubuntu 22.04 step by step.
|
||||
|
||||
HA Proxy is a widely used and opensource HTTP load balancer and proxying solution for Linux, Solaris, and FreeBSD environments. It’s used to enhance the performance and reliability of web servers by distributing the workload across multiple servers. By so doing, it provides high availability of services and applications.
|
||||
|
||||
#### SSL Termination
|
||||
|
||||
The previous guide did not take into consideration the encryption of web traffic that goes through the HA Proxy load balancer. The incoming traffic that goes through the load balancer is in plain text and is, therefore, insecure and prone to eavesdropping by nefarious third parties.
|
||||
|
||||
HAProxy can be configured to encrypt the traffic it receives before distributing it across the multiple backend servers. This is a preferred approach as opposed to encrypting individual backend servers which can be a tedious process This is where SSL termination comes in.
|
||||
|
||||
The HAProxy encrypts the traffic between itself and the client and then relays the messages in clear text to the backend servers in your internal network. It then encrypts the response from the backend servers and relays them to the clients
|
||||
|
||||
The TLS/SSL certificates are stored only on the HAProxy load balancer rather than the multiple backend servers, thus reducing the workload on the servers.
|
||||
|
||||
To demonstrate SSL termination, we will secure and configure the HAProxy load balancer with the Let’s encrypt certificate.
|
||||
|
||||
For this to work, you need a Fully Qualified Domain Name (FQDN) or simply a registered domain name pointed to your HAProxy’s public IP address. In this guide, we have a domain called linuxtechgeek.info pointed to our HAProxy’s public IP.
|
||||
|
||||
### Step 1) Install Certbot
|
||||
|
||||
To obtain an SSL/TL certificate from Let’s Encrypt Authority, you first need to install certbot. Certbot is free and opensource software that is used for automating the deployment of Let’s Encrypt SSL certificates on websites.
|
||||
|
||||
To install certbot login into the HAProxy server and, first, update the local package index:
|
||||
|
||||
```
|
||||
$ sudo apt update
|
||||
```
|
||||
|
||||
Next, install certbot using the following command:
|
||||
|
||||
```
|
||||
$ sudo apt install -y certbot python3-certbot-apache
|
||||
```
|
||||
|
||||
The python3-certbot-apache package is a plugin that allows Cerbot to work with Apache. With certbot installed, we can now proceed to obtain the SSL certificate.
|
||||
|
||||
### Step 2) Obtaining SSL Certificate
|
||||
|
||||
Let’s Encrypt provides a number of ways to obtain SSL Certificates using various plugins. Most of the plugins only assist in obtaining the certificate which requires manual configuration of the web server. These plugins are called ‘authenticators’ because they merely check whether the server should be issued a certificate.
|
||||
|
||||
In this guide, we will show you how to obtain the SSL certificate using the Standalone plugin. The plugin employs a seamless method of obtaining SSL certificates. It does so by temporarily spawning a small web server on port 80 to which Let’s Encrypt CA can connect and validate your server’s identity before issuing a certificate.
|
||||
|
||||
As such, you need to ensure that no service is listening on port 80. To check which services are listening on port 80, run the command.
|
||||
|
||||
```
|
||||
$ netstat -na | grep ':80.*LISTEN'
|
||||
```
|
||||
|
||||
If Apache is running on the HAProxy server, stop it as shown.
|
||||
|
||||
```
|
||||
$ sudo systemctl stop apache2
|
||||
```
|
||||
|
||||
Next, run certbot using the standalone plugin to obtain the certificate
|
||||
|
||||
```
|
||||
$ sudo certbot certonly --standalone --preferred-challenges http --http-01-port 80 -d linuxtechgeek.info -d www.linuxtechgeek.info
|
||||
```
|
||||
|
||||
The plugin will walk you through a series of prompts. You will be prompted to provide your email address and later agree to the Let’s Encrypt Terms of Service. Also, you can decide to opt-in to receive EFF’s emails about news and campaigns surrounding digital freedom.
|
||||
|
||||
If all goes well, the SSL certificate and key will be successfully saved on the server. These files are themselves saved in the /etc/letsencrypt/archives directory, but certbot creates a symbolic link to the /etc/letsencrypt/live/your_domain_name path.
|
||||
|
||||
![Certbot-SSL-Certificates-Linux-Command-Line][1]
|
||||
|
||||
Once the certificate has been obtained, you will have the following files in the /etc/letsencrypt/live/your_domain_name directory.
|
||||
|
||||
* cert.pem – This is your domain’s certificate.
|
||||
* chain.pem – This is Let’s Encrypt chain certificate.
|
||||
* fullchain.pem – Contains a combination of cert.pem and chain.pem
|
||||
* privkey.pem – The private key to your certificate.
|
||||
|
||||
### Step 3) Configure HAProxy to use SSL Certificate
|
||||
|
||||
For HAProxy to carry out SSL Termination – so that it encrypts web traffic between itself and the clients or end users – you must combine the fullchain.pem and privkey.pem file into one file.
|
||||
|
||||
But before you do so, create a directory where all the files will be placed.
|
||||
|
||||
```
|
||||
$ sudo mkdir -p /etc/haproxy/certs
|
||||
```
|
||||
|
||||
Next, create the combined file using the cat command as follows.
|
||||
|
||||
```
|
||||
$ DOMAIN='linuxtechgeek.info' sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/haproxy/certs/$DOMAIN.pem'
|
||||
```
|
||||
|
||||
![Combine-FullChain-Private-Key-Cat-command][2]
|
||||
|
||||
Next, secure the file by assign the following permissions to the directory using chomd command.
|
||||
|
||||
```
|
||||
$ sudo chmod -R go-rwx /etc/haproxy/certs
|
||||
```
|
||||
|
||||
Now access the HAProxy configuration file.
|
||||
|
||||
```
|
||||
$ sudo vim /etc/haproxy/haproxy.cfg
|
||||
```
|
||||
|
||||
In the frontend section add an entry that binds your server’s IP to port 443 followed by the path to the combined key.
|
||||
|
||||
```
|
||||
bind haproxy-ip:443 ssl crt /etc/haproxy/certs/domain.pem
|
||||
```
|
||||
|
||||
To enforce redirection from HTTP to HTTPS, add the following entry.
|
||||
|
||||
```
|
||||
redirect scheme https if !{ ssl_fc }
|
||||
```
|
||||
|
||||
![SSL-Certs-HAProxy-Settings-Linux][3]
|
||||
|
||||
Save the changes and exit the configuration file. Be sure to confirm that the syntax for HAProxy is okay using the following syntax.
|
||||
|
||||
```
|
||||
$ sudo haproxy -f /etc/haproxy/haproxy.cfg -c
|
||||
```
|
||||
|
||||
![Check-HAProxy-Syntax-Command][4]
|
||||
|
||||
To apply the changes made, restart HAProxy
|
||||
|
||||
```
|
||||
$ sudo systemctl restart haproxy
|
||||
```
|
||||
|
||||
And ensure that it is running.
|
||||
|
||||
```
|
||||
$ sudo systemctl status haproxy
|
||||
```
|
||||
|
||||
![HAProxy-Status-Ubuntu-Linux][5]
|
||||
|
||||
### Step 4) Verifying SSL Termination
|
||||
|
||||
Finally, refresh your browser, and this time around, you will find that your load balancer is now secured with a TLS/SSL as evidenced by the padlock icon in the URL bar.
|
||||
|
||||
![Verify-SSL-Tremination-Node1-Page][6]
|
||||
|
||||
![Verify-SSL-Tremination-Node2-Page][7]
|
||||
|
||||
##### Conclusion
|
||||
|
||||
In this guide, we have demonstrated how to set up SSL termination with HAProxy on Ubuntu 22.04. Your feedback is welcome.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://www.linuxtechi.com/how-to-setup-haproxy-ssl-termination-ubuntu/
|
||||
|
||||
作者:[James Kiarie][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://www.linuxtechi.com/author/james/
|
||||
[b]: https://github.com/lkxed
|
||||
[1]: https://www.linuxtechi.com/wp-content/uploads/2022/08/Certbot-SSL-Certificates-Linux-Command-Line.png
|
||||
[2]: https://www.linuxtechi.com/wp-content/uploads/2022/08/Combine-FullChain-Private-Key-Cat-command.png
|
||||
[3]: https://www.linuxtechi.com/wp-content/uploads/2022/08/SSL-Certs-HAProxy-Settings-Linux.png
|
||||
[4]: https://www.linuxtechi.com/wp-content/uploads/2022/08/Check-HAProxy-Syntax-Command.png
|
||||
[5]: https://www.linuxtechi.com/wp-content/uploads/2022/08/HAProxy-Status-Ubuntu-Linux.png
|
||||
[6]: https://www.linuxtechi.com/wp-content/uploads/2022/08/Verify-SSL-Tremination-Node1-Page.png
|
||||
[7]: https://www.linuxtechi.com/wp-content/uploads/2022/08/Verify-SSL-Tremination-Node2-Page.png
|
501
translated/tech/20210921 3 ways to test your API with Python.md
Normal file
501
translated/tech/20210921 3 ways to test your API with Python.md
Normal file
@ -0,0 +1,501 @@
|
||||
[#]: subject: "3 ways to test your API with Python"
|
||||
[#]: via: "https://opensource.com/article/21/9/unit-test-python"
|
||||
[#]: author: "Miguel Brito https://opensource.com/users/miguendes"
|
||||
[#]: collector: "lujun9972"
|
||||
[#]: translator: "Yufei-Yan"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
用 Python 测试 API 的 3 种方式
|
||||
=====
|
||||
|
||||
单元测试可能令人生畏,但是这些 Python 模块会使你的生活变得更容易。
|
||||
|
||||
![Puzzle pieces coming together to form a computer screen][1]
|
||||
|
||||
在这个教程中,你将学到如何对执行 HTTP 请求代码的进行单元测试。也就是说,你将看到用 Python 对 API 进行单元测试的艺术。
|
||||
|
||||
单元测试是指对单个行为的测试。在测试中,一个众所周知的经验法则就是隔离那些需要外部依赖的代码。
|
||||
|
||||
比如,当测试一段执行 HTTP 请求的代码时,建议在测试过程中,把真正的调用替换成一个假的的调用。这种情况下,每次运行测试的时候,就可以对它进行单元测试,而不需要执行一个真正的 HTTP 请求。
|
||||
|
||||
问题就是,_怎样才能隔离这些代码?_
|
||||
|
||||
这就是我希望在这篇博文中回答的问题!我不仅会向你展示如果去做,而且也会权衡不同方法之间的优点和缺点。
|
||||
|
||||
要求:
|
||||
|
||||
* [Python 3.8][2]
|
||||
* pytest-mock
|
||||
* requests
|
||||
* flask
|
||||
* responses
|
||||
* VCR.py
|
||||
|
||||
### 使用一个天气状况 REST API 的演示程序
|
||||
|
||||
为了更好的解决这个问题,假设你正在创建一个天气状况的 app。这个 app 使用第三方天气状况 REST API 来检索一个城市的天气信息。其中一个需求是生成一个简单的 HTML 页面,像下面这个图片:
|
||||
|
||||
![web page displaying London weather][3]
|
||||
|
||||
伦敦的天气,OpenWeatherMap。图片是作者自己制作的。
|
||||
|
||||
为了获得天气的信息,必须得去某个地方找。幸运的是,通过 [OpenWeatherMap][2] 的 REST API 服务,可以获得一切需要的信息。
|
||||
|
||||
_好的,很棒,但是我该怎么用呢?_
|
||||
|
||||
通过发送一个 `GET` 请求到:`https://api.openweathermap.org/data/2.5/weather?q={city_name}&appid={api_key}&units=metric`,就可以获得你所需要的所有东西。在这个教程中,我会把城市名字设置成一个参数,并确定公制单位。
|
||||
|
||||
### 检索数据
|
||||
|
||||
使用<ruby>`请求`<rt>requests</rt></ruby>来检索天气数据。可以创建一个接收城市名字作为参数的函数,然后返回一个 JSON。JSON 包含温度,天气状况的描述,日出和日落时间等数据。
|
||||
|
||||
下面的例子演示了这样一个函数:
|
||||
|
||||
|
||||
```
|
||||
def find_weather_for(city: str) -> dict:
|
||||
"""Queries the weather API and returns the weather data for a particular city."""
|
||||
url = API.format(city_name=city, api_key=API_KEY)
|
||||
resp = requests.get(url)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
这个 URL 是由两个全局变量构成:
|
||||
|
||||
```
|
||||
BASE_URL = "<https://api.openweathermap.org/data/2.5/weather>"
|
||||
API = BASE_URL + "?q={city_name}&appid={api_key}&units=metric"
|
||||
```
|
||||
|
||||
API 以这个格式返回了一个 JSON:
|
||||
|
||||
```
|
||||
{
|
||||
"coord": {
|
||||
"lon": -0.13,
|
||||
"lat": 51.51
|
||||
},
|
||||
"weather": [
|
||||
{
|
||||
"id": 800,
|
||||
"main": "Clear",
|
||||
"description": "clear sky",
|
||||
"icon": "01d"
|
||||
}
|
||||
],
|
||||
"base": "stations",
|
||||
"main": {
|
||||
"temp": 16.53,
|
||||
"feels_like": 15.52,
|
||||
"temp_min": 15,
|
||||
"temp_max": 17.78,
|
||||
"pressure": 1023,
|
||||
"humidity": 72
|
||||
},
|
||||
"visibility": 10000,
|
||||
"wind": {
|
||||
"speed": 2.1,
|
||||
"deg": 40
|
||||
},
|
||||
"clouds": {
|
||||
"all": 0
|
||||
},
|
||||
"dt": 1600420164,
|
||||
"sys": {
|
||||
"type": 1,
|
||||
"id": 1414,
|
||||
"country": "GB",
|
||||
"sunrise": 1600407646,
|
||||
"sunset": 1600452509
|
||||
},
|
||||
"timezone": 3600,
|
||||
"id": 2643743,
|
||||
"name": "London",
|
||||
"cod": 200
|
||||
```
|
||||
|
||||
当调用 `resp.json()` 的时候,数据是以 Python 字典的形式返回的。为了封装所有细节,可以用 `dataclass` 来代表。这个类有一个工厂方法,可以获得这个字典并且返回一个 `WeatherInfo` 实例。
|
||||
|
||||
这种办法很好,因为可以保持这种表示方法的稳定。比如,如果 API 改变了 JSON 的结构,就可以在同一个地方修改逻辑,在 `from_dict` 方法中。其他代码不会受影响。你也可以从不同的源获得信息,然后把他们都整合到 `from_dict` 方法中。
|
||||
|
||||
|
||||
```
|
||||
@dataclass
|
||||
class WeatherInfo:
|
||||
temp: float
|
||||
sunset: str
|
||||
sunrise: str
|
||||
temp_min: float
|
||||
temp_max: float
|
||||
desc: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "WeatherInfo":
|
||||
return cls(
|
||||
temp=data["main"]["temp"],
|
||||
temp_min=data["main"]["temp_min"],
|
||||
temp_max=data["main"]["temp_max"],
|
||||
desc=data["weather"][0]["main"],
|
||||
sunset=format_date(data["sys"]["sunset"]),
|
||||
sunrise=format_date(data["sys"]["sunrise"]),
|
||||
)
|
||||
```
|
||||
|
||||
现在来创建一个叫做 `retrieve_weather` 的函数。使用这个函数调用 API,然后返回一个 `WeatherInfo`,这样就可创建你自己的 HTML 页面。
|
||||
|
||||
```
|
||||
def retrieve_weather(city: str) -> WeatherInfo:
|
||||
"""Finds the weather for a city and returns a WeatherInfo instance."""
|
||||
data = find_weather_for(city)
|
||||
return WeatherInfo.from_dict(data)
|
||||
```
|
||||
|
||||
很好,我们的 app 现在有一些基础了。在继续之前,对这些函数进行单元测试。
|
||||
|
||||
### 1\. 使用 mock 测试 API
|
||||
|
||||
[根据维基百科][4],<ruby>模拟对象<rt>mock object</rt></ruby>是通过模仿真实对象来模拟它行为的一个对象。在 Python 中,你可以使用 `unittest.mock` 库来<ruby>模拟<rt>mock</rt></ruby>任何对象,这个库是标准库中的一部分。为了测试 `retrieve_weather` 函数,可以模拟 `requests.get`,然后返回静态数据。
|
||||
|
||||
#### pytest-mock
|
||||
|
||||
在这个教程中,会使用 `pytest` 作为测试框架。通过插件,`pytest` 库是非常具有扩展性的。为了完成我们的模拟目标,要用 `pytest-mock`。这个插件抽象化了大量 `unittest.mock` 中的设置,也会让你的代码更简洁。如果你还好奇的话,我在[另一篇博文中][5]会有更多的讨论。
|
||||
|
||||
_好的,说的够多的了,现在看代码。_
|
||||
|
||||
下面是一个 `retrieve_weather` 函数的完整测试用例。这个测试使用了两个 fixture:一个是由 `pytest-mock` 插件提供的 `mocker` fixture, 还有一个是我们自己的。就是从之前请求中保存的静态数据。
|
||||
|
||||
|
||||
```
|
||||
@pytest.fixture()
|
||||
def fake_weather_info():
|
||||
"""Fixture that returns a static weather data."""
|
||||
with open("tests/resources/weather.json") as f:
|
||||
return json.load(f)
|
||||
|
||||
[/code] [code]
|
||||
|
||||
def test_retrieve_weather_using_mocks(mocker, fake_weather_info):
|
||||
"""Given a city name, test that a HTML report about the weather is generated
|
||||
correctly."""
|
||||
# Creates a fake requests response object
|
||||
fake_resp = mocker.Mock()
|
||||
# Mock the json method to return the static weather data
|
||||
fake_resp.json = mocker.Mock(return_value=fake_weather_info)
|
||||
# Mock the status code
|
||||
fake_resp.status_code = HTTPStatus.OK
|
||||
|
||||
mocker.patch("weather_app.requests.get", return_value=fake_resp)
|
||||
|
||||
weather_info = retrieve_weather(city="London")
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
如果运行这个测试,会获得下面的输出:
|
||||
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
...[omitted]...
|
||||
tests/test_weather_app.py::test_retrieve_weather_using_mocks PASSED [100%]
|
||||
============================== 1 passed in 0.20s ===============================
|
||||
Process finished with exit code 0
|
||||
```
|
||||
|
||||
很好,测试通过了!但是...生活并非一帆风顺。这个测试有优点,也有缺点。现在来看一下。
|
||||
|
||||
#### 优点
|
||||
|
||||
好的,有一个之前讨论过的优点就是,通过模拟 API 的返回值,测试变得简单了。将通信和 API 隔离,这样测试就可以预测了。这样总会返回你需要的东西。
|
||||
|
||||
#### 缺点
|
||||
|
||||
对于缺点,问题就是,如果不再想用 `requests` 了,并且决定回到标准库的 `urllib`,怎么办。每次改变 `find_weather_for` 的代码,都得去适配测试。好的测试是,当你修改代码实现的时候,测试时不需要改变的。所以,通过模拟,你最终把测试和实现耦合在了一起。
|
||||
|
||||
而且,另一个不好的方面是你需要在调用函数之前进行大量设置——至少是三行代码。
|
||||
|
||||
|
||||
```
|
||||
...
|
||||
# Creates a fake requests response object
|
||||
fake_resp = mocker.Mock()
|
||||
# Mock the json method to return the static weather data
|
||||
fake_resp.json = mocker.Mock(return_value=fake_weather_info)
|
||||
# Mock the status code
|
||||
fake_resp.status_code = HTTPStatus.OK
|
||||
...
|
||||
```
|
||||
|
||||
_我可以做的更好吗?_
|
||||
|
||||
是的,请继续看。我现在看看怎么改进一点。
|
||||
### 使用 responses
|
||||
|
||||
用 `mocker` 功能模拟 `requests` 有点问题,就是有很多设置。避免这个问题的一个好办法就是使用一个库,可以拦截 `requests` 调用并且给他们打<ruby>补丁<rt>patches</rt></ruby>。有不止一个库可以做这件事,但是对我来说最简单的是 `responses`。我们来看一下怎么用,并且替换 `mock`。
|
||||
|
||||
```
|
||||
@responses.activate
|
||||
def test_retrieve_weather_using_responses(fake_weather_info):
|
||||
"""Given a city name, test that a HTML report about the weather is generated
|
||||
correctly."""
|
||||
api_uri = API.format(city_name="London", api_key=API_KEY)
|
||||
responses.add(responses.GET, api_uri, json=fake_weather_info, status=HTTPStatus.OK)
|
||||
|
||||
weather_info = retrieve_weather(city="London")
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
这个函数再次使用了我们的 `fake_weather_info` fixture。
|
||||
|
||||
然后运行测试:
|
||||
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
...
|
||||
tests/test_weather_app.py::test_retrieve_weather_using_responses PASSED [100%]
|
||||
============================== 1 passed in 0.19s ===============================
|
||||
```
|
||||
|
||||
非常好!测试也通过了。但是...并不是那么棒。
|
||||
|
||||
#### 优点
|
||||
|
||||
使用诸如 `responses` 这样的库,好的方面就是不需要再给 `requests` <ruby>打补丁<rt>patch</rt></ruby>。通过将这层抽象交给库,可以减少一些设置。然而,如果你没注意到的话,还是有一些问题。
|
||||
|
||||
#### 缺点
|
||||
|
||||
和 `unittest.mock` 很像,测试和实现再一次耦合了。如果替换 `requests`, 测试就不能用了。
|
||||
|
||||
### 2\. 使用<ruby>适配器<rt>adapter</rt></ruby>测试 API
|
||||
|
||||
_如果用模拟让测试耦合了,我能做什么?_
|
||||
|
||||
设想下面的场景:假如说你不能再用 `requests` 了,而且必须要用 `urllib` 替换,因为这是 Python 自带的。不仅仅是这样,你了解了不要把测试代码和实现耦合,并且你想今后都避免这种情况。你想替换 `urllib`,也不想重写测试了。
|
||||
|
||||
事实证明,你可以抽象出执行 `GET` 请求的代码。
|
||||
|
||||
_真的吗?怎么做?_
|
||||
|
||||
可以使用<ruby>适配器<rt>adapter</rt></ruby>来抽象它。适配器是一种用来封装其他类的接口,并作为新接口暴露出来的一种设计模式。用这种方式,就可以修改适配器而不需要修改代码了。比如,在 `find_weather_for` 函数中,封装关于 `requests` 的所有细节,然后把这部分暴露给只接受 URL 的函数。
|
||||
|
||||
所以,这个:
|
||||
|
||||
```
|
||||
def find_weather_for(city: str) -> dict:
|
||||
"""Queries the weather API and returns the weather data for a particular city."""
|
||||
url = API.format(city_name=city, api_key=API_KEY)
|
||||
resp = requests.get(url)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
变成这样:
|
||||
|
||||
```
|
||||
def find_weather_for(city: str) -> dict:
|
||||
"""Queries the weather API and returns the weather data for a particular city."""
|
||||
url = API.format(city_name=city, api_key=API_KEY)
|
||||
return adapter(url)
|
||||
```
|
||||
|
||||
然后适配器变成这样:
|
||||
|
||||
```
|
||||
def requests_adapter(url: str) -> dict:
|
||||
resp = requests.get(url)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
现在到了重构 `retrieve_weather` 函数的时候:
|
||||
|
||||
|
||||
```
|
||||
def retrieve_weather(city: str) -> WeatherInfo:
|
||||
"""Finds the weather for a city and returns a WeatherInfo instance."""
|
||||
data = find_weather_for(city, adapter=requests_adapter)
|
||||
return WeatherInfo.from_dict(data)
|
||||
```
|
||||
|
||||
所以,如果你决定改为使用 `urllib` 的实现,只要换一下适配器:
|
||||
|
||||
|
||||
```
|
||||
def urllib_adapter(url: str) -> dict:
|
||||
"""An adapter that encapsulates urllib.urlopen"""
|
||||
with urllib.request.urlopen(url) as response:
|
||||
resp = response.read()
|
||||
return json.loads(resp)
|
||||
|
||||
[/code] [code]
|
||||
|
||||
def retrieve_weather(city: str) -> WeatherInfo:
|
||||
"""Finds the weather for a city and returns a WeatherInfo instance."""
|
||||
data = find_weather_for(city, adapter=urllib_adapter)
|
||||
return WeatherInfo.from_dict(data)
|
||||
```
|
||||
|
||||
_好的,那测试怎么做?_
|
||||
|
||||
为了测试 `retrieve_weather`, 只要创建一个在测试过程中使用的假的适配器:
|
||||
|
||||
```
|
||||
@responses.activate
|
||||
def test_retrieve_weather_using_adapter(
|
||||
fake_weather_info,
|
||||
):
|
||||
def fake_adapter(url: str):
|
||||
return fake_weather_info
|
||||
|
||||
weather_info = retrieve_weather(city="London", adapter=fake_adapter)
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
如果运行测试,会获得:
|
||||
|
||||
|
||||
```
|
||||
============================= test session starts ==============================
|
||||
tests/test_weather_app.py::test_retrieve_weather_using_adapter PASSED [100%]
|
||||
============================== 1 passed in 0.22s ===============================
|
||||
```
|
||||
|
||||
#### 优点
|
||||
|
||||
这个方法的优点是可以成功将测试和实现解耦。使用[<ruby>依赖注入<rt>dependency injection</rt></ruby>][6]在测试期间注入一个假的适配器。你也可以在任何时候更换适配器,包括在运行时。这些事情都不会改变任何行为。
|
||||
|
||||
#### 缺点
|
||||
|
||||
缺点就是,因为你在测试中用了假的适配器,如果在实现中往适配器中引入了一个 bug,测试的时候就不会发现。比如说,往 `requests` 传入了一个有问题的参数,像这样:
|
||||
|
||||
```
|
||||
def requests_adapter(url: str) -> dict:
|
||||
resp = requests.get(url, headers=<some broken headers>)
|
||||
return resp.json()
|
||||
```
|
||||
|
||||
在生产环境中,适配器会有问题,而且单元测试没办法发现。但是事实是,之前的方法也会有同样的问题。这就是为什么不仅要单元测试,并且总是要集成测试。也就是说,要考虑另一个选项。
|
||||
|
||||
### 3\. 使用 VCR.py 测试 API
|
||||
|
||||
现在终于到了讨论我们最后一个选项了。诚实地说,我也是最近才发现这个。我用<ruby>模拟<rt>mock</rt></ruby>也很长时间了,而且总是有一些问题。`VCR.py` 是一个库,它可以简化很多 HTTP 请求的测试。
|
||||
|
||||
它的工作原理是将第一次运行测试的 HTTP 交互记录为一个 YAML 文件,叫做 _cassette_。请求和响应都会被序列化。当第二次运行测试的时候,`VCT.py` 将拦截对请求的调用,并且返回一个响应。
|
||||
|
||||
现在看一下下面如何使用 `VCR.py` 测试 `retrieve_weather`:
|
||||
|
||||
|
||||
```
|
||||
@vcr.use_cassette()
|
||||
def test_retrieve_weather_using_vcr(fake_weather_info):
|
||||
weather_info = retrieve_weather(city="London")
|
||||
assert weather_info == WeatherInfo.from_dict(fake_weather_info)
|
||||
```
|
||||
|
||||
_天呐,就这样?没有设置?`@vcr.use_cassette()` 是什么?_
|
||||
|
||||
是的,就这样!没有设置,只要一个 `pytest` 标注告诉 VCR 去拦截调用,然后保存 cassette 文件。
|
||||
|
||||
_cassette 文件是什么样?_
|
||||
|
||||
好问题。这个文件里有很多东西。这是因为 VCR 保存了交互中的所有细节。
|
||||
|
||||
|
||||
```
|
||||
interactions:
|
||||
\- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
User-Agent:
|
||||
- python-requests/2.24.0
|
||||
method: GET
|
||||
uri: [https://api.openweathermap.org/data/2.5/weather?q=London\&appid=\][7]<YOUR API KEY HERE>&units=metric
|
||||
response:
|
||||
body:
|
||||
string: '{"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":800,"main":"Clear","description":"clearsky","icon":"01d"}],"base":"stations","main":{"temp":16.53,"feels_like":15.52,"temp_min":15,"temp_max":17.78,"pressure":1023,"humidity":72},"visibility":10000,"wind":{"speed":2.1,"deg":40},"clouds":{"all":0},"dt":1600420164,"sys":{"type":1,"id":1414,"country":"GB","sunrise":1600407646,"sunset":1600452509},"timezone":3600,"id":2643743,"name":"London","cod":200}'
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
- 'true'
|
||||
Access-Control-Allow-Methods:
|
||||
- GET, POST
|
||||
Access-Control-Allow-Origin:
|
||||
- '*'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '454'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 18 Sep 2020 10:53:25 GMT
|
||||
Server:
|
||||
- openresty
|
||||
X-Cache-Key:
|
||||
- /data/2.5/weather?q=london&units=metric
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
```
|
||||
|
||||
_确实很多!_
|
||||
|
||||
真的!好的方面就是你不需要留意它。`VCR.py` 会为你安排好一切。
|
||||
|
||||
#### 优点
|
||||
|
||||
现在看一下优点,我可以至少列出五个:
|
||||
|
||||
* 没有设置代码。
|
||||
* 测试仍然是分离的,所以很快。
|
||||
* 测试是确定的。
|
||||
* 如果你改了请求,比如说用了错误的 header,测试会失败。
|
||||
* 没有与代码实现耦合,所以你可以换适配器,而且测试会通过。唯一有关系的东西就是请求必须是一样的。
|
||||
|
||||
|
||||
#### 缺点
|
||||
|
||||
再与模拟相比较,除了避免了错误,还是有一些问题。
|
||||
|
||||
如果 API 提供者出于某种原因修改了数据格式,测试仍然会通过。幸运的是,这种情况并不经常发生,而且在这种重大改变之前,API 提供者通常会给他们的 API 提供不同版本。
|
||||
|
||||
另一个需要考虑的事情是<ruby>就地<rt>in place</rt></ruby><ruby>端到端<rt>end-to-end</rt></ruby>测试。每次服务器运行的时候,这些测试都会调用。顾名思义,这是一个范围更广、更慢的测试。他们会比单元测试覆盖更多。事实上,并不是每个项目都需要使用它们。所以,就我看来,`VCR.py` 对于大多数人的需求来说都绰绰有余。
|
||||
|
||||
### 总结
|
||||
|
||||
就这么多了。我希望今天你了解了一些有用的东西。测试 API 客户端应用可能会有点吓人。然而,当武装了合适的工具和知识,你就可以驯服这个野兽。
|
||||
|
||||
在[我的 Github][8] 上可以找到完整的 app。
|
||||
|
||||
* * *
|
||||
|
||||
_这篇文章最早发表在[作者的个人博客][9],并且已得到授权_
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://opensource.com/article/21/9/unit-test-python
|
||||
|
||||
作者:[Miguel Brito][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[https://github.com/Yufei-Yan](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://opensource.com/users/miguendes
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/puzzle_computer_solve_fix_tool.png?itok=U0pH1uwj (Puzzle pieces coming together to form a computer screen)
|
||||
[2]: https://miguendes.me/how-i-set-up-my-python-workspace
|
||||
[3]: https://opensource.com/sites/default/files/sbzkkiywh.jpeg
|
||||
[4]: https://en.wikipedia.org/wiki/Mock_object
|
||||
[5]: https://miguendes.me/7-pytest-plugins-you-must-definitely-use
|
||||
[6]: https://stackoverflow.com/questions/130794/what-is-dependency-injection
|
||||
[7]: https://api.openweathermap.org/data/2.5/weather?q=London\&appid=\
|
||||
[8]: https://github.com/miguendes/tutorials/tree/master/testing_http
|
||||
[9]: https://miguendes.me/3-ways-to-test-api-client-applications-in-python
|
Loading…
Reference in New Issue
Block a user