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