{ "cells": [ { "cell_type": "markdown", "id": "02d718a7-f3a0-41cc-a3fd-1aa6a9907ec4", "metadata": {}, "source": [ "# **Python Book 4**\n", "**[효율적 개발로 이끄는 파이썬 실천기술 Jupyter Notebook](https://nbviewer.org/github/Jpub/fulfillPython/tree/main/)**\n", "\n", "- [How to print colored text to the terminal](https://stackoverflow.com/questions/287871/how-to-print-colored-text-to-the-terminal)\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "id": "00e0a4d8-50b6-443d-a87e-7221a848a29c", "metadata": { "tags": [] }, "source": [ "## **[Chapter 12 단위 테스트](https://nbviewer.org/github/Jpub/fulfillPython/tree/main/12-unittest/)**\n", "## **1 unittest**\n", "── 표준 단위 테스트 라이브러리로 `assert` 메서드로 점검한다\n", "- `assert` 메서드가 여럿일 때, 한개라도 실패하면 Test Fail을 출력한다\n", "- `assertEqual(a,b)`, `assertTrue(x:bool)`, `assertIsNone(x:None)`\n", "- `assertIn(a:list, b)`, `assertAlmostEqual(a:round(a-b,7)==0, b)`" ] }, { "cell_type": "markdown", "id": "47da2a6c-ef28-40f9-ae7d-02ad448d6e69", "metadata": { "tags": [] }, "source": [ "### 01 **[Jupyter Notebook](https://www.wrighters.io/unit-testing-python-code-in-jupyter-notebooks/)** 에서 Unit Test 구현 " ] }, { "cell_type": "code", "execution_count": 1, "id": "99c5c1a3-9e2c-435f-bf7c-79247230a16e", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "test_booksearch (__main__.BookSearchTest) ... ok\n", "\n", "----------------------------------------------------------------------\n", "Ran 1 test in 0.001s\n", "\n", "OK\n" ] } ], "source": [ "import unittest\n", "\n", "def booksearch():\n", " return {}\n", "\n", "class BookSearchTest(unittest.TestCase):\n", " def test_booksearch(self):\n", " self.assertEqual({}, booksearch())\n", "\n", "# $ python -m unittest -v file.py\n", "test_result = unittest.main(argv=[''], verbosity=3, exit=False)\n", "# assert len(test_result.result.failures) == 0" ] }, { "cell_type": "markdown", "id": "95988058-d46b-43e7-95d3-7079d5077bfe", "metadata": { "tags": [] }, "source": [ "### 02 **Unit Test 예제**" ] }, { "cell_type": "code", "execution_count": 2, "id": "143b7d54-4d9f-41d3-8abf-0eac11ff02c4", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "test_booksearch (__main__.BookSearchTest) ... ok\n", "test_get_books_no_connection (__main__.GetBooksTest) ... ok\n", "test_save_thumbnails (__main__.SaveThumbnailsTest) ... ERROR\n", "\n", "======================================================================\n", "ERROR: test_save_thumbnails (__main__.SaveThumbnailsTest)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"/usr/lib/python3.8/unittest/mock.py\", line 1325, in patched\n", " return func(*newargs, **newkeywargs)\n", " File \"/tmp/ipykernel_46845/1025652656.py\", line 39, in test_save_thumbnails\n", " data_path = pathlib.Path(__file__).with_name('data')\n", "NameError: name '__file__' is not defined\n", "\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.014s\n", "\n", "FAILED (errors=1)\n" ] } ], "source": [ "import pathlib\n", "import unittest\n", "from unittest.mock import patch\n", "from urllib.error import URLError\n", "from tempfile import TemporaryDirectory\n", "\n", "THUMBNAIL_URL = (\n", " 'http://books.google.com/books/content'\n", " '?id=OgtBw76OY5EC&printsec=frontcover'\n", " '&img=1&zoom=1&edge=curl&source=gbs_api'\n", ")\n", "\n", "class SaveThumbnailsTest(unittest.TestCase):\n", " def setUp(self):\n", " # 임시 디렉터리 작성\n", " self.tmp = TemporaryDirectory()\n", "\n", " def tearDown(self):\n", " # 임시 디렉터리 정리\n", " self.tmp.cleanup()\n", "\n", " def test_save_thumbnails(self):\n", " from data.booksearch.core import Book\n", " book = Book({'id': '', 'volumeInfo': {\n", " 'imageLinks': {\n", " 'thumbnail': THUMBNAIL_URL\n", " }}})\n", " # 처리를 실행하고 파일이 작성되었음을 확인\n", " filename = book.save_thumbnails(self.tmp.name)[0]\n", " self.assertTrue(pathlib.Path(filename).exists())\n", "\n", " # 테스트 대상의 save_thumbnail()가 이용할 참조 이름을 지정\n", " @patch('booksearch.core.get_data')\n", " def test_save_thumbnails(self, mock_get_data):\n", " from data.booksearch.core import Book\n", "\n", " # 앞에서 얻은 섬네일 이미지 데이터를 목의 반환값으로 설정\n", " data_path = pathlib.Path(__file__).with_name('data')\n", " with open(data_path / 'YkGmfbil6L4C_thumbnail.jpeg', 'rb') as f:\n", " data = f.read()\n", " mock_get_data.return_value = data\n", "\n", " book = Book({'id': '', 'volumeInfo': {\n", " 'imageLinks': {\n", " 'thumbnail': THUMBNAIL_URL\n", " }}})\n", " filename = book.save_thumbnails(self.tmp.name)[0]\n", "\n", " # get_data() 호출 시의 인수 확인\n", " mock_get_data.assert_called_with(THUMBNAIL_URL)\n", "\n", " # 저장된 데이터 확인\n", " with open(filename, 'rb') as f:\n", " self.assertEqual(data, f.read())\n", "\n", "\n", "class GetBooksTest(unittest.TestCase):\n", " def test_get_books_no_connection(self):\n", " from data.booksearch.core import get_books\n", "\n", " # 임시로 네트워크 접속 단절\n", " with patch('socket.socket.connect') as mock:\n", " # connect()가 호출되면 정확하지 않은 값을 반환함\n", " mock.return_value = None\n", " with self.assertRaisesRegex(URLError, 'urlopen error'):\n", " # 예외가 발생하는 처리를 with 블록 안에서 실행\n", " get_books(q='python')\n", "\n", "# $ python -m unittest -v file.py\n", "test_result = unittest.main(argv=[''], verbosity=3, exit=False)\n", "# assert len(test_result.result.failures) == 0" ] }, { "cell_type": "markdown", "id": "03a3007c-6e89-40e7-a1aa-65115299d2b6", "metadata": { "tags": [] }, "source": [ "## **[Chapter 13 Typing](https://www.daleseo.com/python-typing/)**\n", "## **1 Annotation**\n", "- 파이썬은 인터프린터 언어로, 동적 타입변수를 사용합니다.\n", "- 함수와, 변수에 `annotation` 을 추가 할 수 있지만, 실질적인 제약은 기능하지 않습니다\n", "- 작성한 어노테이션을 확인하려면 `__annotations__` 내장 객체를 사용 합니다. " ] }, { "cell_type": "code", "execution_count": 3, "id": "918d1472-50b7-4697-a37e-201f13088153", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'age': int, 'num': int, 'name': str, 'emails': list, 'address': dict}" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "age: int = 25\n", "num: int = 1\n", "name: str = \"John Doe\"\n", "emails: list = [\"john1@doe.com\", \"john2@doe.com\"]\n", "address: dict = {\n", " \"street\": \"54560 Daugherty Brooks Suite 581\",\n", " \"city\": \"Stokesmouth\",\n", " \"state\": \"NM\",\n", " \"zip\": \"80556\"\n", "}\n", "\n", "__annotations__" ] }, { "cell_type": "code", "execution_count": 4, "id": "a2a180cd-523e-43cf-8eb1-75bc02224224", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'message': str, 'times': int, 'return': list}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def repeat(message: str, times: int = 2) -> list:\n", " return [message] * times\n", "\n", "repeat.__annotations__" ] }, { "cell_type": "markdown", "id": "666800b3-bcbf-4507-ac82-e518b31d47f8", "metadata": { "tags": [] }, "source": [ "## **2 [Typing](https://www.daleseo.com/python-typing/)**\n", "── Python 3.5 부터 타입힌트(Type Hint)를 언어 차원에서 지원하고 있습니다.\n", "- 복잡한 타입의 Annotation 을 추가 하지만, 기능적 제약은 없다.\n", "- 따라서 `! pip install mypy` [모듈을 사용해야 디버깅 내용을 명확하게 안내 한다](https://www.daleseo.com/python-mypy/)" ] }, { "cell_type": "markdown", "id": "7c198ad3-f6d7-4882-b199-f6404abd2a78", "metadata": { "tags": [] }, "source": [ "### 01 **List, Dict, Tuple, Set**\n", "- 파이썬 내장 자료구조에 대한 타입을 명시 가능합니다\n", "- `python -m mypy --strict example.py` 와 같이 외부 모듈을 사용 합니다" ] }, { "cell_type": "code", "execution_count": 5, "id": "3e95215d-0a0e-4a57-9052-15b87666f037", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "{'age': int,\n", " 'num': int,\n", " 'name': str,\n", " 'emails': list,\n", " 'address': dict,\n", " 'nums': typing.List[int],\n", " 'unique_nums': typing.Set[int],\n", " 'vision': typing.Dict[str, float],\n", " 'john': typing.Tuple[int, str, typing.List[float]],\n", " 'chars': typing.Set[str]}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from typing import List, Set, Dict, Tuple\n", "\n", "nums: List[int] = [1]\n", "unique_nums: Set[int] = {6, 7}\n", "vision: Dict[str, float] = {'left': 1.0, 'right': 0.9}\n", "john: Tuple[int, str, List[float]] = (25, \"John Doe\", [1.0, 0.9])\n", "\n", "from typing import Set\n", "chars: Set[str] = {\"A\", \"B\", \"C\"}\n", "\n", "__annotations__" ] }, { "cell_type": "markdown", "id": "92668eaf-24dc-4f15-940e-4c84baa5697b", "metadata": { "tags": [] }, "source": [ "### 02 **Final**\n", "재할당이 불가능한 상수변수에 타입을 추가하는 경우에 활용" ] }, { "cell_type": "code", "execution_count": 6, "id": "546022ec-3d9d-4ff3-8bcd-804ae1658736", "metadata": {}, "outputs": [], "source": [ "from typing import Final\n", "TIME_OUT: Final[int] = 10" ] }, { "cell_type": "markdown", "id": "30ba9a70-39cd-4c55-ad9a-a1cf7b8683f4", "metadata": { "tags": [] }, "source": [ "### 03 **Union**\n", "여러 개의 타입이 허용될 수 있는 상황에서는 typing 모듈의 Union을 사용할 수 있습니다" ] }, { "cell_type": "code", "execution_count": 7, "id": "035b1629-f017-405a-8b7c-3b4b2d5dd45c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('1', '1.5', 'python')" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from typing import Union\n", "\n", "def toString(num: Union[int, float]) -> str:\n", " return str(num)\n", "\n", "toString(1), toString(1.5), toString('python')" ] }, { "cell_type": "markdown", "id": "8b21edd8-3a9b-4509-9689-8ab38be78481", "metadata": { "tags": [] }, "source": [ "### 04 **Optional**\n", "`None` 이 허용되는 함수의 매개 변수에 대한 타입을 명시할 때 유용합니다." ] }, { "cell_type": "code", "execution_count": 8, "id": "3ba58337-8ccf-4a5b-98ee-7627fdb4f39a", "metadata": {}, "outputs": [], "source": [ "from typing import Optional\n", "\n", "def repeat(message: str, times: Optional[int] = None) -> list:\n", " if times:\n", " return [message] * times\n", " else:\n", " return [message]" ] }, { "cell_type": "markdown", "id": "2d089aa1-604d-40b5-a450-891db555dca1", "metadata": { "tags": [] }, "source": [ "### 05 **Callable**\n", "함수에 대한 타입 어노테이션을 추가할 때 사용합니다.\n", "- 예시로 작성한 `repeat` 함수는 첫 번째 매개 변수 `greet` 인자를 갖는다. \n", "- 매개 변수에 타입 어노테이션 `Callable[[str], str]` 를 추가 한다.\n", "- `str` 타입의 인자를 하나 받고, 결과값은 `str` 로 반환하는 함수가 된다.\n", "- 람다 함수를 작성할 때도 동일한 타입 어노테이션을 사용할 수 있습니다." ] }, { "cell_type": "code", "execution_count": 9, "id": "90d49d4e-d844-4b33-bc7c-60669583ec0e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hi, Dale!\n", "Hi, Dale!\n" ] } ], "source": [ "from typing import Callable\n", "\n", "greet: Callable[[str], str] = lambda name: f\"Hi, {name}!\"\n", "\n", "def repeat(\n", " greet: Callable[[str], str], \n", " name: str, \n", " times: int = 2) -> None:\n", "\n", " for _ in range(times):\n", " print(greet(name))\n", " \n", "repeat(greet, \"Dale\")" ] }, { "cell_type": "markdown", "id": "3c708739-79ed-4ab1-a246-a42ace966516", "metadata": { "tags": [] }, "source": [ "### 06 **타입 추상화**\n", "- 함수의 매개 변수에 대한 타입 어노테이션을 `타입을 추상적으로 명시` 해주는 것이 유리한 경우가 많습니다. \n", "- 아래 toStrings() 함수는 nums 매개 변수의 타입을 List[int] 대신에 Iterable[int]로 명시 합니다" ] }, { "cell_type": "code", "execution_count": 10, "id": "b398e267-3543-423a-abbb-8573e10157e1", "metadata": {}, "outputs": [], "source": [ "from typing import Iterable, List\n", "\n", "def toStrings(nums: Iterable[int]) -> List[str]:\n", " return [str(x) for x in nums]" ] }, { "cell_type": "markdown", "id": "397c5014-1559-4f3c-80dc-2b734bf56933", "metadata": { "tags": [] }, "source": [ "## **2 [VPN 우회 크롤링](https://jvvp.tistory.com/1114)**\n", "- https://surpassing.tistory.com/917\n", "- https://free-proxy-list.net/\n", "- https://jvvp.tistory.com/1114" ] }, { "cell_type": "code", "execution_count": 11, "id": "3ad434ed-6432-4611-aa4f-deeffcc2d01b", "metadata": {}, "outputs": [], "source": [ "# 140.227.238.217:3128\tJP\tJapan\telite proxy\tno\tno\t12 mins ago\n", "# 140.227.201.6:3128\tJP\tJapan\tanonymous\tno\tyes\t12 mins ago\n", "url = 'https://javgg.net'\n", "proxy = 'socks5://92.42.109.187:1080'\n", "\n", "proxies = {\"http\": proxy, 'https': proxy}\n", "headers = {\n", " \"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36\",\n", " \"content-type\": \"application/json\",\n", "}\n", "\n", "import requests\n", "# response = requests.get(url, headers=headers, proxies=proxies, timeout=10)\n", "# response" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 5 }