The layering in the cards project is important because it ensures a clear separation of concerns. The CLI layer handles the user interface, the API layer contains the business logic, and the DB layer manages database interactions. This strict separation prevents direct access between layers, ensuring that the CLI does not directly access the database and the API does not interact with the CLI, promoting modularity and maintainability.
Using `import cards` in CLI.py instead of `from . import API` allows the CLI to serve as an example of how to use the cards API from an external project. This approach mimics how a third-party application would import and use the cards package, making the CLI a practical reference for API usage. It also ensures that the CLI remains a true example of using the cards package as a library, rather than just importing internal modules directly.
When `import cards` is executed in a REPL, the `cards` variable holds the module defined in `__init__.py`. This module includes the version string, the app object from CLI, and everything imported via `from .api import *`, which is controlled by the `__all__` list in `api.py`. Essentially, `cards` represents the top-level package interface.
Having multiple clients for an API, such as CLI and tests, provides additional opportunities to evaluate the API's usability and design. If the API is difficult to use in tests or the CLI, it may indicate issues that need addressing before wider adoption. This approach aligns with test-driven development principles, ensuring the API is robust and user-friendly before being exposed to external users.
Moving the version string from `__init__.py` to `api.py` requires adding it to the `__all__` list in `api.py` to make it accessible. While this change can be made to fix test failures, it is not ideal because it disrupts the traditional placement of version strings, which are typically in `__init__.py` or `pyproject.toml`. Additionally, it makes the CLI less representative of how external users would interact with the package.
from . import module
and import package
.import package
is preferred over from . import API
in this context.今天我们要讨论的是将包的一部分导入到同一个包的另一部分。我们将学习`from . import module`和`from .module import something`,以及`import package`从包内访问外部API。如果`from . import api`可以正常工作,为什么还要使用`import package`呢?好吧,我们将在本集中讨论这个问题。
欢迎来到Test & Code。本集由HelloPytest(学习pytest的最快方法)和PythonTest社区赞助播出。更多信息请访问courses.pythontest.com。虽然我将在本集中讨论的技术适用于许多情况,但这主题源于我收到的一个关于在《Python Testing with PyTest》第二版和完整PyTest课程中使用的cards应用程序的问题。
因此,我将讨论cards中的导入。如果您想查看代码,请访问github.com/Aachen/cards。项目的实际源代码非常小。源代码位于source cards,实际上是SRC/cards。共有四个文件:db.py、api.py、cli.py和__init__.py。
这些实际上是分层结构。用户界面是CLI.py中实现的命令行界面。CLI有意设计得尽可能精简,将大部分逻辑留给API层。API.py包含项目的预期API
以及尽可能多的业务逻辑。API.py调用db.py,因此API调用数据库层来处理数据库问题。db.py包含一个DB类,它充当存储服务的接口。
cards的分层结构是有意为之且严格的。CLI使用API,但不直接访问数据库。API使用db.py中的数据库,但不直接访问数据库,也不访问CLI代码。而DB(db.py层)完全不知道API或CLI。我想最顶层是__init__.py,它也
这个文件包含一个版本字符串,从CLI导入app对象,并包含`from .api import *`。导入语句中`*`获取的所有内容都由API.py中定义的__all__列表定义,内容不多。现在我看着它,我本可以以不同的方式构建__init__.py,也许包含CLI app并不是真的必要。
不过现在不重要。好的,这就是相关的项目。现在让我们看看导入,具体的导入语句。我们试图讨论在一个项目中进行导入,对吧?所以看看db.py,底层,数据库,不需要导入项目的任何其他部分。api.py使用数据库,所以它导入它。
它使用`from .db import DB`这行代码导入它,因为`DB`是我用来包含数据库对象的类名。`.db`表示在当前包中查找DB,并在其中导入DB类。所以这就是API.py中的导入语句`from .db import DB`。
CLI使用API,所以它需要导入它。但这有点不同。我本可以做类似的事情。我本可以写`from .API import whatever`。或者更可能的是,由于CLI使用API中的所有内容,`from . import API`。但我没有。相反,我写了`import cards`。我为什么要这么做?现在我们来看看我从读者那里得到的问题。主要就是这个。
我相信对此的解释与__init__.py有关,您有帮助地将其标记为cards的顶级包,但我并不真正理解那里发生了什么。为什么不直接从CLI导入API?另外,__init__中的导入如何允许CLI访问API?当CLI导入cards时,cards是什么?这基本上是我从本书读者那里得到的问题,也就是
`import cards`与`from . import API`相比有什么好处?
然后,在这个文件中,这个cards对象包含什么内容?让我们来回答这个问题。我选择`import cards`而不是`from . import API`的原因是允许将CLI文件用作他人如何使用API的示例。cards包既是一个可以用作基本项目管理工具的命令行工具,也是可以被其他程序使用的库。
所以假设我想为cards编写一个不同的UI作为不同的Python项目,也许是一个基于curses或textual的东西,或者也许是一个GUI。当我构建这个东西时,我可以使用CLI.py作为如何使用cards API的示例。我希望这是真的。所以我故意将CLI实现写成`import cards`,因为这就是第三方程序会做的事情。
这真的是我的理由。我希望CLI.py能够用作如何使用Cards API的示例。那么这一切是如何运作的呢?如果我通过`pip install cards`安装了Cards,那么我既可以访问Cards作为应用程序,也可以访问Cards作为可以被另一个应用程序导入的包。所以这个新的应用程序,当它导入Cards时,最终结果是什么?Cards是什么?
换句话说,变量cards的值是什么样的?这可以用REPL轻松测试。如果我用Python或Python -i启动它来进行交互,它就会启动REPL。
如果我在其中运行`import cards`,我可以查看这个东西是什么。我可以简单地说`print cards`。这就行了,并向我展示了它是什么。它显示的是:模块cards来自blah, blah, blah, something cards, __init__.py。是的,那个cards对象,无论那个变量是什么,它都包含我放在__init__.py中的东西。太棒了。
这就是内部细节。你可以自己看看。再说一次,我选择`import cards`而不是`from . import API`的原因是我真的希望CLI.py能够用作示例。但也有一些其他的好处。API必须被设计、实现、测试和使用。
如果你的测试代码涵盖了整个API,那么你的测试代码就是API的一个用户。这很好。如果针对API编写测试很痛苦,那么在实际工作中使用API也可能很痛苦。尽早发现这一点是很好的。所以在提交给其他人使用之前,你可以更改它,改进它,使其更容易使用。至少这是关于测试驱动开发的理论之一。
但由于某种原因,即使在为API编写测试时,人们有时也不会注意到糟糕的API。所以现在你可以用命令行界面获得使用API的第二个示例。如果我们编写使用API的CLI或GUI或其他东西,它就是API的另一个客户端。
我们又一次有机会尝试API,试用一下,看看它是否好用。好的,如果你仍然不相信我做对了,改变它并让CLI导入API需要多少工作?我已经尝试过了,这并不需要太多工作。
而且由于我对我的测试套件充满信心,我可以使用测试套件来查看我是否已经涵盖了所有内容(双关语),并进行了更改。因此,为了以最少的代码更改来测试这一点,我可以首先确保我已经在我的虚拟环境中安装了cards的可编辑版本,使用`pip install -e`。
基本上,或者目录。所以`pip install -e`,然后是你正在安装的东西,来自cards项目目录的当前目录的点。然后为了保险起见,我运行pytest来确保在任何更改之前测试都通过了。
然后我将CLI.py中的`import cards`改为`from . import API as cards`。我之所以使用`as cards`,是为了不必更改任何其他代码。所以`from .`是来自当前包。`from . import API`是从当前包导入API对象。这将把API作为名为API的局部变量导入。
然后我可以遍历并更改所有文件以使用,或者文件中的所有内容,以使用API而不是cards。但这很混乱,所以我使用了`from . import API as cards`。这样我就可以保留代码中所有的cards.whatever。现在运行pytest。我得到一个失败。所以这里有一个失败。是什么?
我有一个版本测试,而__version__在__init__中,它不是API的一部分。这很容易解决。我可以将版本从__init__移动到API,并将version添加到__all__列表中。我也尝试过这样做,以确保它能工作,并且它通过了。这样更好吗?我实际上并不认为它更好。
我不太喜欢将版本放在API文件中。我有一种传统,要么把它放在__init__.py中,要么把它放在pyproject.toml中,然后让__init__.py查找那个版本。我不喜欢这个的另一个原因是,现在它不是一个好例子。我的CLI并不是使用API的真实示例。
所以`from . import API as cards`可以工作,但这并不是其他人将cards用作包的方式。他们只会说`import cards`。我可以在导入行中添加一个注释,说明通常你只需要说`import cards`,但为什么不直接这样做呢?为什么不直接说`import cards`呢?而且较长的形式并没有给我们带来任何好处。
我还认为使用API.whatever的代码并不比cards.whatever更容易阅读。我认为实际使用cards接口很好。无论如何,这就是这个导入以及我对它的思考,以及为什么我认为在应用程序中有一个使用API的示例是很好的,特别是如果它是一个开源应用程序的话。
感谢您的收听,并感谢所有通过购买课程支持节目的所有人,包括Hello PyTest(学习PyTest的最快方法)和完整的PyTest课程,如果您想真正成为PyTest专家。两者都可以在courses.pythontest.com上找到,您也可以在那里加入Python测试社区。现在就到这里。现在去测试一些东西吧。