diff --git a/jira/client.py b/jira/client.py index b23aff2de..a983e1f3d 100644 --- a/jira/client.py +++ b/jira/client.py @@ -32,8 +32,10 @@ Callable, Generic, Literal, + ParamSpec, SupportsIndex, TypeVar, + cast, no_type_check, overload, ) @@ -100,7 +102,7 @@ try: from requests_jwt import JWTAuth except ImportError: - pass + JWTAuth = None LOG = _logging.getLogger("jira") @@ -183,7 +185,11 @@ def is_experimental(*args, **kwargs): return is_experimental -def translate_resource_args(func: Callable): +P = ParamSpec("P") +R = TypeVar("R") + + +def translate_resource_args(func: Callable[P, R]) -> Callable[P, R]: """Decorator that converts Issue and Project resources to their keys when used as arguments. Args: @@ -191,8 +197,8 @@ def translate_resource_args(func: Callable): """ @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - arg_list = [] + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + arg_list: list[Any] = [] for arg in args: if isinstance(arg, Issue | Project): arg_list.append(arg.key) @@ -200,8 +206,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: arg_list.append(arg.name) else: arg_list.append(arg) - result = func(*arg_list, **kwargs) - return result + return cast(Callable[..., R], func)(*arg_list, **kwargs) return wrapper @@ -214,13 +219,13 @@ def _field_worker( return {"fields": fieldargs} -ResourceType = TypeVar("ResourceType", contravariant=True, bound=Resource) +ResourceType = TypeVar("ResourceType", bound=Resource) -class ResultList(list, Generic[ResourceType]): +class ResultList(list[ResourceType], Generic[ResourceType]): def __init__( self, - iterable: Iterable | None = None, + iterable: Iterable[ResourceType] | None = None, _startAt: int = 0, _maxResults: int = 0, _total: int | None = None, @@ -306,8 +311,7 @@ def _generate_qsh(self, req): return qsh def _sort_and_quote_values(self, values): - ordered_values = sorted(values) - return [quote(value, safe="~") for value in ordered_values] + return [quote(value, safe="~") for value in sorted(values)] class JiraCookieAuth(AuthBase): @@ -464,6 +468,8 @@ class JIRA: JIRA_BASE_URL = Resource.JIRA_BASE_URL AGILE_BASE_URL = AgileResource.AGILE_BASE_URL + _session_obj: ResilientSession | None + def __init__( self, server: str | None = None, @@ -611,21 +617,21 @@ def __init__( assert isinstance(self._options["headers"], dict) # for mypy benefit # Create Session object and update with config options first - self._session = ResilientSession(timeout=timeout) + self._session_obj = ResilientSession(timeout=timeout) # Add the client authentication certificate to the request if configured self._add_client_cert_to_session() # Add the SSL Cert to the request if configured self._add_ssl_cert_verif_strategy_to_session() - self._session.headers.update(self._options["headers"]) + self._session_obj.headers.update(self._options["headers"]) if "cookies" in self._options: - self._session.cookies.update(self._options["cookies"]) + self._session_obj.cookies.update(self._options["cookies"]) - self._session.max_retries = max_retries + self._session_obj.max_retries = max_retries if proxies: - self._session.proxies = proxies + self._session_obj.proxies = proxies # Setup the Auth last, # so that if any handlers take a copy of the session obj it will be ready @@ -737,17 +743,15 @@ def __del__(self): self.close() def close(self): - session = getattr(self, "_session", None) - if session is not None: + if self._session_obj: try: - session.close() + self._session_obj.close() except TypeError: # TypeError: "'NoneType' object is not callable" could still happen here # because other references are also in the process to be torn down, # see warning section in https://docs.python.org/2/reference/datamodel.html#object.__del__ pass - # TODO: https://github.com/pycontribs/jira/issues/1881 - self._session = None # type: ignore[arg-type,assignment] + self._session_obj = None def _check_for_html_error(self, content: str): # Jira has the bad habit of returning errors in pages with 200 and embedding the @@ -809,6 +813,7 @@ def json_params() -> dict[str, Any]: page_params = json_params() + batch_size = None if startAt: page_params["startAt"] = startAt if maxResults: @@ -909,7 +914,7 @@ def json_params() -> dict[str, Any]: else: # TODO: unreachable # it seems that search_users can return a list() containing a single user! return ResultList( - [item_type(self._options, self._session, resource)], 0, 1, 1, True + [item_type(self._options, self._session_obj, resource)], 0, 1, 1, True ) @cloud_api @@ -1060,6 +1065,12 @@ def application_properties( params["key"] = key return self._get_json("application-properties", params=params) + @property + def _session(self) -> ResilientSession: + if self._session_obj is None: + raise JIRAError("JIRA instance has been closed and can no longer be used.") + return self._session_obj + def set_application_property(self, key: str, value: str): """Set the application property. @@ -2552,8 +2563,7 @@ def add_remote_link( url = self._get_url("issue/" + str(issue) + "/remotelink") r = self._session.post(url, data=json.dumps(data)) - remote_link = RemoteLink(self._options, self._session, raw=json_loads(r)) - return remote_link + return RemoteLink(self._options, self._session, raw=json_loads(r)) def add_simple_link(self, issue: str, object: dict[str, Any]): """Add a simple remote link from an issue to web resource. @@ -2577,8 +2587,7 @@ def add_simple_link(self, issue: str, object: dict[str, Any]): url = self._get_url("issue/" + str(issue) + "/remotelink") r = self._session.post(url, data=json.dumps(data)) - simple_link = RemoteLink(self._options, self._session, raw=json_loads(r)) - return simple_link + return RemoteLink(self._options, self._session, raw=json_loads(r)) # non-resource @translate_resource_args @@ -4006,10 +4015,8 @@ def search_assignable_users_for_issues( "Either 'username' or 'query' arguments must be specified." ) - if username is not None: - params = {"username": username} - if query is not None: - params = {"query": query} + params: dict[str, Any] = {"query": query} if query else {"username": username} + if project is not None: params["project"] = project if issueKey is not None: @@ -4170,7 +4177,8 @@ def delete_remote_link( if internal_id is not None: url = self._get_url(f"issue/{issue}/remotelink/{internal_id}") - elif global_id is not None: + else: + assert global_id is not None # stop "&" and other special characters in global_id from messing around with the query global_id = urllib.parse.quote(global_id, safe="") url = self._get_url(f"issue/{issue}/remotelink?globalId={global_id}") @@ -4495,11 +4503,10 @@ def _timestamp(dt: datetime.timedelta | None = None): return calendar.timegm(t.timetuple()) def _create_jwt_session(self, jwt: dict[str, Any]): - try: - jwt_auth = JWTAuth(jwt["secret"], alg="HS256") - except NameError as e: - self.log.error("JWT authentication requires requests_jwt") - raise e + if JWTAuth is None: + raise JIRAError("JWT authentication requires requests_jwt") + + jwt_auth = JWTAuth(jwt["secret"], alg="HS256") jwt_auth.set_header_format("JWT %s") jwt_auth.add_field("iat", lambda req: JIRA._timestamp()) @@ -5505,7 +5512,7 @@ def boards( @translate_resource_args def sprints( self, - board_id: int, + board_id: int | str, extended: bool | None = None, startAt: int = 0, maxResults: int = 50, @@ -5543,7 +5550,7 @@ def sprints( def sprints_by_name( self, id: str | int, extended: bool = False, state: str | None = None - ) -> dict[str, dict[str, Any]]: + ) -> dict[str, dict[str, Any] | None]: """Get a dictionary of sprint Resources where the name of the sprint is the key. Args: @@ -5651,6 +5658,7 @@ def sprint_info(self, board_id: str, sprint_id: str) -> dict[str, Any]: """ sprint = Sprint(self._options, self._session) sprint.find(sprint_id) + assert sprint.raw is not None, "sprint.raw is None but should be set by sprint.find()" return sprint.raw def sprint(self, id: int) -> Sprint: @@ -5836,7 +5844,8 @@ def rank( if next_issue is not None: before_or_after = "Before" other_issue = next_issue - elif prev_issue is not None: + else: + assert prev_issue is not None before_or_after = "After" other_issue = prev_issue diff --git a/jira/resources.py b/jira/resources.py index 7127e2402..1d28db796 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -15,7 +15,9 @@ from requests import Response from requests.structures import CaseInsensitiveDict +from typing_extensions import override +from jira.exceptions import JIRAError from jira.resilientsession import ResilientSession, parse_errors from jira.utils import json_loads, remove_empty_attributes, threaded_requests @@ -129,7 +131,7 @@ class Resource: def __init__( self, resource: str, - options: dict[str, Any], + options: dict[str, Any] | None, session: ResilientSession, base_url: str = JIRA_BASE_URL, ): @@ -143,6 +145,8 @@ def __init__( """ self._resource = resource + if options is None: + options = {} self._options = options self._session = session self._base_url = base_url @@ -508,10 +512,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "attachment/{0}", options, session) + super().__init__("attachment/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def get(self): """Return the file content as a string.""" @@ -533,10 +536,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "component/{0}", options, session) + super().__init__("component/{0}", options, session) if raw: self._parse_raw(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. @@ -560,10 +562,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "customFieldOption/{0}", options, session) + super().__init__("customFieldOption/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Dashboard(Resource): @@ -575,11 +576,10 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "dashboard/{0}", options, session) + super().__init__("dashboard/{0}", options, session) if raw: self._parse_raw(raw) self.gadgets: list[DashboardGadget] = [] - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class DashboardItemPropertyKey(Resource): @@ -591,10 +591,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "dashboard/{0}/items/{1}/properties", options, session) + super().__init__("dashboard/{0}/items/{1}/properties", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class DashboardItemProperty(Resource): @@ -611,7 +610,6 @@ def __init__( ) if raw: self._parse_raw(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] @@ -633,6 +631,8 @@ def update( # type: ignore[override] # incompatible supertype ignored options["path"] = ( f"dashboard/{dashboard_id}/items/{item_id}/properties/{self.key}" ) + if not self.raw: + self.raw = {"value": {}} self.raw["value"].update(value) self._session.put(self.JIRA_BASE_URL.format(**options), self.raw["value"]) @@ -666,11 +666,10 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "dashboard/{0}/gadget/{1}", options, session) + super().__init__("dashboard/{0}/gadget/{1}", options, session) if raw: self._parse_raw(raw) self.item_properties: list[DashboardItemProperty] = [] - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] # incompatible supertype ignored self, @@ -742,10 +741,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "field/{0}", options, session) + super().__init__("field/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Filter(Resource): @@ -757,10 +755,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "filter/{0}", options, session) + super().__init__("filter/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Issue(Resource): @@ -805,14 +802,13 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}", options, session) + super().__init__("issue/{0}", options, session) self.fields: Issue._IssueFields self.id: str self.key: str if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] # incompatible supertype ignored self, @@ -930,10 +926,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/comment/{1}", options, session) + super().__init__("issue/{0}/comment/{1}", options, session) if raw: self._parse_raw(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 @@ -986,10 +981,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/pinned-comments", options, session) + super().__init__("issue/{0}/pinned-comments", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class RemoteLink(Resource): @@ -1001,10 +995,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/remotelink/{1}", options, session) + super().__init__("issue/{0}/remotelink/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] self, @@ -1045,20 +1038,18 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/votes", options, session) + super().__init__("issue/{0}/votes", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueTypeScheme(Resource): """An issue type scheme.""" def __init__(self, options, session, raw=None): - Resource.__init__(self, "issuetypescheme", options, session) + super().__init__("issuetypescheme", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueSecurityLevelScheme(Resource): @@ -1070,7 +1061,6 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class NotificationScheme(Resource): @@ -1082,7 +1072,6 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class PermissionScheme(Resource): @@ -1094,7 +1083,6 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class PriorityScheme(Resource): @@ -1106,7 +1094,6 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class WorkflowScheme(Resource): @@ -1118,7 +1105,6 @@ def __init__(self, options, session, raw=None): ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Watchers(Resource): @@ -1130,10 +1116,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/watchers", options, session) + super().__init__("issue/{0}/watchers", options, session) if raw: self._parse_raw(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.""" @@ -1143,15 +1128,14 @@ def delete(self, username): # type: ignore[override] class TimeTracking(Resource): def __init__( self, - options: dict[str, str], + options: dict[str, Any] | None, session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/worklog/{1}", options, session) + super().__init__("issue/{0}/worklog/{1}", options, session) self.remainingEstimate = None if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Worklog(Resource): @@ -1163,10 +1147,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/worklog/{1}", options, session) + super().__init__("issue/{0}/worklog/{1}", options, session) if raw: self._parse_raw(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 @@ -1200,11 +1183,11 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issue/{0}/properties/{1}", options, session) + super().__init__("issue/{0}/properties/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) + @override def _find_by_url( self, url: str, @@ -1224,10 +1207,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issueLink/{0}", options, session) + super().__init__("issueLink/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueLinkType(Resource): @@ -1239,10 +1221,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issueLinkType/{0}", options, session) + super().__init__("issueLinkType/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class IssueType(Resource): @@ -1254,10 +1235,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "issuetype/{0}", options, session) + super().__init__("issuetype/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Priority(Resource): @@ -1269,10 +1249,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "priority/{0}", options, session) + super().__init__("priority/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Project(Resource): @@ -1284,10 +1263,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "project/{0}", options, session) + super().__init__("project/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Role(Resource): @@ -1299,10 +1277,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "project/{0}/role/{1}", options, session) + super().__init__("project/{0}/role/{1}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) def update( # type: ignore[override] self, @@ -1320,15 +1297,13 @@ def update( # type: ignore[override] if groups is not None and isinstance(groups, str): groups = (groups,) - data = { - "id": self.id, - "categorisedActors": { + super().update( + ids=self.id, + categorisedActors={ "atlassian-user-role-actor": users, "atlassian-group-role-actor": groups, }, - } - - super().update(**data) + ) def add_user( self, @@ -1359,10 +1334,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "resolution/{0}", options, session) + super().__init__("resolution/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class SecurityLevel(Resource): @@ -1374,10 +1348,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "securitylevel/{0}", options, session) + super().__init__("securitylevel/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Status(Resource): @@ -1389,10 +1362,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "status/{0}", options, session) + super().__init__("status/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class StatusCategory(Resource): @@ -1404,10 +1376,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "statuscategory/{0}", options, session) + super().__init__("statuscategory/{0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class User(Resource): @@ -1425,10 +1396,9 @@ def __init__( if raw and "accountId" in raw["self"]: _query_param = "accountId" - Resource.__init__(self, f"user?{_query_param}" + "={0}", options, session) + super().__init__(f"user?{_query_param}" + "={0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Group(Resource): @@ -1440,10 +1410,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "group?groupname={0}", options, session) + super().__init__("group?groupname={0}", options, session) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Version(Resource): @@ -1455,10 +1424,9 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__(self, "version/{0}", options, session) + super().__init__("version/{0}", options, session) if raw: self._parse_raw(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. @@ -1478,7 +1446,7 @@ def delete(self, moveFixIssuesTo=None, moveAffectedIssuesTo=None): return super().delete(params) # TODO: https://github.com/pycontribs/jira/issues/1881 - def update(self, **kwargs): # type: ignore[override] + def update(self, **kwargs: Any) -> None: # type: ignore[override] """Update this project version from the server. It is prior used to archive versions. Refer to Atlassian REST API `documentation`_. @@ -1527,10 +1495,9 @@ def __init__( ): self.self = "" - Resource.__init__(self, path, options, session, self.AGILE_BASE_URL) + super().__init__(path, options, session, self.AGILE_BASE_URL) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class Sprint(AgileResource): @@ -1542,7 +1509,7 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - AgileResource.__init__(self, "sprint/{0}", options, session, raw) + super().__init__("sprint/{0}", options, session, raw) class Board(AgileResource): @@ -1554,7 +1521,7 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - AgileResource.__init__(self, "board/{id}", options, session, raw) + super().__init__("board/{id}", options, session, raw) # Service Desk @@ -1569,12 +1536,11 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__( - self, "customer", options, session, "{server}/rest/servicedeskapi/{path}" + super().__init__( + "customer", options, session, "{server}/rest/servicedeskapi/{path}" ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class ServiceDesk(Resource): @@ -1586,8 +1552,7 @@ def __init__( session: ResilientSession, raw: dict[str, Any] | None = None, ): - Resource.__init__( - self, + super().__init__( "servicedesk/{0}", options, session, @@ -1595,7 +1560,6 @@ def __init__( ) if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) class RequestType(Resource): @@ -1617,14 +1581,16 @@ def __init__( if raw: self._parse_raw(raw) - self.raw: dict[str, Any] = cast(dict[str, Any], self.raw) # Utilities def dict2resource( - raw: dict[str, Any], top=None, options=None, session=None + raw: dict[str, Any], + top=None, + options: dict[str, Any] | None = None, + session: ResilientSession | None = None, ) -> PropertyHolder | type[Resource]: """Convert a dictionary into a Jira Resource object. @@ -1634,12 +1600,13 @@ def dict2resource( if top is None: top = PropertyHolder() - seqs = tuple, list, set, frozenset for i, j in raw.items(): 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"])) + if session is None: + raise JIRAError(f"Session is required for resource {j['self']}") resource = cast( type[Resource], resource_class( # type: ignore @@ -1650,11 +1617,12 @@ def dict2resource( ) setattr(top, i, resource) elif i == "timetracking": + if session is None: + raise JIRAError("Session is required to parse timetracking") setattr(top, "timetracking", TimeTracking(options, session, j)) else: setattr(top, i, dict2resource(j, options=options, session=session)) - elif isinstance(j, seqs): - j = cast(list[dict[str, Any]], j) # help mypy + elif isinstance(j, tuple | list | set | frozenset): seq_list: list[Any] = [] for seq_elem in j: if isinstance(seq_elem, dict): @@ -1663,6 +1631,8 @@ def dict2resource( resource_class = cast( type[Resource], cls_for_resource(seq_elem["self"]) ) + if session is None: + raise JIRAError(f"Session is required for resource {seq_elem['self']}") resource = cast( type[Resource], resource_class( # type: ignore @@ -1738,7 +1708,6 @@ 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) def cls_for_resource(resource_literal: str) -> type[Resource]: diff --git a/tests/tests.py b/tests/tests.py index 4a4217d00..0cbae496d 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -414,7 +414,7 @@ def test_fetch_pages( mock_session.request.return_value = responses mock_session.get.return_value = responses self.jira._session.close() - self.jira._session = mock_session + self.jira._session = mock_session # type: ignore items = self.jira._fetch_pages( Issue, "issues", "search", start_at, max_results, params=params )