From 48d5ead7488226ad0390e8def1a9b8afe582f68b Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Sat, 24 Aug 2024 11:50:15 +0100 Subject: [PATCH] Remove support for py38 and enable py312 testing --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/jira_cloud_ci.yml | 4 +- .github/workflows/jira_server_ci.yml | 2 +- .github/workflows/release.yml | 4 +- .github/workflows/tox.yml | 26 +++- bindep.txt | 2 + constraints.txt | 214 +++++++++++++------------- docs/conf.py | 2 +- docs/contributing.rst | 4 +- docs/installation.rst | 2 +- jira/client.py | 28 ++-- jira/resources.py | 94 +++++------ pyproject.toml | 12 +- tests/resources/test_board.py | 2 +- tests/resources/test_epic.py | 2 +- tests/resources/test_sprint.py | 2 +- tests/test_exceptions.py | 16 +- tox.ini | 12 +- 18 files changed, 223 insertions(+), 207 deletions(-) create mode 100644 bindep.txt diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b3b7cfee5..44708ffd7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -50,7 +50,7 @@ body: attributes: label: Python Interpreter version description: The version(s) of Python used. - placeholder: "3.8" + placeholder: "3.9" validations: required: true - type: checkboxes diff --git a/.github/workflows/jira_cloud_ci.yml b/.github/workflows/jira_cloud_ci.yml index 2ecc5b47f..377dd06cc 100644 --- a/.github/workflows/jira_cloud_ci.yml +++ b/.github/workflows/jira_cloud_ci.yml @@ -23,7 +23,7 @@ jobs: os: [ubuntu-latest] # We only test a single version to prevent concurrent # running of tests influencing one another - python-version: ["3.8"] + python-version: ["3.9"] steps: - uses: actions/checkout@v4 @@ -41,7 +41,7 @@ jobs: python -m pip install --upgrade tox tox-gh-actions - name: Test with tox - run: tox -e py38 -- -m allow_on_cloud + run: tox -e py39 -- -m allow_on_cloud env: CI_JIRA_TYPE: CLOUD CI_JIRA_CLOUD_ADMIN: ${{ secrets.CLOUD_ADMIN }} diff --git a/.github/workflows/jira_server_ci.yml b/.github/workflows/jira_server_ci.yml index 6b1c46644..9b5ca43b2 100644 --- a/.github/workflows/jira_server_ci.yml +++ b/.github/workflows/jira_server_ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11"] jira-version: [8.17.1] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 473b73a39..2e7fb6c47 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,10 +17,10 @@ jobs: TOX_PARALLEL_NO_SPINNER: 1 steps: - - name: Switch to using Python 3.8 by default + - name: Switch to using Python 3.9 by default uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install build dependencies run: python3 -m pip install --user tox diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index f6c953481..0097018e2 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -32,8 +32,14 @@ jobs: lint docs pkg - # ^ arm64 runner is using py311 for matching python version used in AAP 2.5 - platforms: linux,macos,linux-arm64:ubuntu-24.04-arm64-2core + py39:tox -e py39 --notest + py310:tox -e py310 --notest + py311:tox -e py311 --notest + py312:tox -e py312 --notest + py39-macos:tox -e py312 --notest + py312-macos:tox -e py312 --notest + # ^ macos is also used to validate arm64 building + platforms: linux,macos skip_explode: "1" build: name: ${{ matrix.name }} @@ -45,6 +51,13 @@ jobs: matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} steps: + - name: Install package dependencies (ubuntu) + if: ${{ contains(matrix.os, 'ubuntu') }} + run: | + sudo apt remove -y docker-compose + sudo apt-get update -y + sudo apt-get --assume-yes --no-install-recommends install -y apt-transport-https curl libkrb5-dev + - uses: actions/checkout@v4 with: fetch-depth: 0 # needed by setuptools-scm @@ -138,8 +151,13 @@ jobs: name: logs.zip path: . - - name: Check for expected number of coverage reports - run: .github/check-coverage.sh + - name: Check for expected number of coverage.xml reports + run: | + JOBS_PRODUCING_COVERAGE=0 + if [ "$(find . -name coverage.xml | wc -l | bc)" -ne "${JOBS_PRODUCING_COVERAGE}" ]; then + echo "::error::Number of coverage.xml files was not the expected one (${JOBS_PRODUCING_COVERAGE}): $(find . -name coverage.xml |xargs echo)" + exit 1 + fi # Single uploads inside check job for codecov to allow use to retry # it when it fails without running tests again. Fails often enough! diff --git a/bindep.txt b/bindep.txt new file mode 100644 index 000000000..e4246ba0b --- /dev/null +++ b/bindep.txt @@ -0,0 +1,2 @@ +# gssapi pypi wheel build needs: +libkrb5-dev [platform:dpkg] diff --git a/constraints.txt b/constraints.txt index db5b572fb..589254686 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,134 +1,137 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # -# pip-compile --extra=async --extra=cli --extra=docs --extra=opt --extra=test --output-file=constraints.txt --strip-extras setup.cfg +# pip-compile --extra=async --extra=cli --extra=docs --extra=opt --extra=test --output-file=constraints.txt --strip-extras # -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx asttokens==2.4.1 # via stack-data -babel==2.14.0 +babel==2.16.0 # via sphinx -backcall==0.2.0 - # via ipython +backports-tarfile==1.2.0 + # via jaraco-context beautifulsoup4==4.12.3 # via furo -certifi==2024.2.2 +certifi==2024.7.4 # via requests -cffi==1.16.0 +cffi==1.17.0 # via cryptography charset-normalizer==3.3.2 # via requests -colorama==0.4.6 - # via - # ipython - # pytest - # sphinx -coverage==7.4.4 +coverage==7.6.1 # via pytest-cov -cryptography==42.0.5 +cryptography==43.0.0 # via # pyspnego # requests-kerberos decorator==5.1.1 - # via ipython + # via + # gssapi + # ipython defusedxml==0.7.1 - # via jira (setup.cfg) + # via jira (pyproject.toml) docutils==0.21.2 # via - # jira (setup.cfg) + # jira (pyproject.toml) # sphinx -exceptiongroup==1.2.0 - # via pytest -execnet==2.0.2 +exceptiongroup==1.2.2 + # via + # ipython + # pytest +execnet==2.1.1 # via # pytest-cache # pytest-xdist executing==2.0.1 # via stack-data filemagic==1.6 - # via jira (setup.cfg) + # via jira (pyproject.toml) flaky==3.8.1 - # via jira (setup.cfg) -furo==2024.1.29 - # via jira (setup.cfg) -idna==3.6 + # via jira (pyproject.toml) +furo==2024.8.6 + # via jira (pyproject.toml) +gssapi==1.8.3 + # via pyspnego +idna==3.8 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.1.0 +importlib-metadata==8.4.0 # via # keyring # sphinx -importlib-resources==6.4.0 - # via keyring iniconfig==2.0.0 # via pytest -ipython==8.12.3 - # via jira (setup.cfg) -jaraco-classes==3.3.1 +ipython==8.18.1 + # via jira (pyproject.toml) +jaraco-classes==3.4.0 # via keyring -jaraco-context==4.3.0 +jaraco-context==6.0.1 # via keyring -jaraco-functools==4.0.0 +jaraco-functools==4.0.2 # via keyring jedi==0.19.1 # via ipython -jinja2==3.1.3 +jinja2==3.1.4 # via sphinx -keyring==25.0.0 - # via jira (setup.cfg) +keyring==25.3.0 + # via jira (pyproject.toml) +krb5==0.6.0 + # via pyspnego markupsafe==2.1.5 # via # jinja2 - # jira (setup.cfg) -matplotlib-inline==0.1.6 + # jira (pyproject.toml) +matplotlib-inline==0.1.7 # via ipython -more-itertools==10.2.0 +more-itertools==10.4.0 # via # jaraco-classes # jaraco-functools oauthlib==3.2.2 # via - # jira (setup.cfg) + # jira (pyproject.toml) # requests-oauthlib -packaging==24.0 +packaging==24.1 # via - # jira (setup.cfg) + # jira (pyproject.toml) # pytest # pytest-sugar # sphinx parameterized==0.9.0 - # via jira (setup.cfg) -parso==0.8.3 + # via jira (pyproject.toml) +parso==0.8.4 # via jedi -pickleshare==0.7.5 +pexpect==4.9.0 # via ipython -pillow==10.2.0 - # via jira (setup.cfg) -pluggy==1.4.0 +pillow==10.4.0 + # via jira (pyproject.toml) +pluggy==1.5.0 # via pytest -prompt-toolkit==3.0.43 +prompt-toolkit==3.0.47 # via ipython -pure-eval==0.2.2 +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.3 # via stack-data -pycparser==2.21 +pycparser==2.22 # via cffi -pygments==2.17.2 +pygments==2.18.0 # via # furo # ipython # sphinx -pyjwt==2.8.0 +pyjwt==2.9.0 # via - # jira (setup.cfg) + # jira (pyproject.toml) # requests-jwt -pyspnego==0.10.2 +pyspnego==0.11.1 # via requests-kerberos -pytest==8.1.1 +pytest==8.3.2 # via - # jira (setup.cfg) + # jira (pyproject.toml) # pytest-cache # pytest-cov # pytest-instafail @@ -136,26 +139,22 @@ pytest==8.1.1 # pytest-timeout # pytest-xdist pytest-cache==1.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) pytest-cov==5.0.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) pytest-instafail==0.5.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) pytest-sugar==1.0.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) pytest-timeout==2.3.1 - # via jira (setup.cfg) -pytest-xdist==3.5.0 - # via jira (setup.cfg) -pytz==2024.1 - # via babel -pywin32-ctypes==0.2.2 - # via keyring -pyyaml==6.0.1 - # via jira (setup.cfg) -requests==2.31.0 - # via - # jira (setup.cfg) + # via jira (pyproject.toml) +pytest-xdist==3.6.1 + # via jira (pyproject.toml) +pyyaml==6.0.2 + # via jira (pyproject.toml) +requests==2.32.3 + # via + # jira (pyproject.toml) # requests-futures # requests-jwt # requests-kerberos @@ -165,78 +164,73 @@ requests==2.31.0 # requires-io # sphinx requests-futures==1.0.1 - # via jira (setup.cfg) + # via jira (pyproject.toml) requests-jwt==0.6.0 - # via jira (setup.cfg) -requests-kerberos==0.14.0 - # via jira (setup.cfg) -requests-mock==1.11.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) +requests-kerberos==0.15.0 + # via jira (pyproject.toml) +requests-mock==1.12.1 + # via jira (pyproject.toml) requests-oauthlib==2.0.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) requests-toolbelt==1.0.0 - # via jira (setup.cfg) + # via jira (pyproject.toml) requires-io==0.2.6 - # via jira (setup.cfg) + # via jira (pyproject.toml) six==1.16.0 - # via - # asttokens - # requests-mock + # via asttokens snowballstemmer==2.2.0 # via sphinx -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 -sphinx==7.1.2 +sphinx==7.4.7 # via # furo - # jira (setup.cfg) + # jira (pyproject.toml) # sphinx-basic-ng # sphinx-copybutton sphinx-basic-ng==1.0.0b2 # via furo sphinx-copybutton==0.5.2 - # via jira (setup.cfg) -sphinxcontrib-applehelp==1.0.4 + # via jira (pyproject.toml) +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sspilib==0.1.0 - # via pyspnego stack-data==0.6.3 # via ipython -tenacity==8.2.3 - # via jira (setup.cfg) +tenacity==9.0.0 + # via jira (pyproject.toml) termcolor==2.4.0 # via pytest-sugar tomli==2.0.1 # via # coverage # pytest -traitlets==5.14.2 + # sphinx +traitlets==5.14.3 # via # ipython # matplotlib-inline -typing-extensions==4.10.0 +typing-extensions==4.12.2 # via # ipython - # jira (setup.cfg) -urllib3==2.2.1 + # jira (pyproject.toml) +urllib3==2.2.2 # via requests wcwidth==0.2.13 # via prompt-toolkit -wheel==0.43.0 - # via jira (setup.cfg) +wheel==0.44.0 + # via jira (pyproject.toml) yanc==0.3.3 - # via jira (setup.cfg) -zipp==3.18.1 - # via - # importlib-metadata - # importlib-resources + # via jira (pyproject.toml) +zipp==3.20.0 + # via importlib-metadata diff --git a/docs/conf.py b/docs/conf.py index df10616e4..40a859a22 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,7 @@ ] intersphinx_mapping = { - "python": ("https://docs.python.org/3.8", None), + "python": ("https://docs.python.org/3.9", None), "requests": ("https://requests.readthedocs.io/en/latest/", None), "requests-oauthlib": ("https://requests-oauthlib.readthedocs.io/en/latest/", None), "ipython": ("https://ipython.readthedocs.io/en/stable/", None), diff --git a/docs/contributing.rst b/docs/contributing.rst index 0f8c39c23..0d6fcf96f 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -75,10 +75,10 @@ Using tox * Run tests - ``tox`` * Run tests for one env only - - ``tox -e py38`` + - ``tox -e py`` * Specify what tests to run with pytest_ - ``tox -e py39 -- tests/resources/test_attachment.py`` - - ``tox -e py38 -- -m allow_on_cloud`` (Run only the cloud tests) + - ``tox -e py310 -- -m allow_on_cloud`` (Run only the cloud tests) * Debug tests with breakpoints by disabling the coverage plugin, with the ``--no-cov`` argument. - Example for VSCode on Windows : diff --git a/docs/installation.rst b/docs/installation.rst index 723abda01..d8df76072 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -34,7 +34,7 @@ Source packages are also available at PyPI: Dependencies ============ -Python >=3.8 is required. +Python >=3.9 is required. - :py:mod:`requests` - `python-requests `_ library handles the HTTP business. Usually, the latest version available at time of release is the minimum version required; at this writing, that version is 1.2.0, but any version >= 1.0.0 should work. - :py:mod:`requests-oauthlib` - Used to implement OAuth. The latest version as of this writing is 1.3.0. diff --git a/jira/client.py b/jira/client.py index b090db913..851b7f5d8 100644 --- a/jira/client.py +++ b/jira/client.py @@ -22,16 +22,14 @@ import urllib import warnings from collections import OrderedDict -from collections.abc import Iterable -from functools import lru_cache, wraps +from collections.abc import Iterable, Iterator +from functools import cache, wraps from io import BufferedReader from numbers import Number from typing import ( Any, Callable, Generic, - Iterator, - List, Literal, SupportsIndex, TypeVar, @@ -1138,7 +1136,7 @@ def prepare( if not js or not isinstance(js, Iterable): raise JIRAError(f"Unable to parse JSON: {js}. Failed to add attachment?") jira_attachment = Attachment( - self._options, self._session, js[0] if isinstance(js, List) else js + self._options, self._session, js[0] if isinstance(js, list) else js ) if jira_attachment.size == 0: raise JIRAError( @@ -4772,7 +4770,7 @@ def _gain_sudo_session(self, options, destination): data=payload, ) - @lru_cache(maxsize=None) + @cache def templates(self) -> dict: url = self.server_url + "/rest/project-templates/latest/templates" @@ -4787,7 +4785,7 @@ def templates(self) -> dict: # pprint(templates.keys()) return templates - @lru_cache(maxsize=None) + @cache def permissionschemes(self): url = self._get_url("permissionscheme") @@ -4796,7 +4794,7 @@ def permissionschemes(self): return data["permissionSchemes"] - @lru_cache(maxsize=None) + @cache def issue_type_schemes(self) -> list[IssueTypeScheme]: """Get all issue type schemes defined (Admin required). @@ -4810,7 +4808,7 @@ def issue_type_schemes(self) -> list[IssueTypeScheme]: return data["schemes"] - @lru_cache(maxsize=None) + @cache def issuesecurityschemes(self): url = self._get_url("issuesecurityschemes") @@ -4819,7 +4817,7 @@ def issuesecurityschemes(self): return data["issueSecuritySchemes"] - @lru_cache(maxsize=None) + @cache def projectcategories(self): url = self._get_url("projectCategory") @@ -4828,7 +4826,7 @@ def projectcategories(self): return data - @lru_cache(maxsize=None) + @cache def avatars(self, entity="project"): url = self._get_url(f"avatar/{entity}/system") @@ -4837,7 +4835,7 @@ def avatars(self, entity="project"): return data["system"] - @lru_cache(maxsize=None) + @cache def notificationschemes(self): # TODO(ssbarnea): implement pagination support url = self._get_url("notificationscheme") @@ -4846,7 +4844,7 @@ def notificationschemes(self): data: dict[str, Any] = json_loads(r) return data["values"] - @lru_cache(maxsize=None) + @cache def screens(self): # TODO(ssbarnea): implement pagination support url = self._get_url("screens") @@ -4855,7 +4853,7 @@ def screens(self): data: dict[str, Any] = json_loads(r) return data["values"] - @lru_cache(maxsize=None) + @cache def workflowscheme(self): # TODO(ssbarnea): implement pagination support url = self._get_url("workflowschemes") @@ -4864,7 +4862,7 @@ def workflowscheme(self): data = json_loads(r) return data # ['values'] - @lru_cache(maxsize=None) + @cache def workflows(self): # TODO(ssbarnea): implement pagination support url = self._get_url("workflow") diff --git a/jira/resources.py b/jira/resources.py index 523a54324..e72b7f456 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -10,7 +10,7 @@ import logging import re import time -from typing import TYPE_CHECKING, Any, Dict, List, Type, cast +from typing import TYPE_CHECKING, Any, cast from requests import Response from requests.structures import CaseInsensitiveDict @@ -489,7 +489,7 @@ def __init__( Resource.__init__(self, "attachment/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def get(self): """Return the file content as a string.""" @@ -514,7 +514,7 @@ def __init__( Resource.__init__(self, "component/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def delete(self, moveIssuesTo: str | None = None): # type: ignore[override] """Delete this component from the server. @@ -541,7 +541,7 @@ def __init__( Resource.__init__(self, "customFieldOption/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Dashboard(Resource): @@ -557,7 +557,7 @@ def __init__( if raw: self._parse_raw(raw) self.gadgets: list[DashboardGadget] = [] - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class DashboardItemPropertyKey(Resource): @@ -572,7 +572,7 @@ def __init__( Resource.__init__(self, "dashboard/{0}/items/{1}/properties", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class DashboardItemProperty(Resource): @@ -589,7 +589,7 @@ def __init__( ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] # incompatible supertype ignored self, dashboard_id: str, item_id: str, value: dict[str, Any] @@ -648,7 +648,7 @@ def __init__( if raw: self._parse_raw(raw) self.item_properties: list[DashboardItemProperty] = [] - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] # incompatible supertype ignored self, @@ -723,7 +723,7 @@ def __init__( Resource.__init__(self, "field/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Filter(Resource): @@ -738,7 +738,7 @@ def __init__( Resource.__init__(self, "filter/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Issue(Resource): @@ -790,7 +790,7 @@ def __init__( self.key: str if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] # incompatible supertype ignored self, @@ -911,7 +911,7 @@ def __init__( Resource.__init__(self, "issue/{0}/comment/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] # The above ignore is added because we've added new parameters and order of @@ -967,7 +967,7 @@ def __init__( Resource.__init__(self, "issue/{0}/remotelink/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] self, @@ -1011,7 +1011,7 @@ def __init__( Resource.__init__(self, "issue/{0}/votes", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueTypeScheme(Resource): @@ -1021,7 +1021,7 @@ def __init__(self, options, session, raw=None): Resource.__init__(self, "issuetypescheme", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueSecurityLevelScheme(Resource): @@ -1033,7 +1033,7 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class NotificationScheme(Resource): @@ -1045,7 +1045,7 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class PermissionScheme(Resource): @@ -1057,7 +1057,7 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class PriorityScheme(Resource): @@ -1069,7 +1069,7 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class WorkflowScheme(Resource): @@ -1081,7 +1081,7 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Watchers(Resource): @@ -1096,7 +1096,7 @@ def __init__( Resource.__init__(self, "issue/{0}/watchers", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def delete(self, username): # type: ignore[override] """Remove the specified user from the watchers list.""" @@ -1114,7 +1114,7 @@ def __init__( self.remainingEstimate = None if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Worklog(Resource): @@ -1129,7 +1129,7 @@ def __init__( Resource.__init__(self, "issue/{0}/worklog/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def delete( # type: ignore[override] self, adjustEstimate: str | None = None, newEstimate=None, increaseBy=None @@ -1166,7 +1166,7 @@ def __init__( Resource.__init__(self, "issue/{0}/properties/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def _find_by_url( self, @@ -1190,7 +1190,7 @@ def __init__( Resource.__init__(self, "issueLink/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueLinkType(Resource): @@ -1205,7 +1205,7 @@ def __init__( Resource.__init__(self, "issueLinkType/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueType(Resource): @@ -1220,7 +1220,7 @@ def __init__( Resource.__init__(self, "issuetype/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Priority(Resource): @@ -1235,7 +1235,7 @@ def __init__( Resource.__init__(self, "priority/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Project(Resource): @@ -1250,7 +1250,7 @@ def __init__( Resource.__init__(self, "project/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Role(Resource): @@ -1265,7 +1265,7 @@ def __init__( Resource.__init__(self, "project/{0}/role/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] self, @@ -1325,7 +1325,7 @@ def __init__( Resource.__init__(self, "resolution/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class SecurityLevel(Resource): @@ -1340,7 +1340,7 @@ def __init__( Resource.__init__(self, "securitylevel/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Status(Resource): @@ -1355,7 +1355,7 @@ def __init__( Resource.__init__(self, "status/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class StatusCategory(Resource): @@ -1370,7 +1370,7 @@ def __init__( Resource.__init__(self, "statuscategory/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class User(Resource): @@ -1391,7 +1391,7 @@ def __init__( Resource.__init__(self, f"user?{_query_param}" + "={0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Group(Resource): @@ -1406,7 +1406,7 @@ def __init__( Resource.__init__(self, "group?groupname={0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Version(Resource): @@ -1421,7 +1421,7 @@ def __init__( Resource.__init__(self, "version/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def delete(self, moveFixIssuesTo=None, moveAffectedIssuesTo=None): """Delete this project version from the server. @@ -1493,7 +1493,7 @@ def __init__( Resource.__init__(self, path, options, session, self.AGILE_BASE_URL) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Sprint(AgileResource): @@ -1537,7 +1537,7 @@ def __init__( ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class ServiceDesk(Resource): @@ -1558,7 +1558,7 @@ def __init__( ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class RequestType(Resource): @@ -1580,7 +1580,7 @@ def __init__( if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) # Utilities @@ -1602,9 +1602,9 @@ def dict2resource( if isinstance(j, dict): if "self" in j: # to try and help mypy know that cls_for_resource can never be 'Resource' - resource_class = cast(Type[Resource], cls_for_resource(j["self"])) + resource_class = cast(type[Resource], cls_for_resource(j["self"])) resource = cast( - Type[Resource], + type[Resource], resource_class( # type: ignore options=options, session=session, @@ -1617,17 +1617,17 @@ def dict2resource( else: setattr(top, i, dict2resource(j, options=options, session=session)) elif isinstance(j, seqs): - j = cast(List[Dict[str, Any]], j) # help mypy + j = cast(list[dict[str, Any]], j) # help mypy seq_list: list[Any] = [] for seq_elem in j: if isinstance(seq_elem, dict): if "self" in seq_elem: # to try and help mypy know that cls_for_resource can never be 'Resource' resource_class = cast( - Type[Resource], cls_for_resource(seq_elem["self"]) + type[Resource], cls_for_resource(seq_elem["self"]) ) resource = cast( - Type[Resource], + type[Resource], resource_class( # type: ignore options=options, session=session, @@ -1700,7 +1700,7 @@ def __init__( Resource.__init__(self, "unknown{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def cls_for_resource(resource_literal: str) -> type[Resource]: diff --git a/pyproject.toml b/pyproject.toml index 2260e162d..8878969f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "jira" authors = [{name = "Ben Speakmon", email = "ben.speakmon@gmail.com"}] maintainers = [{name = "Sorin Sbarnea", email = "sorin.sbarnea@gmail.com"}] description = "Python library for interacting with JIRA via REST APIs." -requires-python = ">=3.8" +requires-python = ">=3.9" license = {text = "BSD-2-Clause"} classifiers = [ "Development Status :: 5 - Production/Stable", @@ -15,10 +15,10 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP", ] @@ -163,7 +163,7 @@ filterwarnings = ["ignore::pytest.PytestWarning"] markers = ["allow_on_cloud: opt in for the test to run on Jira Cloud"] [tool.mypy] -python_version = "3.8" +python_version = "3.9" warn_unused_configs = true namespace_packages = true check_untyped_defs = true @@ -177,8 +177,8 @@ disable_error_code = "annotation-unchecked" # Same as Black. line-length = 88 -# Assume Python 3.8. (minimum supported) -target-version = "py38" +# Assume Python 3.9 (minimum supported) +target-version = "py39" # The source code paths to consider, e.g., when resolving first- vs. third-party imports src = ["jira", "tests"] @@ -204,6 +204,8 @@ ignore = [ "D401", "D402", "D417", + "UP006", + "UP035", ] # Allow unused variables when underscore-prefixed. diff --git a/tests/resources/test_board.py b/tests/resources/test_board.py index 8393d43cf..183d84fe2 100644 --- a/tests/resources/test_board.py +++ b/tests/resources/test_board.py @@ -1,7 +1,7 @@ from __future__ import annotations +from collections.abc import Iterator from contextlib import contextmanager -from typing import Iterator from jira.resources import Board from tests.conftest import JiraTestCase, rndstr diff --git a/tests/resources/test_epic.py b/tests/resources/test_epic.py index e38e42c07..8d82c2b86 100644 --- a/tests/resources/test_epic.py +++ b/tests/resources/test_epic.py @@ -1,8 +1,8 @@ from __future__ import annotations +from collections.abc import Iterator from contextlib import contextmanager from functools import cached_property -from typing import Iterator from parameterized import parameterized diff --git a/tests/resources/test_sprint.py b/tests/resources/test_sprint.py index 83c0d43b1..2ccb14837 100644 --- a/tests/resources/test_sprint.py +++ b/tests/resources/test_sprint.py @@ -1,8 +1,8 @@ from __future__ import annotations +from collections.abc import Iterator from contextlib import contextmanager from functools import lru_cache -from typing import Iterator import pytest as pytest diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 191c6614b..088ff6dc1 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -116,9 +116,11 @@ def test_jira_error_log_to_tempfile_if_env_var_set(self): # WHEN: a JIRAError's __str__ method is called and # log details are expected to be sent to the tempfile - with patch.dict("os.environ", env_vars), patch( - f"{PATCH_BASE}.tempfile.mkstemp", autospec=True - ) as mock_mkstemp, patch(f"{PATCH_BASE}.open", mocked_open): + with ( + patch.dict("os.environ", env_vars), + patch(f"{PATCH_BASE}.tempfile.mkstemp", autospec=True) as mock_mkstemp, + patch(f"{PATCH_BASE}.open", mocked_open), + ): mock_mkstemp.return_value = 0, str(test_jira_error_filename) str(JIRAError(response=self.MockResponse(text=DUMMY_TEXT))) @@ -137,9 +139,11 @@ def test_jira_error_log_to_tempfile_not_used_if_env_var_not_set(self): mocked_open = mock_open() # WHEN: a JIRAError's __str__ method is called - with patch.dict("os.environ", env_vars), patch( - f"{PATCH_BASE}.tempfile.mkstemp", autospec=True - ) as mock_mkstemp, patch(f"{PATCH_BASE}.open", mocked_open): + with ( + patch.dict("os.environ", env_vars), + patch(f"{PATCH_BASE}.tempfile.mkstemp", autospec=True) as mock_mkstemp, + patch(f"{PATCH_BASE}.open", mocked_open), + ): mock_mkstemp.return_value = 0, str(test_jira_error_filename) str(JIRAError(response=self.MockResponse(text=DUMMY_TEXT))) diff --git a/tox.ini b/tox.ini index 8e7441699..e11430199 100644 --- a/tox.ini +++ b/tox.ini @@ -2,27 +2,25 @@ minversion = 4.0 isolated_build = True requires = - # plugins disabled until they gets compatible with tox v4 - # tox-extra - # tox-pyenv + tox-extra envlist = lint pkg docs + py312 py311 py310 py39 - py38 ignore_basepython_conflict = True skip_missing_interpreters = True skipdist = True [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] @@ -72,13 +70,13 @@ allowlist_externals = description = Update dependency lock files # Force it to use oldest supported version of python or we would lose ability # to get pinning correctly. -basepython = python3.8 +basepython = python3.9 skip_install = true deps = pip-tools >= 6.4.0 pre-commit >= 2.13.0 commands = - pip-compile --upgrade -o constraints.txt setup.cfg --extra cli --extra docs --extra opt --extra async --extra test --strip-extras + pip-compile --upgrade -o constraints.txt --extra cli --extra docs --extra opt --extra async --extra test --strip-extras {envpython} -m pre_commit autoupdate [testenv:docs]