diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dae4d9..9474c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ v0.8.0 (in development) ----------------------- - Support Python 3.9 +- `PyPISimple` is now usable as a context manager that will close the session + on exit v0.7.0 (2020-10-15) ------------------- diff --git a/README.rst b/README.rst index 4963ac3..b96223c 100644 --- a/README.rst +++ b/README.rst @@ -48,8 +48,8 @@ Example ======= >>> from pypi_simple import PyPISimple ->>> client = PyPISimple() ->>> requests_page = client.get_project_page('requests') +>>> with PyPISimple() as client: +... requests_page = client.get_project_page('requests') >>> pkg = requests_page.packages[0] >>> pkg DistributionPackage(filename='requests-0.2.0.tar.gz', url='https://files.pythonhosted.org/packages/ba/bb/dfa0141a32d773c47e4dede1a617c59a23b74dd302e449cf85413fc96bc4/requests-0.2.0.tar.gz#sha256=813202ace4d9301a3c00740c700e012fb9f3f8c73ddcfe02ab558a8df6f175fd', project='requests', version='0.2.0', package_type='sdist', requires_python=None, has_sig=None, yanked=None) diff --git a/docs/changelog.rst b/docs/changelog.rst index 117daf6..a560f27 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,8 @@ Changelog v0.8.0 (in development) ----------------------- - Support Python 3.9 +- `PyPISimple` is now usable as a context manager that will close the session + on exit v0.7.0 (2020-10-15) diff --git a/docs/index.rst b/docs/index.rst index 1916762..58c8281 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,8 +39,8 @@ Example ======== >>> from pypi_simple import PyPISimple ->>> client = PyPISimple() ->>> requests_page = client.get_project_page('requests') +>>> with PyPISimple() as client: +... requests_page = client.get_project_page('requests') >>> pkg = requests_page.packages[0] >>> pkg DistributionPackage(filename='requests-0.2.0.tar.gz', url='https://files.pythonhosted.org/packages/ba/bb/dfa0141a32d773c47e4dede1a617c59a23b74dd302e449cf85413fc96bc4/requests-0.2.0.tar.gz#sha256=813202ace4d9301a3c00740c700e012fb9f3f8c73ddcfe02ab558a8df6f175fd', project='requests', version='0.2.0', package_type='sdist', requires_python=None, has_sig=None, yanked=None) diff --git a/src/pypi_simple/client.py b/src/pypi_simple/client.py index 1f32225..f727667 100644 --- a/src/pypi_simple/client.py +++ b/src/pypi_simple/client.py @@ -34,6 +34,13 @@ class PyPISimple: caching), the user must create & configure a `requests.Session` object appropriately and pass it to the constructor as the ``session`` parameter. + A `PyPISimple` instance can be used as a context manager that will + automatically close its session on exit, regardless of where the session + object came from. + + .. versionchanged:: 0.8.0 + Now usable as a context manager + .. versionchanged:: 0.5.0 ``session`` argument added @@ -64,6 +71,12 @@ def __init__(self, endpoint: str = PYPI_SIMPLE_ENDPOINT, auth: Any = None, if auth is not None: self.s.auth = auth + def __enter__(self) -> "PyPISimple": + return self + + def __exit__(self, exc_type: Any, exc_value: Any, exc_tb: Any) -> None: + self.s.close() + def get_index_page( self, timeout: Union[float, Tuple[float,float], None] = None, diff --git a/test/test_client.py b/test/test_client.py index 5b7abd7..019ab5e 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -42,88 +42,88 @@ def test_session(mocker, content_type): body='Does not exist', status=404, ) - simple = PyPISimple('https://test.nil/simple/') - spy = mocker.spy(simple.s, 'get') - assert simple.get_index_page(timeout=3.14) == IndexPage( - projects=['in_place', 'foo', 'BAR'], - last_serial='12345', - repository_version='1.0', - ) - call, = spy.call_args_list - assert call[1]["timeout"] == 3.14 - spy.reset_mock() - assert simple.get_project_url('IN.PLACE') \ - == 'https://test.nil/simple/in-place/' - assert simple.get_project_page('IN.PLACE', timeout=2.718) == ProjectPage( - project='IN.PLACE', - packages=[ - DistributionPackage( - filename='in_place-0.1.1-py2.py3-none-any.whl', - project='in_place', - version='0.1.1', - package_type='wheel', - url="https://files.pythonhosted.org/packages/34/81/2baaaa588ee1a6faa6354b7c9bc365f1b3da867707cd136dfedff7c06608/in_place-0.1.1-py2.py3-none-any.whl#sha256=e0732b6bdc2f1bfc4e1b96c1de2fbbd053bb2a9534547474a0485baa339bfa97", - requires_python=None, - has_sig=None, - yanked=None, - ), - DistributionPackage( - filename='in_place-0.1.1.tar.gz', - project='in_place', - version='0.1.1', - package_type='sdist', - url="https://files.pythonhosted.org/packages/b9/ba/f1c67fb32c37ba4263326ae4ac6fd00b128025c9289b2fb31a60a0a22f90/in_place-0.1.1.tar.gz#sha256=ffa729fd0b818ac750aa31bafc886f266380e1c8557ba38f70f422d2f6a77e23", - requires_python=None, - has_sig=None, - yanked=None, - ), - DistributionPackage( - filename='in_place-0.2.0-py2.py3-none-any.whl', - project='in_place', - version='0.2.0', - package_type='wheel', - url="https://files.pythonhosted.org/packages/9f/46/9f5679f3b2068e10b33c16a628a78b2b36531a9df08671bd0104f11d8461/in_place-0.2.0-py2.py3-none-any.whl#sha256=e1ad42a41dfde02092b411b1634a4be228e28c27553499a81ef04b377b28857c", - requires_python=None, - has_sig=None, - yanked=None, - ), - DistributionPackage( - filename='in_place-0.2.0.tar.gz', - project='in_place', - version='0.2.0', - package_type='sdist', - url="https://files.pythonhosted.org/packages/f0/51/c30f1fad2b857f7b5d5ff76ec01f1f80dd0f2ab6b6afcde7b2aed54faa7e/in_place-0.2.0.tar.gz#sha256=ff783dca5d06f85b8d084871abd11a170d732423edb48c53ccb68c55fcbbeb76", - requires_python=None, - has_sig=None, - yanked=None, - ), - DistributionPackage( - filename='in_place-0.3.0-py2.py3-none-any.whl', - project='in_place', - version='0.3.0', - package_type='wheel', - url="https://files.pythonhosted.org/packages/6f/84/ced31e646df335f8cd1b7884e3740b8c012314a28504542ef5631cdc1449/in_place-0.3.0-py2.py3-none-any.whl#sha256=af5ce9bd309f85a6bbe4119acbc0a67cda68f0ae616f0a76a947addc62791fda", - requires_python=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", - has_sig=None, - yanked=None, - ), - DistributionPackage( - filename='in_place-0.3.0.tar.gz', - project='in_place', - version='0.3.0', - package_type='sdist', - url="https://files.pythonhosted.org/packages/b6/cd/1dc736d5248420b15dd1546c2938aec7e6dab134e698e0768f54f1757af7/in_place-0.3.0.tar.gz#sha256=4758db1457c8addcd5f5b15ef870eab66b238e46e7d784bff99ab1b2126660ea", - requires_python=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", - has_sig=None, - yanked=None, - ), - ], - last_serial='54321', - repository_version='1.0', - ) - call, = spy.call_args_list - assert call[1]["timeout"] == 2.718 - assert simple.get_project_page('nonexistent') is None + with PyPISimple('https://test.nil/simple/') as simple: + spy = mocker.spy(simple.s, 'get') + assert simple.get_index_page(timeout=3.14) == IndexPage( + projects=['in_place', 'foo', 'BAR'], + last_serial='12345', + repository_version='1.0', + ) + call, = spy.call_args_list + assert call[1]["timeout"] == 3.14 + spy.reset_mock() + assert simple.get_project_url('IN.PLACE') \ + == 'https://test.nil/simple/in-place/' + assert simple.get_project_page('IN.PLACE', timeout=2.718) == ProjectPage( + project='IN.PLACE', + packages=[ + DistributionPackage( + filename='in_place-0.1.1-py2.py3-none-any.whl', + project='in_place', + version='0.1.1', + package_type='wheel', + url="https://files.pythonhosted.org/packages/34/81/2baaaa588ee1a6faa6354b7c9bc365f1b3da867707cd136dfedff7c06608/in_place-0.1.1-py2.py3-none-any.whl#sha256=e0732b6bdc2f1bfc4e1b96c1de2fbbd053bb2a9534547474a0485baa339bfa97", + requires_python=None, + has_sig=None, + yanked=None, + ), + DistributionPackage( + filename='in_place-0.1.1.tar.gz', + project='in_place', + version='0.1.1', + package_type='sdist', + url="https://files.pythonhosted.org/packages/b9/ba/f1c67fb32c37ba4263326ae4ac6fd00b128025c9289b2fb31a60a0a22f90/in_place-0.1.1.tar.gz#sha256=ffa729fd0b818ac750aa31bafc886f266380e1c8557ba38f70f422d2f6a77e23", + requires_python=None, + has_sig=None, + yanked=None, + ), + DistributionPackage( + filename='in_place-0.2.0-py2.py3-none-any.whl', + project='in_place', + version='0.2.0', + package_type='wheel', + url="https://files.pythonhosted.org/packages/9f/46/9f5679f3b2068e10b33c16a628a78b2b36531a9df08671bd0104f11d8461/in_place-0.2.0-py2.py3-none-any.whl#sha256=e1ad42a41dfde02092b411b1634a4be228e28c27553499a81ef04b377b28857c", + requires_python=None, + has_sig=None, + yanked=None, + ), + DistributionPackage( + filename='in_place-0.2.0.tar.gz', + project='in_place', + version='0.2.0', + package_type='sdist', + url="https://files.pythonhosted.org/packages/f0/51/c30f1fad2b857f7b5d5ff76ec01f1f80dd0f2ab6b6afcde7b2aed54faa7e/in_place-0.2.0.tar.gz#sha256=ff783dca5d06f85b8d084871abd11a170d732423edb48c53ccb68c55fcbbeb76", + requires_python=None, + has_sig=None, + yanked=None, + ), + DistributionPackage( + filename='in_place-0.3.0-py2.py3-none-any.whl', + project='in_place', + version='0.3.0', + package_type='wheel', + url="https://files.pythonhosted.org/packages/6f/84/ced31e646df335f8cd1b7884e3740b8c012314a28504542ef5631cdc1449/in_place-0.3.0-py2.py3-none-any.whl#sha256=af5ce9bd309f85a6bbe4119acbc0a67cda68f0ae616f0a76a947addc62791fda", + requires_python=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", + has_sig=None, + yanked=None, + ), + DistributionPackage( + filename='in_place-0.3.0.tar.gz', + project='in_place', + version='0.3.0', + package_type='sdist', + url="https://files.pythonhosted.org/packages/b6/cd/1dc736d5248420b15dd1546c2938aec7e6dab134e698e0768f54f1757af7/in_place-0.3.0.tar.gz#sha256=4758db1457c8addcd5f5b15ef870eab66b238e46e7d784bff99ab1b2126660ea", + requires_python=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", + has_sig=None, + yanked=None, + ), + ], + last_serial='54321', + repository_version='1.0', + ) + call, = spy.call_args_list + assert call[1]["timeout"] == 2.718 + assert simple.get_project_page('nonexistent') is None @responses.activate def test_project_hint_received(): @@ -138,34 +138,34 @@ def test_project_hint_received(): body=fp.read(), content_type='text/html', ) - simple = PyPISimple('https://test.nil/simple/') - assert simple.get_project_page('aws-adfs-ebsco') == ProjectPage( - project='aws-adfs-ebsco', - packages=[ - DistributionPackage( - filename='aws-adfs-ebsco-0.3.6-2.tar.gz', - project='aws-adfs-ebsco', - version='0.3.6-2', - package_type='sdist', - url="https://files.pythonhosted.org/packages/13/b7/a69bdbf294db5ba0973ee45a2b2ce7045030cd922e1c0ca052d102c45b95/aws-adfs-ebsco-0.3.6-2.tar.gz#sha256=6eadd17408e1f26a313bc75afaa3011333bc2915461c446720bafd7608987e1e", - requires_python=None, - has_sig=None, - yanked=None, - ), - DistributionPackage( - filename='aws-adfs-ebsco-0.3.7-1.tar.gz', - project='aws-adfs-ebsco', - version='0.3.7-1', - package_type='sdist', - url="https://files.pythonhosted.org/packages/86/8a/46c2a99113cfbb7d6c089b2128ca9e4faaea1f6a1d4e17577fd9a3396bee/aws-adfs-ebsco-0.3.7-1.tar.gz#sha256=7992abc36d0061896a3f06f055e053ffde9f3fcf483340adfa675c65ebfb3f8d", - requires_python=None, - has_sig=None, - yanked=None, - ), - ], - last_serial=None, - repository_version=None, - ) + with PyPISimple('https://test.nil/simple/') as simple: + assert simple.get_project_page('aws-adfs-ebsco') == ProjectPage( + project='aws-adfs-ebsco', + packages=[ + DistributionPackage( + filename='aws-adfs-ebsco-0.3.6-2.tar.gz', + project='aws-adfs-ebsco', + version='0.3.6-2', + package_type='sdist', + url="https://files.pythonhosted.org/packages/13/b7/a69bdbf294db5ba0973ee45a2b2ce7045030cd922e1c0ca052d102c45b95/aws-adfs-ebsco-0.3.6-2.tar.gz#sha256=6eadd17408e1f26a313bc75afaa3011333bc2915461c446720bafd7608987e1e", + requires_python=None, + has_sig=None, + yanked=None, + ), + DistributionPackage( + filename='aws-adfs-ebsco-0.3.7-1.tar.gz', + project='aws-adfs-ebsco', + version='0.3.7-1', + package_type='sdist', + url="https://files.pythonhosted.org/packages/86/8a/46c2a99113cfbb7d6c089b2128ca9e4faaea1f6a1d4e17577fd9a3396bee/aws-adfs-ebsco-0.3.7-1.tar.gz#sha256=7992abc36d0061896a3f06f055e053ffde9f3fcf483340adfa675c65ebfb3f8d", + requires_python=None, + has_sig=None, + yanked=None, + ), + ], + last_serial=None, + repository_version=None, + ) @pytest.mark.parametrize('endpoint', [ 'https://test.nil/simple', @@ -177,8 +177,9 @@ def test_project_hint_received(): 'SOME_PROJECT', ]) def test_get_project_url(endpoint, project): - assert PyPISimple(endpoint).get_project_url(project) \ - == 'https://test.nil/simple/some-project/' + with PyPISimple(endpoint) as simple: + assert simple.get_project_url(project) \ + == 'https://test.nil/simple/some-project/' @responses.activate def test_redirected_project_page(): @@ -194,24 +195,24 @@ def test_redirected_project_page(): body='project-0.1.0.tar.gz', content_type='text/html', ) - simple = PyPISimple('https://nil.test/simple/') - assert simple.get_project_page('project') == ProjectPage( - project='project', - packages=[ - DistributionPackage( - filename='project-0.1.0.tar.gz', - project='project', - version='0.1.0', - package_type='sdist', - url="https://test.nil/simple/files/project-0.1.0.tar.gz", - requires_python=None, - has_sig=None, - yanked=None, - ), - ], - last_serial=None, - repository_version=None, - ) + with PyPISimple('https://nil.test/simple/') as simple: + assert simple.get_project_page('project') == ProjectPage( + project='project', + packages=[ + DistributionPackage( + filename='project-0.1.0.tar.gz', + project='project', + version='0.1.0', + package_type='sdist', + url="https://test.nil/simple/files/project-0.1.0.tar.gz", + requires_python=None, + has_sig=None, + yanked=None, + ), + ], + last_serial=None, + repository_version=None, + ) @pytest.mark.parametrize('content_type,body_decl', [ ('text/html; charset=utf-8', b''), @@ -226,24 +227,24 @@ def test_utf8_declarations(content_type, body_decl): body=body_decl + b'project-0.1.0-p\xC3\xBF42-none-any.whl', content_type=content_type, ) - simple = PyPISimple('https://test.nil/simple/') - assert simple.get_project_page('project') == ProjectPage( - project='project', - packages=[ - DistributionPackage( - filename='project-0.1.0-p\xFF42-none-any.whl', - project='project', - version='0.1.0', - package_type='wheel', - url="https://test.nil/simple/files/project-0.1.0-p\xFF42-none-any.whl", - requires_python=None, - has_sig=None, - yanked=None, - ), - ], - last_serial=None, - repository_version=None, - ) + with PyPISimple('https://test.nil/simple/') as simple: + assert simple.get_project_page('project') == ProjectPage( + project='project', + packages=[ + DistributionPackage( + filename='project-0.1.0-p\xFF42-none-any.whl', + project='project', + version='0.1.0', + package_type='wheel', + url="https://test.nil/simple/files/project-0.1.0-p\xFF42-none-any.whl", + requires_python=None, + has_sig=None, + yanked=None, + ), + ], + last_serial=None, + repository_version=None, + ) @pytest.mark.parametrize('content_type,body_decl', [ ('text/html; charset=iso-8859-2', b''), @@ -261,52 +262,53 @@ def test_latin2_declarations(content_type, body_decl): body=body_decl + b'project-0.1.0-p\xC3\xBF42-none-any.whl', content_type=content_type, ) - simple = PyPISimple('https://test.nil/simple/') - assert simple.get_project_page('project') == ProjectPage( - project='project', - packages=[ - DistributionPackage( - filename='project-0.1.0-p\u0102\u017C42-none-any.whl', - project='project', - version='0.1.0', - package_type='wheel', - url="https://test.nil/simple/files/project-0.1.0-p\u0102\u017C42-none-any.whl", - requires_python=None, - has_sig=None, - yanked=None, - ), - ], - last_serial=None, - repository_version=None, - ) + with PyPISimple('https://test.nil/simple/') as simple: + assert simple.get_project_page('project') == ProjectPage( + project='project', + packages=[ + DistributionPackage( + filename='project-0.1.0-p\u0102\u017C42-none-any.whl', + project='project', + version='0.1.0', + package_type='wheel', + url="https://test.nil/simple/files/project-0.1.0-p\u0102\u017C42-none-any.whl", + requires_python=None, + has_sig=None, + yanked=None, + ), + ], + last_serial=None, + repository_version=None, + ) def test_auth_new_session(): - simple = PyPISimple('https://test.nil/simple/', auth=('user', 'password')) - assert simple.s.auth == ('user', 'password') + with PyPISimple('https://test.nil/simple/', auth=('user', 'password')) \ + as simple: + assert simple.s.auth == ('user', 'password') def test_custom_session(): s = requests.Session() - simple = PyPISimple('https://test.nil/simple/', session=s) - assert simple.s is s - assert simple.s.auth is None + with PyPISimple('https://test.nil/simple/', session=s) as simple: + assert simple.s is s + assert simple.s.auth is None def test_auth_custom_session(): - simple = PyPISimple( + with PyPISimple( 'https://test.nil/simple/', auth=('user', 'password'), session=requests.Session(), - ) - assert simple.s.auth == ('user', 'password') + ) as simple: + assert simple.s.auth == ('user', 'password') def test_auth_override_custom_session(): s = requests.Session() s.auth = ('login', 'secret') - simple = PyPISimple( + with PyPISimple( 'https://test.nil/simple/', auth=('user', 'password'), session=s, - ) - assert simple.s.auth == ('user', 'password') + ) as simple: + assert simple.s.auth == ('user', 'password') @responses.activate def test_stream_project_names(mocker): @@ -326,9 +328,9 @@ def test_stream_project_names(mocker): body='This URL should only be requested once.', status=500, ) - simple = PyPISimple('https://test.nil/simple/') - spy = mocker.spy(simple.s, 'get') - assert list(simple.stream_project_names(timeout=1.618)) \ - == ['in_place', 'foo', 'BAR'] - call, = spy.call_args_list - assert call[1]["timeout"] == 1.618 + with PyPISimple('https://test.nil/simple/') as simple: + spy = mocker.spy(simple.s, 'get') + assert list(simple.stream_project_names(timeout=1.618)) \ + == ['in_place', 'foo', 'BAR'] + call, = spy.call_args_list + assert call[1]["timeout"] == 1.618