diff options
author | Jordan Cook <jordan.cook.git@proton.me> | 2022-09-30 18:42:34 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-30 18:42:34 -0500 |
commit | f7e029ee27562634d42b609f6484216dc7960a57 (patch) | |
tree | 0070428d0148f9142be8ea3cc797bff5ce2e4da0 | |
parent | e1de1742fc8f91229fff069b12c3ce0b23a43d16 (diff) | |
parent | 55dc02d773048362f0738759f25d07effc9cd951 (diff) | |
download | requests-cache-f7e029ee27562634d42b609f6484216dc7960a57.tar.gz |
Merge pull request #699 from requests-cache/deploy-config
Improvements for deployment workflow
-rw-r--r-- | .github/workflows/build.yml | 2 | ||||
-rw-r--r-- | .github/workflows/deploy.yml | 88 | ||||
-rw-r--r-- | HISTORY.md | 1 | ||||
-rw-r--r-- | requests_cache/backends/base.py | 17 | ||||
-rwxr-xr-x | requests_cache/models/response.py | 6 | ||||
-rw-r--r-- | tests/conftest.py | 10 | ||||
-rw-r--r-- | tests/unit/test_base_cache.py | 63 | ||||
-rw-r--r-- | tests/unit/test_patcher.py | 5 | ||||
-rw-r--r-- | tests/unit/test_session.py | 3 |
9 files changed, 131 insertions, 64 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2f5e9e..7f02b51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: # Start integration test databases - uses: supercharge/mongodb-github-action@1.8.0 with: - mongodb-version: 4.4 + mongodb-version: 5.0 - uses: supercharge/redis-github-action@1.4.0 with: redis-version: 6 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8c3d14b..2efcd7d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,25 +16,27 @@ on: description: 'Version number for pre-releases; defaults to build number' required: false default: '' + skip-stress: + description: 'Set to "true" to skip stress tests' + required: false + default: 'false' + skip-publish: + description: 'Set to "true" to skip publishing to PyPI' + required: false + default: 'false' env: LATEST_PY_VERSION: '3.10' PYTEST_VERBOSE: 'true' - STRESS_TEST_MULTIPLIER: 5 + STRESS_TEST_MULTIPLIER: 7 jobs: - # Run tests for all supported requests versions and minimum supported python version - test: + + # Run additional integration stress tests + test-stress: + if: ${{ github.event.inputs.skip-stress != 'true' }} runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.7] - requests-version: [2.22, 2.23, 2.24, 2.25, 2.26, 2.27, 2.28] - # Run tests for most recent python and requests versions - include: - - python-version: '3.10' - requests-version: latest - fail-fast: false + services: nginx: image: kennethreitz/httpbin @@ -46,7 +48,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ env.LATEST_PY_VERSION }} - uses: snok/install-poetry@v1.3 with: virtualenvs-in-project: true @@ -54,7 +56,7 @@ jobs: # Start integration test databases - uses: supercharge/mongodb-github-action@1.8.0 with: - mongodb-version: 4.4 + mongodb-version: 5.0 - uses: supercharge/redis-github-action@1.4.0 with: redis-version: 6 @@ -66,18 +68,15 @@ jobs: uses: actions/cache@v3 with: path: .venv - key: venv-${{ matrix.python-version }}-${{ matrix.requests-version }}-${{ hashFiles('poetry.lock') }} + key: venv-${{ env.LATEST_PY_VERSION }}-latest-${{ hashFiles('poetry.lock') }} - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' - run: | - poetry add requests@${{ matrix.requests-version }} --lock - poetry install -v -E all + run: poetry install -v -E all - # Run unit + integration tests, with additional stress tests - - name: Run tests + # Run tests + - name: Run stress tests run: | source $VENV - nox -e test-current nox -e stress -- ${{ env.STRESS_TEST_MULTIPLIER }} # Run unit tests without any optional dependencies installed @@ -92,7 +91,6 @@ jobs: python-version: ${{ env.LATEST_PY_VERSION }} - uses: snok/install-poetry@v1.3 with: - version: 1.2.0b1 virtualenvs-in-project: true # Cache packages per python version, and reuse until lockfile changes @@ -106,22 +104,60 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: poetry install -v - - name: Run unit tests with no optional dependencies + # Run tests + - name: Run tests with no optional dependencies run: | source $VENV pytest -n auto tests/unit + # Run unit tests for all supported platforms, python versions, and requests versions + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.7, 3.8, 3.9, '3.10'] + requests-version: [2.22, 2.23, 2.24, 2.25, 2.26, 2.27, latest] + fail-fast: false + runs-on: ${{ matrix.os }} + + steps: + # Set up python + poetry + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: snok/install-poetry@v1.3 + with: + virtualenvs-in-project: true + + # Cache packages per python version, and reuse until lockfile changes + - name: Cache python packages + id: cache + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.requests-version }}-${{ hashFiles('poetry.lock') }} + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: | + poetry add requests@${{ matrix.requests-version }} --lock + poetry install -v -E all + + # Run tests + - name: Run tests + run: poetry run pytest -n auto tests/unit # Deploy stable builds on tags only, and pre-release builds from manual trigger ("workflow_dispatch") release: - needs: [test, test-minimum-deps] + if: ${{ github.event.inputs.skip-publish != 'true' }} + needs: [test, test-minimum-deps, test-stress] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ env.LATEST_PY_VERSION }} - - uses: snok/install-psoetry@v1.3 + - uses: snok/install-poetry@v1.3 with: virtualenvs-in-project: true @@ -134,7 +170,7 @@ jobs: poetry version $(poetry version -s).${{ env.pre-release-suffix }}${{ env.pre-release-version }} poetry version - - name: Build and publish to pypi + - name: Build and publish to PyPI run: | poetry build poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} @@ -89,6 +89,7 @@ * Fix usage of memory backend with `install_cache()` * Add `CachedRequest.path_url` property for compatibility with `RequestEncodingMixin` * Add compatibility with cattrs 22.1+ +* Fix issue on Windows with occasional missing `CachedResponse.created_at` timestamp **Dependencies:** * Replace `appdirs` with `platformdirs` diff --git a/requests_cache/backends/base.py b/requests_cache/backends/base.py index 9814799..cd1dd44 100644 --- a/requests_cache/backends/base.py +++ b/requests_cache/backends/base.py @@ -247,7 +247,8 @@ class BaseCache: def delete_url(self, url: str, method: str = 'GET', **kwargs): warn( - 'BaseCache.delete_url() is deprecated; please use .delete() instead', DeprecationWarning + 'BaseCache.delete_url() is deprecated; please use .delete() instead', + DeprecationWarning, ) self.delete(requests=[Request(method, url, **kwargs)]) @@ -260,14 +261,15 @@ class BaseCache: def has_url(self, url: str, method: str = 'GET', **kwargs) -> bool: warn( - 'BaseCache.has_url() is deprecated; please use .contains() instead', DeprecationWarning + 'BaseCache.has_url() is deprecated; please use .contains() instead', + DeprecationWarning, ) return self.contains(request=Request(method, url, **kwargs)) def keys(self, check_expiry: bool = False) -> Iterator[str]: warn( - 'BaseCache.keys() is deprecated; please use .filter() or ' - 'BaseCache.responses.keys() instead', + 'BaseCache.keys() is deprecated; ' + 'please use .filter() or BaseCache.responses.keys() instead', DeprecationWarning, ) yield from self.redirects.keys() @@ -276,15 +278,16 @@ class BaseCache: def response_count(self, check_expiry: bool = False) -> int: warn( - 'BaseCache.response_count() is deprecated; please use .filter() or ' - 'len(BaseCache.responses) instead', + 'BaseCache.response_count() is deprecated; ' + 'please use .filter() or len(BaseCache.responses) instead', DeprecationWarning, ) return len(list(self.filter(expired=not check_expiry))) def remove_expired_responses(self, expire_after: ExpirationTime = None): warn( - 'BaseCache.remove_expired_responses() is deprecated; please use .delete() instead', + 'BaseCache.remove_expired_responses() is deprecated; ' + 'please use .delete(expired=True) instead', DeprecationWarning, ) self.delete(expired=True, invalid=True) diff --git a/requests_cache/models/response.py b/requests_cache/models/response.py index c1f85e1..e704f03 100755 --- a/requests_cache/models/response.py +++ b/requests_cache/models/response.py @@ -66,7 +66,7 @@ class CachedResponse(RichMixin, BaseResponse): _decoded_content: DecodedContent = field(default=None) _next: Optional[CachedRequest] = field(default=None) cookies: RequestsCookieJar = field(factory=RequestsCookieJar) - created_at: datetime = field(factory=datetime.utcnow) + created_at: datetime = field(default=None) elapsed: timedelta = field(factory=timedelta) encoding: str = field(default=None) expires: Optional[datetime] = field(default=None) @@ -79,7 +79,9 @@ class CachedResponse(RichMixin, BaseResponse): url: str = field(default=None) def __attrs_post_init__(self): - """Re-initialize raw (urllib3) response after deserialization""" + # Not using created_at field default due to possible bug on Windows with omit_if_default + self.created_at = self.created_at or datetime.utcnow() + # Re-initialize raw (urllib3) response after deserialization self.raw = self.raw or CachedHTTPResponse.from_cached_response(self) @classmethod diff --git a/tests/conftest.py b/tests/conftest.py index e09d7f1..84710ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,8 @@ Note: The protocol ``http(s)+mock://`` helps :py:class:`requests_mock.Adapter` p https://requests-mock.readthedocs.io/en/latest/adapter.html """ import os +import warnings +from contextlib import contextmanager from datetime import datetime, timedelta from functools import wraps from importlib import import_module @@ -278,5 +280,13 @@ def skip_missing_deps(module_name: str) -> pytest.Mark: ) +@contextmanager +def ignore_deprecation(): + """Temporarily ilence deprecation warnings""" + with warnings.catch_warnings(): + warnings.simplefilter('ignore', category=DeprecationWarning) + yield + + # Some tests must disable url normalization to retain the custom `http+mock://` protocol patch_normalize_url = patch('requests_cache.cache_keys.normalize_url', side_effect=lambda x, y: x) diff --git a/tests/unit/test_base_cache.py b/tests/unit/test_base_cache.py index 033a673..0a0b265 100644 --- a/tests/unit/test_base_cache.py +++ b/tests/unit/test_base_cache.py @@ -17,6 +17,7 @@ from tests.conftest import ( MOCKED_URL_HTTPS, MOCKED_URL_JSON, MOCKED_URL_REDIRECT, + ignore_deprecation, patch_normalize_url, ) @@ -58,7 +59,8 @@ def test_delete__expired(mock_normalize_url, mock_session): mock_session.settings.expire_after = 1 mock_session.get(MOCKED_URL) mock_session.get(MOCKED_URL_JSON) - sleep(1) + sleep(1.1) + mock_session.settings.expire_after = 2 mock_session.get(unexpired_url) # At this point we should have 1 unexpired response and 2 expired responses @@ -71,7 +73,7 @@ def test_delete__expired(mock_normalize_url, mock_session): assert cached_response.url == unexpired_url # Now the last response should be expired as well - sleep(1) + sleep(2) BaseCache.delete(mock_session.cache, expired=True) assert len(mock_session.cache.responses) == 0 @@ -226,8 +228,8 @@ def test_clear(mock_session): mock_session.get(MOCKED_URL) mock_session.get(MOCKED_URL_REDIRECT) mock_session.cache.clear() - assert not mock_session.cache.has_url(MOCKED_URL) - assert not mock_session.cache.has_url(MOCKED_URL_REDIRECT) + assert not mock_session.cache.contains(request=Request('GET', MOCKED_URL)) + assert not mock_session.cache.contains(request=Request('GET', MOCKED_URL_REDIRECT)) def test_save_response__manual(mock_session): @@ -273,51 +275,59 @@ def test_urls__error(mock_session): def test_has_url(mock_session): mock_session.get(MOCKED_URL, params={'foo': 'bar'}) - assert mock_session.cache.has_url(MOCKED_URL, params={'foo': 'bar'}) - assert not mock_session.cache.has_url(MOCKED_URL) + with ignore_deprecation(): + assert mock_session.cache.has_url(MOCKED_URL, params={'foo': 'bar'}) + assert not mock_session.cache.has_url(MOCKED_URL) def test_delete_url(mock_session): mock_session.get(MOCKED_URL) - mock_session.cache.delete_url(MOCKED_URL) - assert not mock_session.cache.has_url(MOCKED_URL) + with ignore_deprecation(): + mock_session.cache.delete_url(MOCKED_URL) + assert not mock_session.cache.has_url(MOCKED_URL) def test_delete_url__request_args(mock_session): mock_session.get(MOCKED_URL, params={'foo': 'bar'}) - mock_session.cache.delete_url(MOCKED_URL, params={'foo': 'bar'}) - assert not mock_session.cache.has_url(MOCKED_URL, params={'foo': 'bar'}) + with ignore_deprecation(): + mock_session.cache.delete_url(MOCKED_URL, params={'foo': 'bar'}) + assert not mock_session.cache.has_url(MOCKED_URL, params={'foo': 'bar'}) def test_delete_url__nonexistent_response(mock_session): """Deleting a response that was either already deleted (or never added) should fail silently""" - mock_session.cache.delete_url(MOCKED_URL) + with ignore_deprecation(): + mock_session.cache.delete_url(MOCKED_URL) - mock_session.get(MOCKED_URL) - mock_session.cache.delete_url(MOCKED_URL) - assert not mock_session.cache.has_url(MOCKED_URL) - mock_session.cache.delete_url(MOCKED_URL) # Should fail silently + mock_session.get(MOCKED_URL) + mock_session.cache.delete_url(MOCKED_URL) + + assert not mock_session.cache.has_url(MOCKED_URL) + mock_session.cache.delete_url(MOCKED_URL) # Should fail silently def test_delete_urls(mock_session): mock_session.get(MOCKED_URL) - mock_session.cache.delete_urls([MOCKED_URL]) - assert not mock_session.cache.has_url(MOCKED_URL) + with ignore_deprecation(): + mock_session.cache.delete_urls([MOCKED_URL]) + assert not mock_session.cache.has_url(MOCKED_URL) def test_keys(mock_session): for url in [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_REDIRECT]: mock_session.get(url) - all_keys = set(mock_session.cache.responses.keys()) | set(mock_session.cache.redirects.keys()) - assert set(mock_session.cache.keys()) == all_keys + with ignore_deprecation(): + response_keys = set(mock_session.cache.responses.keys()) + redirect_keys = set(mock_session.cache.redirects.keys()) + assert set(mock_session.cache.keys()) == response_keys | redirect_keys def test_remove_expired_responses(mock_session): """Test for backwards-compatibility""" - with patch.object(mock_session.cache, 'delete') as mock_delete, patch.object( - mock_session.cache, 'reset_expiration' - ) as mock_reset: + with ignore_deprecation(), patch.object( + mock_session.cache, 'delete' + ) as mock_delete, patch.object(mock_session.cache, 'reset_expiration') as mock_reset: mock_session.cache.remove_expired_responses(expire_after=1) mock_delete.assert_called_once_with(expired=True, invalid=True) mock_reset.assert_called_once_with(1) @@ -335,14 +345,17 @@ def test_response_count(check_expiry, expected_count, mock_session): mock_session.cache.responses['expired_response'] = CachedResponse(expires=YESTERDAY) mock_session.cache.responses['invalid_response'] = InvalidResponse() - assert mock_session.cache.response_count(check_expiry=check_expiry) == expected_count + with ignore_deprecation(): + response_count = mock_session.cache.response_count(check_expiry=check_expiry) + assert response_count == expected_count def test_values(mock_session): for url in [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_HTTPS]: mock_session.get(url) - responses = list(mock_session.cache.values()) + with ignore_deprecation(): + responses = list(mock_session.cache.values()) assert len(responses) == 3 assert all([isinstance(response, CachedResponse) for response in responses]) @@ -354,7 +367,7 @@ def test_values__with_invalid_responses(check_expiry, expected_count, mock_sessi responses[1] = AttributeError responses[2] = CachedResponse(expires=YESTERDAY, url='test') - with patch.object(SQLiteDict, '__getitem__', side_effect=responses): + with ignore_deprecation(), patch.object(SQLiteDict, '__getitem__', side_effect=responses): values = mock_session.cache.values(check_expiry=check_expiry) assert len(list(values)) == expected_count diff --git a/tests/unit/test_patcher.py b/tests/unit/test_patcher.py index ceb2a4c..8977271 100644 --- a/tests/unit/test_patcher.py +++ b/tests/unit/test_patcher.py @@ -6,7 +6,7 @@ from requests.sessions import Session as OriginalSession import requests_cache from requests_cache import CachedSession from requests_cache.backends import BaseCache, SQLiteCache -from tests.conftest import CACHE_NAME +from tests.conftest import CACHE_NAME, ignore_deprecation def test_install_uninstall(): @@ -98,6 +98,7 @@ def test_delete__cache_not_installed(mock_delete): @patch.object(BaseCache, 'delete') def test_remove_expired_responses(mock_delete): requests_cache.install_cache(backend='memory', expire_after=360) - requests_cache.remove_expired_responses() + with ignore_deprecation(): + requests_cache.remove_expired_responses() assert mock_delete.called is True requests_cache.uninstall_cache() diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index dda449a..60f2d42 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -29,6 +29,7 @@ from tests.conftest import ( MOCKED_URL_REDIRECT, MOCKED_URL_REDIRECT_TARGET, MOCKED_URL_VARY, + ignore_deprecation, patch_normalize_url, ) @@ -895,6 +896,6 @@ def test_request_force_refresh__prepared_request(mock_session): def test_remove_expired_responses(mock_session): - with patch.object(mock_session.cache, 'delete') as mock_delete: + with ignore_deprecation(), patch.object(mock_session.cache, 'delete') as mock_delete: mock_session.remove_expired_responses() mock_delete.assert_called_once_with(expired=True, invalid=True) |