Skip to content

Commit 1f16f36

Browse files
committed
Add type hints and py.typed marker (PEP 561)
Annotate the whole codebase (client, main GrobidClient, the TEI2LossyJSON and TEI2Markdown converters, the CLIs and the validator) and ship a py.typed marker so type checkers pick up the inline hints. Each module uses 'from __future__ import annotations' so annotations stay lazy and cannot affect runtime behavior (and remain valid on Python 3.8). Packaging: - add grobid_client/py.typed and expose it via package-data and MANIFEST.in - include the grobid_client.format subpackage in the distribution - add a [tool.mypy] section (ignore missing third-party stubs; relax the BeautifulSoup-heavy converter modules whose dynamic API yields false positives) CI: - install mypy and run 'mypy grobid_client' in the build workflow (on 3.11) mypy runs clean on the package and the existing test-suite still passes. Closes #71
1 parent 07e4b48 commit 1f16f36

13 files changed

Lines changed: 244 additions & 171 deletions

.github/workflows/ci-build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@ jobs:
3030
- name: Install dependencies
3131
run: |
3232
python -m pip install --upgrade pip
33-
pip install --upgrade flake8 pytest pycodestyle pytest-cov pytest-mock
33+
pip install --upgrade flake8 pytest pycodestyle pytest-cov pytest-mock "mypy==2.3.*"
3434
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
3535
- name: Lint with flake8
3636
run: |
3737
# stop the build if there are Python syntax errors or undefined names
3838
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
3939
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
4040
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
41+
- name: Type check with mypy
42+
if: matrix.python-version == '3.11'
43+
run: |
44+
mypy grobid_client
4145
- name: Test with pytest
4246
run: |
4347
pytest tests/ -v --cov=grobid_client --cov-report=xml --cov-report=term-missing

MANIFEST.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
include Readme.md
1+
include Readme.md
2+
include grobid_client/py.typed

Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ concurrent processing capabilities for PDF documents, reference strings, and pat
3333
- **Sentence Segmentation**: Layout-aware sentence segmentation capabilities
3434
- **JSON Output**: Convert TEI XML output to structured JSON format with CORD-19-like structure
3535
- **Markdown Output**: Convert TEI XML output to clean Markdown format with structured sections
36+
- **Type Hints**: Ships inline type annotations and a `py.typed` marker (PEP 561) for static type checking
3637

3738
## 📋 Prerequisites
3839

grobid_client/client.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
""" Generic API Client """
2+
from __future__ import annotations
3+
24
from copy import deepcopy
35
import json
6+
from typing import Any, Optional, Tuple
7+
48
import requests
59

610
try:
@@ -16,12 +20,17 @@ class ApiClient(object):
1620
service methods, i.e. ``get``, ``post``, ``put`` and ``delete``.
1721
"""
1822

19-
accept_type = "application/xml"
20-
api_base = None
23+
accept_type: str = "application/xml"
24+
api_base: Optional[str] = None
2125

2226
def __init__(
23-
self, base_url, username=None, api_key=None, status_endpoint=None, timeout=60
24-
):
27+
self,
28+
base_url: str,
29+
username: Optional[str] = None,
30+
api_key: Optional[str] = None,
31+
status_endpoint: Optional[str] = None,
32+
timeout: int = 60,
33+
) -> None:
2534
"""Initialise client.
2635
2736
Args:
@@ -37,7 +46,7 @@ def __init__(
3746
self.timeout = timeout
3847

3948
@staticmethod
40-
def encode(request, data):
49+
def encode(request: Any, data: Optional[dict]) -> Any:
4150
"""Add request content data to request body, set Content-type header.
4251
4352
Should be overridden by subclasses if not using JSON encoding.
@@ -58,7 +67,7 @@ def encode(request, data):
5867
return request
5968

6069
@staticmethod
61-
def decode(response):
70+
def decode(response: Any) -> Any:
6271
"""Decode the returned data in the response.
6372
6473
Should be overridden by subclasses if something else than JSON is
@@ -73,9 +82,9 @@ def decode(response):
7382
try:
7483
return response.json()
7584
except ValueError as e:
76-
return e.message
85+
return e.message # type: ignore[attr-defined] # pre-existing (Python 2 style)
7786

78-
def get_credentials(self):
87+
def get_credentials(self) -> dict:
7988
"""Returns parameters to be added to authenticate the request.
8089
8190
This lives on its own to make it easier to re-implement it if needed.
@@ -87,14 +96,14 @@ def get_credentials(self):
8796

8897
def call_api(
8998
self,
90-
method,
91-
url,
92-
headers=None,
93-
params=None,
94-
data=None,
95-
files=None,
96-
timeout=None,
97-
):
99+
method: str,
100+
url: str,
101+
headers: Optional[dict] = None,
102+
params: Optional[dict] = None,
103+
data: Optional[dict] = None,
104+
files: Optional[dict] = None,
105+
timeout: Optional[int] = None,
106+
) -> Tuple[requests.Response, int]:
98107
"""Call API.
99108
100109
This returns object containing data, with error details if applicable.
@@ -130,7 +139,7 @@ def call_api(
130139

131140
return r, r.status_code
132141

133-
def get(self, url, params=None, **kwargs):
142+
def get(self, url: str, params: Optional[dict] = None, **kwargs: Any) -> Tuple[requests.Response, int]:
134143
"""Call the API with a GET request.
135144
136145
Args:
@@ -142,7 +151,7 @@ def get(self, url, params=None, **kwargs):
142151
"""
143152
return self.call_api("GET", url, params=params, **kwargs)
144153

145-
def delete(self, url, params=None, **kwargs):
154+
def delete(self, url: str, params: Optional[dict] = None, **kwargs: Any) -> Tuple[requests.Response, int]:
146155
"""Call the API with a DELETE request.
147156
148157
Args:
@@ -154,7 +163,14 @@ def delete(self, url, params=None, **kwargs):
154163
"""
155164
return self.call_api("DELETE", url, params=params, **kwargs)
156165

157-
def put(self, url, params=None, data=None, files=None, **kwargs):
166+
def put(
167+
self,
168+
url: str,
169+
params: Optional[dict] = None,
170+
data: Optional[dict] = None,
171+
files: Optional[dict] = None,
172+
**kwargs: Any,
173+
) -> Tuple[requests.Response, int]:
158174
"""Call the API with a PUT request.
159175
160176
Args:
@@ -170,7 +186,14 @@ def put(self, url, params=None, data=None, files=None, **kwargs):
170186
"PUT", url, params=params, data=data, files=files, **kwargs
171187
)
172188

173-
def post(self, url, params=None, data=None, files=None, **kwargs):
189+
def post(
190+
self,
191+
url: str,
192+
params: Optional[dict] = None,
193+
data: Optional[dict] = None,
194+
files: Optional[dict] = None,
195+
**kwargs: Any,
196+
) -> Tuple[requests.Response, int]:
174197
"""Call the API with a POST request.
175198
176199
Args:
@@ -186,7 +209,7 @@ def post(self, url, params=None, data=None, files=None, **kwargs):
186209
method="POST", url=url, params=params, data=data, files=files, **kwargs
187210
)
188211

189-
def service_status(self, **kwargs):
212+
def service_status(self, **kwargs: Any) -> Tuple[requests.Response, int]:
190213
"""Call the API to get the status of the service.
191214
192215
Returns:

0 commit comments

Comments
 (0)