Requests 源碼閱讀-Day1

Requests 源碼閱讀-Day1

[toc]python

Requests介紹

requests是一個Python的網絡請求庫,和urllib、httplib之流相比起來最大的優勢就是好用,requests官方標榜的就是「咱們的庫是給人用的哦」。linux

此外,requests還支持https驗證而且是線程安全的git

官方文檔github

分析環境

系統:Linux -Fedora 29 x64
開發工具: vscode
Requests版本: V2.20.0
python版本: 3.6.7web

git下載地址算法

源碼分析

首先git clone源碼到本地,而後使用cloc工具查看文件格式瀏覽器

若是系統沒有cloc工具,能夠進行安裝。
因爲我本地是fedora系統,可使用dnf install cloc命令進行安裝

使用下面命令查看項目關於python文件的統計狀況:安全

[jian@laptop requests]$ cloc --include-lang=Python .
Language files blank comment code
Python 35 1951 1990 5937


能夠看到總共python文件有35個,代碼量是5937行服務器

單元測試

[jian@laptop requests]$ pwd
/home/jian/prj/github/requests

[jian@laptop requests]$ python

Python 3.6.7 (default, Mar 21 2019, 20:23:57) 
[GCC 8.3.1 20190223 (Red Hat 8.3.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> r = requests.get("http://www.baidu.com")
>>> r.status_code
200



打開README.md文件看看requests當前都支持哪些功能cookie

International Domains and URLs      #國際化域名和URLS
Keep-Alive & Connection Pooling        #keep—Alive&鏈接池
Sessions with Cookie Persistence    #持久性cookie的會話
Browser-style SSL Verification        #瀏覽器式SSL認證
Basic & Digest Authentication       #基本/摘要認證
Familiar  dict–like Cookies            #key/value cookies
Automatic Decompression of Content  #自動內容解壓縮
Automatic Content Decoding            #自動內容解碼
Automatic Connection Pooling        #自動鏈接池
Unicode Response Bodies                #Unicode響應體
Multi-part File Uploads             #文件分塊上傳
SOCKS Proxy Support                    #HTTP(S)代理支持
Connection Timeouts                 #鏈接超時
Streaming Downloads                    #數據流下載
Automatic honoring of .netrc        #netrc支持
Chunked HTTP Requests                #Chunked請求



看源碼看的是思想,要明白做者的設計思路究竟是什麼。

好比requests,看完了你應當問問本身,cookie爲何要封裝而不是直接用?request爲何要有兩個形態?設計session是爲了解決什麼問題?

只有理解了設計思路,再去看具體的細節實現纔能有收穫,不然你看到的就是滿屏的raise、isinstanceof,這樣去看代碼恐怕是浪費時間了。

so...開始開幹吧

test_requests.py

源碼目錄下有一個tests文件夾,這裏面以test開頭的測試文件是專門用於測試requests接口,使用的是pytest方式,pytest我會單獨寫一章節介紹具體的內容

首先選第一個方法分析,找到test_DIGEST_HTTP_200_OK_GET方法

tests/test_requests.py

def test_DIGEST_HTTP_200_OK_GET(self, httpbin):

        for authtype in self.digest_auth_algo:
            auth = HTTPDigestAuth('user', 'pass')
            url = httpbin('digest-auth', 'auth', 'user', 'pass', authtype, 'never')

            r = requests.get(url, auth=auth)
            assert r.status_code == 200

            r = requests.get(url)
            assert r.status_code == 401
            print(r.headers['WWW-Authenticate'])

            s = requests.session()
            s.auth = HTTPDigestAuth('user', 'pass')
            r = s.get(url)
            assert r.status_code == 200

上面這段代碼主要作了下面的工做:

1.傳遞一個httpbin參數,這個 httpbin是什麼東西?

2.遍歷不一樣的摘要認證算法,self.digest_auth_algo是什麼?

3.摘要認證變量auth及url變量設置, HTTPDigestAuth是什麼?

4.對url發起get請求,200表示請求成功,401表示未經受權。

這個測試是爲了驗證auth的必要性

5.新建了一個會話對象s,同時也設置了auth變量,跟前面不一樣的是這個請求是由會話對象s發起的


digest_auth_algo

首先回答前面說的self.digest_auto_algo的問題:

tests/test_requests.py

class TestRequests:

    digest_auth_algo = ('MD5', 'SHA-256', 'SHA-512')
    ....

摘要訪問認證,它是一種協議規定的web服務器用來同網頁瀏覽器進行認證信息協商的方法。

瀏覽器在向服務器發送請求的過程當中須要傳遞認證信息auth

auth通過摘要算法加密造成祕文,最後發送給服務器。

服務器驗證成功後返回「200」告知瀏覽器能夠繼續訪問

若驗證失敗則返回"401"告訴瀏覽器禁止訪問。

當前該摘要算法分別選用了"MD5","SHA-256","SHA-512"。

HTTPDigestAuth

tests/test_requests.py

from requests.auth import HTTPDigestAuth, _basic_auth_str

看到是導入requests.auth模塊裏面的HTTPDigestAuth方法
好 咱們去查看下這個東西是什麼?

requests/auth.py

class HTTPDigestAuth(AuthBase):
    """Attaches HTTP Digest Authentication to the given Request object."""

    def __init__(self, username, password):
        self.username = username
        self.password = password
        # Keep state in per-thread local storage
        self._thread_local = threading.local()

    ....
    def __call__(self, r):
        # Initialize per-thread state, if needed
        self.init_per_thread_state()
        # If we have a saved nonce, skip the 401
        if self._thread_local.last_nonce:
            r.headers['Authorization'] = self.build_digest_header(r.method, r.url)
        try:
            self._thread_local.pos = r.body.tell()
        except AttributeError:
            # In the case of HTTPDigestAuth being reused and the body of
            # the previous request was a file-like object, pos has the
            # file position of the previous body. Ensure it's set to
            # None.
            self._thread_local.pos = None
        r.register_hook('response', self.handle_401)
        r.register_hook('response', self.handle_redirect)
        self._thread_local.num_401_calls = 1

        return r

    ...


HTTPDigestAuth:爲http請求對象提供摘要認證。

實例化對象auth時須要傳入認證所需的username及password。

threading.local()在這裏的做用是保存一個全局變量,可是這個全局變量只能在當前線程才能訪問,每個線程都有單獨的內存空間來保存這個變量,它們在邏輯上是隔離的,其餘線程都沒法訪問。

咱們能夠經過實例演示一下摘要認證:

[jian@laptop requests]$ python
Python 3.6.7 (default, Mar 21 2019, 20:23:57) 
[GCC 8.3.1 20190223 (Red Hat 8.3.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> from requests.auth import HTTPDigestAuth
>>> r = requests.get('http://httpbin.org/digest-auth/auth/user/pass',auth=HTTPDigestAuth
... ('user','pass'))

>>> r.status_code
200


httpbin

終於要解決這個東西了,這個東西是啥呢?

1.設置斷點:
tests/conftest.py

@pytest.fixture
def httpbin(httpbin):
    pytest.set_trace() # 設置斷點
    return prepare_url(httpbin)


在tests目錄新建立個文件test_xx.py
tests/test_xx.py

from requests.auth import HTTPDigestAuth
import requests
import pytest


class TestRequests:

    digest_auth_algo = ('MD5', 'SHA-256', 'SHA-512')

    def test_DIGEST_HTTP_200_OK_GET(self, httpbin):

        for authtype in self.digest_auth_algo:
            auth = HTTPDigestAuth('user', 'pass')
            url = httpbin('digest-auth', 'auth', 'user', 'pass', authtype,
                          'never')
            pytest.set_trace() # 設置斷點

            r = requests.get(url, auth=auth)
            assert r.status_code == 200

            r = requests.get(url)
            assert r.status_code == 401
            print(r.headers['WWW-Authenticate'])

            s = requests.session()
            s.auth = HTTPDigestAuth('user', 'pass')
            r = s.get(url)
            assert r.status_code == 200


而後在終端執行

[jian@laptop requests]$ pwd
/home/jian/prj/github/requests
[jian@laptop requests]$ pytest  tests/test_xx.py 
xxx
  @pytest.fixture
  def httpbin(httpbin):
E       fixture 'httpbin' not found

怎麼報錯呢? -說httpbin 這個fixture沒有找到

查資料是說缺乏pytest-httpbin模塊

pip安裝起來:

pip install pytest-httpbin


而後再次執行 pytest tests/test_xx.py 命令

[jian@laptop requests]$ pytest  tests/test_xx.py 
Test session starts (platform: linux, Python 3.6.7, pytest 3.2.1, pytest-sugar 0.9.2)
rootdir: /home/jian/prj/github/requests, inifile: pytest.ini
plugins: sugar-0.9.2, pep8-1.0.6, mock-1.6.2, httpbin-1.0.0, flakes-2.0.0, env-0.6.2, cov-2.5.1, assume-2.2.0, allure-adaptor-1.7.10, celery-4.4.0

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /home/jian/prj/github/requests/tests/conftest.py(20)httpbin()
-> return prepare_url(httpbin)
(Pdb) httpbin
<pytest_httpbin.serve.Server object at 0x7f5e78e44438>
(Pdb) c

>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>
> /home/jian/prj/github/requests/tests/test_xx.py(18)test_DIGEST_HTTP_200_OK_GET()
-> r = requests.get(url, auth=auth)
(Pdb) httpbin
<function prepare_url.<locals>.inner at 0x7f5e78ea3c80>
(Pdb) url
'http://127.0.0.1:44131/digest-auth/auth/user/pass/MD5/never'
(Pdb)



在調試窗口PDB set_trace中能夠看到,首先被調用的是的conftest.py中的httpbin()方法

咱們在(pdb)中輸入httpbin變量,結果
返回了<pytest_httpbin.serve.Server object at 0x7f5e78e44438>

而後繼續調用方法test_DIGEST_HTTP_200_OK_GET(),輸入httpbin變量,結果返回了<function prepare_url.<locals>.inner at 0x7f5e78ea3c80>

通過調試後,httpbin的面貌漸漸變得清晰了

test_DIGEST_HTTP_200_OK_GET(self, httpbin)中的httpbin對象爲<function prepare_url.<locals>.inner at 0x7f5e78ea3c80>

也就是源碼中prepare_url(value)方法裏的inner(*suffix)方法。

也就是這個文件:
tests/conftest.py

def prepare_url(value):
    # Issue #1483: Make sure the URL always has a trailing slash
    httpbin_url = value.url.rstrip('/') + '/'

    def inner(*suffix):
        return urljoin(httpbin_url, '/'.join(suffix))

    return inner

這裏使用了函數閉包,有什麼做用? -保持程序上一次運行後的狀態而後繼續執行

httpbin(httpbin)方法中參數httpbin對象爲<pytest_httpbin.serve.Server object at 0x7f5e78e44438>

pytest_httpbin是pytest的一個插件,那確定跟pytest調用有關係了

而後Server是什麼東東?咱們來查看下它的源碼:

pytest_httpbin/serve.py

class Server(object):
    """
    HTTP server running a WSGI application in its own thread.
    """

    port_envvar = 'HTTPBIN_HTTP_PORT'

    def __init__(self, host='127.0.0.1', port=0, application=None, **kwargs):
        self.app = application
        if self.port_envvar in os.environ:
            port = int(os.environ[self.port_envvar])
        self._server = make_server(
            host,
            port,
            self.app,
            handler_class=Handler,
            **kwargs
        )


原來這是一個本地的WSGI服務器,專門用於pytest進行網絡測試,這樣的好處在於咱們無需鏈接外部網絡環境,在本地就能實現一系列的網絡測試工做。

WSGI全稱是Web Server Gateway Interface,它實際上是一個標準,介於web應用與web服務器之間。

只要咱們遵循WSGI接口標準設計web應用,就無需在乎TCP鏈接,HTTP請求等等底層的實現,全權交由web服務器便可。

上述代碼實現的邏輯已經比較清晰了,httpbin對象被實例化的時候調用__init__(self, host='127.0.0.1',port=0, application=None, **kwargs)。

提到fixture方法httpbin(httpbin)中的參數httpbin是一個Server對象,可是這個對象是在何時建立的?原來這個httpbin也是一個fixture方法,存在於pytest-httpbin插件中。

pytest-httpbin/plugin.py

from __future__ import absolute_import
import pytest
from httpbin import app as httpbin_app
from . import serve, certs

@pytest.fixture(scope='session')
def httpbin(request):
    server = serve.Server(application=httpbin_app)
    server.start()
    request.addfinalizer(server.stop)
    return server


這是一個"session"級別的fixture方法,首先實例化Server對象爲server,傳入application參數"httpbin_app",application參數咱們在前面提到過,它指向咱們的web應用程序。

這裏的httpbin_app是pytest-httpbin下app模塊的別稱,該模塊是專門用於http測試而編寫的web應用程序,這裏就不擴展了。

而後server繼續調用start()方法,啓動線程,開啓WSGI服務器,最後返回server。

相關文章
相關標籤/搜索