web-dev-qa-db-ja.com

PythonリクエストでローカルURLからファイルをフェッチしますか?

アプリケーションの1つのメソッドでPythonの requests ライブラリを使用しています。メソッドの本体は次のようになります。

def handle_remote_file(url, **kwargs):
    response = requests.get(url, ...)
    buff = StringIO.StringIO()
    buff.write(response.content)
    ...
    return True

そのメソッドのユニットテストをいくつか記述したいのですが、次のような偽のローカルURLを渡します。

class RemoteTest(TestCase):
    def setUp(self):
        self.url = 'file:///tmp/dummy.txt'

    def test_handle_remote_file(self):
        self.assertTrue(handle_remote_file(self.url))

ローカルURLでrequests.getを呼び出すと、KeyError以下の例外:

requests.get('file:///tmp/dummy.txt')

/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/requests/packages/urllib3/poolmanager.pyc in connection_from_Host(self, Host, port, scheme)
76 
77         # Make a fresh ConnectionPool of the desired type
78         pool_cls = pool_classes_by_scheme[scheme]
79         pool = pool_cls(Host, port, **self.connection_pool_kw)
80 

KeyError: 'file'

問題は、ローカルURLをrequests.getに渡すにはどうすればよいですか?

PS:上記の例を作りました。多くのエラーが含まれている可能性があります。

24
ozgur

@WooParadogがリクエストを説明したように、ライブラリはローカルファイルを処理する方法を知りません。ただし、現在のバージョンでは トランスポートアダプター を定義できます。

したがって、ローカルファイルを処理できる独自のアダプタを定義するだけで済みます。例:

from requests_testadapter import Resp

class LocalFileAdapter(requests.adapters.HTTPAdapter):
    def build_response_from_file(self, request):
        file_path = request.url[7:]
        with open(file_path, 'rb') as file:
            buff = bytearray(os.path.getsize(file_path))
            file.readinto(buff)
            resp = Resp(buff)
            r = self.build_response(request, resp)

            return r

    def send(self, request, stream=False, timeout=None,
             verify=True, cert=None, proxies=None):

        return self.build_response_from_file(request)

requests_session = requests.session()
requests_session.mount('file://', LocalFileAdapter())
requests_session.get('file://<some_local_path>')

上記の例では requests-testadapter モジュールを使用しています。

30
b1r3k

これが私が書いたトランスポートアダプタです。これはb1r3kよりも機能が豊富で、リクエスト自体以外に依存関係はありません。私はまだ完全にテストしていませんが、私が試したことはバグがないようです。

import requests
import os, sys

if sys.version_info.major < 3:
    from urllib import url2pathname
else:
    from urllib.request import url2pathname

class LocalFileAdapter(requests.adapters.BaseAdapter):
    """Protocol Adapter to allow Requests to GET file:// URLs

    @todo: Properly handle non-empty hostname portions.
    """

    @staticmethod
    def _chkpath(method, path):
        """Return an HTTP status for the given filesystem path."""
        if method.lower() in ('put', 'delete'):
            return 501, "Not Implemented"  # TODO
        Elif method.lower() not in ('get', 'head'):
            return 405, "Method Not Allowed"
        Elif os.path.isdir(path):
            return 400, "Path Not A File"
        Elif not os.path.isfile(path):
            return 404, "File Not Found"
        Elif not os.access(path, os.R_OK):
            return 403, "Access Denied"
        else:
            return 200, "OK"

    def send(self, req, **kwargs):  # pylint: disable=unused-argument
        """Return the file specified by the given request

        @type req: C{PreparedRequest}
        @todo: Should I bother filling `response.headers` and processing
               If-Modified-Since and friends using `os.stat`?
        """
        path = os.path.normcase(os.path.normpath(url2pathname(req.path_url)))
        response = requests.Response()

        response.status_code, response.reason = self._chkpath(req.method, path)
        if response.status_code == 200 and req.method.lower() != 'head':
            try:
                response.raw = open(path, 'rb')
            except (OSError, IOError) as err:
                response.status_code = 500
                response.reason = str(err)

        if isinstance(req.url, bytes):
            response.url = req.url.decode('utf-8')
        else:
            response.url = req.url

        response.request = req
        response.connection = self

        return response

    def close(self):
        pass

(名前にもかかわらず、Googleをチェックする前に完全に記述されていたため、b1r3kとは関係ありません。)他の回答と同様に、次のようにしてください:

requests_session = requests.session()
requests_session.mount('file://', LocalFileAdapter())
r = requests_session.get('file:///path/to/your/file')
16
ssokolow

packages/urllib3/poolmanager.py かなり説明しています。リクエストはローカルURLをサポートしていません。

pool_classes_by_scheme = {                                                        
    'http': HTTPConnectionPool,                                                   
    'https': HTTPSConnectionPool,                                              
}                                                                                 
9
WooParadog

最近のプロジェクトでは、同じ問題がありました。リクエストは「ファイル」スキームをサポートしていないため、コンテンツをローカルにロードするようにコードにパッチを当てます。まず、requests.getを置き換える関数を定義します。

def local_get(self, url):
    "Fetch a stream from local files."
    p_url = six.moves.urllib.parse.urlparse(url)
    if p_url.scheme != 'file':
        raise ValueError("Expected file scheme")

    filename = six.moves.urllib.request.url2pathname(p_url.path)
    return open(filename, 'rb')

次に、テストセットアップまたはテスト関数の装飾のどこかで、mock.patchを使用して、要求時にget関数にパッチを適用します。

@mock.patch('requests.get', local_get)
def test_handle_remote_file(self):
    ...

この手法はやや脆弱です。基盤となるコードがrequests.requestを呼び出したり、Sessionを作成してそれを呼び出したりしても、役に立ちません。 file:のURLをサポートするためにリクエストを下位レベルでパッチする方法はあるかもしれませんが、私の最初の調査では、明らかなフックポイントはないようだったので、この単純なアプローチを採用しました。

6
Jason R. Coombs