Skip to content

Commit

Permalink
Merge pull request #197 from eggplants/typing
Browse files Browse the repository at this point in the history
Typing
  • Loading branch information
nicholascar authored Feb 21, 2022
2 parents 38c939d + 8cc80aa commit 3f12d4b
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 295 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ jobs:
- name: Install dependencies
run: |
pip install -U pip
pip install -e '.[dev]'
pip install '.[dev]'
- name: Mypy
run: |
mypy --show-error-context --show-error-codes
- name: Test [wrapper]
if: ${{ always() }}
run: |
python -m unittest test/test_wrapper.py -v
- name: Test [all]
Expand Down
18 changes: 8 additions & 10 deletions SPARQLWrapper/KeyCaseInsensitiveDict.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,45 @@

"""
A simple implementation of a key case-insensitive dictionary.
..
Developers involved:
* Ivan Herman <http://www.ivan-herman.net>
* Sergio Fernández <http://www.wikier.org>
* Carlos Tejo Alonso <http://www.dayures.net>
* Alexey Zakhlestin <https://indeyets.ru/>
Organizations involved:
* `World Wide Web Consortium <http://www.w3.org>`_
* `Foundation CTIC <http://www.fundacionctic.org/>`_
:license: `W3C® Software notice and license <http://www.w3.org/Consortium/Legal/copyright-software>`_
"""

from typing import Dict, Mapping, TypeVar

_V = TypeVar("_V")

class KeyCaseInsensitiveDict(dict):
class KeyCaseInsensitiveDict(Dict[str, _V]):
"""
A simple implementation of a key case-insensitive dictionary
"""

def __init__(self, d={}):
def __init__(self, d: Mapping[str, _V]={}) -> None:
"""
:param dict d: The source dictionary.
"""
for k, v in d.items():
self[k] = v

def __setitem__(self, key, value):
def __setitem__(self, key: str, value: _V) -> None:
if hasattr(key, "lower"):
key = key.lower()
dict.__setitem__(self, key, value)

def __getitem__(self, key):
def __getitem__(self, key: str) -> _V:
if hasattr(key, "lower"):
key = key.lower()
return dict.__getitem__(self, key)

def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
if hasattr(key, "lower"):
key = key.lower()
dict.__delitem__(self, key)
10 changes: 5 additions & 5 deletions SPARQLWrapper/SPARQLExceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"""

from typing import Optional


class SPARQLWrapperException(Exception):
"""
Expand All @@ -28,12 +30,12 @@ class SPARQLWrapperException(Exception):

msg = "an exception has occurred"

def __init__(self, response=None):
def __init__(self, response: Optional[bytes] = None):
"""
:param string response: The server response
"""
if response:
formatted_msg = "%s: %s. \n\nResponse:\n%s" % (
formatted_msg = "%s: %s. \n\nResponse:\n%r" % (
self.__class__.__name__,
self.msg,
response,
Expand Down Expand Up @@ -65,9 +67,7 @@ class EndPointNotFound(SPARQLWrapperException):
End Point Not Found exception. Usually HTTP response status code ``404``.
"""

msg = (
"It was not possible to connect to the given endpoint: check it is correct"
)
msg = "It was not possible to connect to the given endpoint: check it is correct"


class Unauthorized(SPARQLWrapperException):
Expand Down
100 changes: 61 additions & 39 deletions SPARQLWrapper/SmartWrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
:requires: `RDFLib <https://rdflib.readthedocs.io>`_ package.
"""

import urllib.request, urllib.error, urllib.parse
from types import *
import SPARQLWrapper
from SPARQLWrapper.Wrapper import JSON, SELECT

from typing import Any, Dict, List, Optional, Tuple, Union

from SPARQLWrapper.Wrapper import JSON, SELECT, QueryResult
from SPARQLWrapper.Wrapper import SPARQLWrapper as SW

######################################################################################

Expand Down Expand Up @@ -53,7 +54,7 @@ class Value(object):
BNODE = "bnode"
"""the string denoting a blank node variable."""

def __init__(self, variable, binding):
def __init__(self, variable: str, binding: Dict[str, str]) -> None:
"""
:param variable: the variable for that binding. Stored for an easier reference.
:type variable: string
Expand All @@ -75,7 +76,7 @@ def __init__(self, variable, binding):
except:
pass

def __repr__(self):
def __repr__(self) -> str:
cls = self.__class__.__name__
return "%s(%s:%r)" % (cls, self.type, self.value)

Expand Down Expand Up @@ -110,26 +111,27 @@ class Bindings(object):
:vartype askResult: bool
"""

def __init__(self, retval):
def __init__(self, retval: QueryResult):
"""
:param retval: the query result.
:type retval: :class:`QueryResult<SPARQLWrapper.Wrapper.QueryResult>`
"""
self.fullResult = retval._convertJSON()
self.head = self.fullResult["head"]
self.variables = None
self.variables: Optional[List[str]] = None
try:
self.variables = self.fullResult["head"]["vars"]
except:
pass

self.bindings = []
self.bindings: List[Dict[str, Value]] = []
try:
for b in self.fullResult["results"]["bindings"]:
# This is a single binding. It is a dictionary per variable; each value is a dictionary again
# that has to be converted into a Value instance
newBind = {}
for key in self.variables:
# type error: Item "None" of "Union[List[str], Any, None]" has no attribute "__iter__" (not iterable)
for key in self.variables: # type: ignore [union-attr]
if key in b:
# there is a real binding for this key
newBind[key] = Value(key, b[key])
Expand All @@ -143,7 +145,7 @@ def __init__(self, retval):
except:
pass

def getValues(self, key):
def getValues(self, key: str) -> Optional[List[Value]]:
"""A shorthand for the retrieval of all bindings for a single key. It is
equivalent to ``[b[key] for b in self[key]]``
Expand All @@ -157,7 +159,7 @@ def getValues(self, key):
except:
return []

def __contains__(self, key):
def __contains__(self, key: Union[str, List[str], Tuple[str]]) -> bool:
"""Emulation of the "``key in obj``" operator. Key can be a string for a variable or an array/tuple
of strings.
Expand All @@ -173,7 +175,8 @@ def __contains__(self, key):
return False
if type(key) is list or type(key) is tuple:
# check first whether they are all really variables
if False in [k in self.variables for k in key]:
# type error: Unsupported right operand type for in ("Optional[List[str]]")
if False in [k in self.variables for k in key]: # type: ignore [operator]
return False
for b in self.bindings:
# try to find a binding where all key elements are present
Expand All @@ -185,14 +188,15 @@ def __contains__(self, key):
return True
return False
else:
if key not in self.variables:
# type error: Unsupported right operand type for in ("Optional[List[str]]")
if key not in self.variables: # type: ignore [operator]
return False
for b in self.bindings:
if key in b:
return True
return False

def __getitem__(self, key):
def __getitem__(self, key: Union[slice, str, List[str]]) -> List[Dict[str, Value]]:
"""Emulation of the ``obj[key]`` operator. Slice notation is also available.
The goal is to choose the right bindings among the available ones. The return values are always
arrays of bindings, ie, arrays of dictionaries mapping variable keys to :class:`Value` instances.
Expand All @@ -212,16 +216,27 @@ def __getitem__(self, key):
:rtype: array of variable -> :class:`Value` dictionaries
"""

def _checkKeys(keys):
def _checkKeys(keys: Union[List[Any], Tuple[Any, ...]]) -> bool:
if len(keys) == 0:
return False
for k in keys:
if not isinstance(k, str) or k not in self.variables:
# type error: Unsupported right operand type for in ("Optional[List[str]]")
if (
not isinstance(k, str)
or k not in self.variables # type: ignore [operator]
):
return False
return True

def _nonSliceCase(key):
if isinstance(key, str) and key != "" and key in self.variables:
def _nonSliceCase(
key: Union[
str,
List[Any],
Tuple[Any],
]
) -> Union[List[Any], bool, Tuple[Any]]:
# type error: Unsupported right operand type for in ("Optional[List[str]]")
if isinstance(key, str) and key != "" and key in self.variables: # type: ignore[operator]
# unicode or string:
return [key]
elif type(key) is list or type(key) is tuple:
Expand All @@ -230,8 +245,8 @@ def _nonSliceCase(key):
return False

# The arguments should be reduced to arrays of variables, ie, unicode strings
yes_keys = []
no_keys = []
yes_keys: Union[List[Any], bool, Tuple[Any]] = []
no_keys: Union[List[Any], bool, Tuple[Any]] = []
if type(key) is slice:
# Note: None for start or stop is all right
if key.start:
Expand All @@ -246,12 +261,14 @@ def _nonSliceCase(key):
yes_keys = _nonSliceCase(key)

# got it right, now get the right binding line with the constraints
retval = []
retval: List[Dict[str, Value]] = []
for b in self.bindings:
# first check whether the 'yes' part is all there:
if False in [k in b for k in yes_keys]:
# type error: Item "bool" of "Union[List[Any], bool, Tuple[Any]]" has no attribute "__iter__" (not iterable)
if False in [k in b for k in yes_keys]: # type: ignore[union-attr]
continue
if True in [k in b for k in no_keys]:
# type error: Item "bool" of "Union[List[Any], bool, Tuple[Any]]" has no attribute "__iter__" (not iterable)
if True in [k in b for k in no_keys]: # type: ignore[union-attr]
continue
# if we got that far, we should be all right!
retval.append(b)
Expand All @@ -260,26 +277,29 @@ def _nonSliceCase(key):
raise IndexError
return retval

def convert(self):
def convert(self) -> "Bindings":
"""This is just a convenience method, returns ``self``.
Although :class:`SPARQLWrapper2.Bindings` is not a subclass of :class:`SPARQLWrapper.QueryResult<SPARQLWrapper.Wrapper.QueryResult>`, it is returned as a result by
Although :class:`SPARQLWrapper2.Bindings` is not a subclass of
:class:`SPARQLWrapper.QueryResult<SPARQLWrapper.Wrapper.QueryResult>`, it is returned as a result by
:func:`SPARQLWrapper2.query`, just like :class:`QueryResult<SPARQLWrapper.Wrapper.QueryResult>` is returned by
:func:`SPARQLWrapper.query()<SPARQLWrapper.Wrapper.SPARQLWrapper.query>`. Consequently,
having an empty :func:`convert` method to imitate :class:`QueryResult's convert() method<SPARQLWrapper.Wrapper.QueryResult.convert>` may avoid unnecessary problems.
having an empty :func:`convert` method to imitate
:class:`QueryResult's convert() method<SPARQLWrapper.Wrapper.QueryResult.convert>`
may avoid unnecessary problems.
"""
return self


##############################################################################################################


class SPARQLWrapper2(SPARQLWrapper.SPARQLWrapper):
class SPARQLWrapper2(SW):
"""Subclass of :class:`~SPARQLWrapper.Wrapper.SPARQLWrapper` that works with a JSON SELECT return result only. The
query result is automatically set to a :class:`Bindings` instance. Makes the average query processing a bit
simpler..."""

def __init__(self, baseURI, defaultGraph=None):
def __init__(self, baseURI: str, defaultGraph: Optional[str] = None):
"""
Class encapsulating a full SPARQL call. In contrast to the :class:`~SPARQLWrapper.Wrapper.SPARQLWrapper`
superclass, the return format cannot be set (it is defaulted to
Expand All @@ -294,7 +314,7 @@ def __init__(self, baseURI, defaultGraph=None):
baseURI, returnFormat=JSON, defaultGraph=defaultGraph
)

def setReturnFormat(self, format):
def setReturnFormat(self, format: Optional[str]) -> None:
"""
Set the return format (:meth:`overriding the inherited method
<SPARQLWrapper.Wrapper.SPARQLWrapper.setReturnFormat>`).
Expand All @@ -310,18 +330,18 @@ def setReturnFormat(self, format):
"""
pass

def query(self):
def query(self) -> Union[Bindings, QueryResult]: # type: ignore[override]
"""
Execute the query and do an automatic conversion.
Execute the query and do an automatic conversion.
Exceptions can be raised if either the URI is wrong or the HTTP sends back an error.
The usual urllib2 exceptions are raised, which cover possible SPARQL errors, too.
Exceptions can be raised if either the URI is wrong or the HTTP sends back an error.
The usual urllib2 exceptions are raised, which cover possible SPARQL errors, too.
If the query type is *not* SELECT, the method falls back to the
:meth:`corresponding method in the superclass<SPARQLWrapper.Wrapper.SPARQLWrapper.query>`.
If the query type is *not* SELECT, the method falls back to the
:meth:`corresponding method in the superclass<SPARQLWrapper.Wrapper.SPARQLWrapper.query>`.
:return: query result
:rtype: :class:`Bindings` instance
:return: query result
:rtype: :class:`Bindings` instance
"""
res = super(SPARQLWrapper2, self).query()

Expand All @@ -330,7 +350,9 @@ def query(self):
else:
return res

def queryAndConvert(self):
def queryAndConvert( # type: ignore[override]
self,
) -> Union[Union[Bindings, QueryResult], QueryResult.ConvertResult]:
"""This is here to override the inherited method; it is equivalent to :class:`query`.
If the query type is *not* SELECT, the method falls back to the
Expand Down
Loading

0 comments on commit 3f12d4b

Please sign in to comment.