Skip to content

Commit 4dbb425

Browse files
Python: Add new linting rules to High-level client (#302)
1 parent 65db1e0 commit 4dbb425

58 files changed

Lines changed: 1018 additions & 950 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

python/lib/sift_client/.ruff.toml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
extend = "../../pyproject.toml"
2+
3+
[lint]
4+
select = [
5+
# Core linting
6+
"F", # pyflakes - detect various errors
7+
"W", # pycodestyle warnings - style recommendations
8+
"B", # flake8-bugbear - detect potential bugs and design problems
9+
"PERF", # perflint - performance optimizations
10+
"RUF", # ruff-specific rules
11+
# "ASYNC", # flake8-async - async/await best practices # TODO
12+
13+
# Code style and formatting
14+
"I", # isort - import sorting
15+
"N", # pep8-naming - naming conventions
16+
"C4", # flake8-comprehensions - better list/dict/set comprehensions
17+
"UP", # pyupgrade - modernize syntax to latest Python
18+
19+
# Documentation
20+
"D", # pydocstyle - docstring style checking
21+
22+
# Imports and modules
23+
"TID", # flake8-tidy-imports - import organization
24+
"INP", # flake8-no-pep420 - no implicit namespace packages
25+
26+
# Type checking and annotations
27+
"FA", # flake8-future-annotations - necessary for backwards compatibility
28+
"TC", # flake8-type-checking - type checking best practices
29+
30+
# Built-ins and standard library
31+
"A", # flake8-builtins - prevent overriding built-ins
32+
"DTZ", # flake8-datetimez - good timezone practices
33+
34+
# Exception handling
35+
# "TRY", # tryceratops - exception handling antipatterns # TODO: FD-102
36+
37+
# Logging best practices
38+
# "G", # flake8-logging-format - logging format best practices # TODO: FD-101
39+
"LOG", # flake8-logging - logging best practices
40+
41+
# Tests
42+
# "PT", # flake8-pytest-style - pytest best practices # TODO: FD-59
43+
44+
]
45+
46+
ignore = ["W191", "D206", "D300", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
47+
"B024", # ignore missing abstract methods
48+
"D104", # Missing docstring in public package
49+
"D105", # Missing docstring in magic method
50+
"D205", # 1 blank line required between summary line and description
51+
"D100", # Missing docstring in public module
52+
]
53+
54+
55+
[lint.pydocstyle]
56+
convention = "google"
57+
58+
[lint.per-file-ignores]
59+
"examples/*" = ["D"]
60+
"_internal/*" = ["D"] # Private docs, be less strict

python/lib/sift_client/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ asset.update({
130130
For more complex updates, you can create update models (instead of a key-value dictionary):
131131

132132
```python
133-
from sift_client.types.asset import AssetUpdate
133+
from sift_client.sift_types.asset import AssetUpdate
134134

135135
# Create an update model
136136
update = AssetUpdate(tags=["new", "tags"])

python/lib/sift_client/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
!!! warning
1+
"""!!! warning
32
The Sift Client is experimental and is subject to change.
43
54

python/lib/sift_client/_internal/gen_pyi.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,16 @@ class {cls_name}:
2323
{methods}
2424
"""
2525

26-
METHOD_TEMPLATE = '''\
26+
METHOD_TEMPLATE = """\
2727
{decorator}
2828
def {meth_name}(self{params}){ret_ann}:
29-
"""
30-
{meth_doc}
31-
"""
29+
{docstring_section}
3230
...
33-
'''
31+
"""
3432

3533

3634
def extract_imports(path: pathlib.Path) -> list[str]:
37-
"""
38-
Parse the given Python file and return a list of its import statements (as strings).
39-
"""
35+
"""Parse the given Python file and return a list of its import statements (as strings)."""
4036
source = path.read_text()
4137
tree = ast.parse(source, filename=str(path))
4238

@@ -125,22 +121,23 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path
125121
async_class = matched.get("async_cls")
126122
if async_class is None:
127123
warnings.warn(
128-
f"Could not find async class for {cls_name}. Skipping stub generation."
124+
f"Could not find async class for {cls_name}. Skipping stub generation.",
125+
stacklevel=2,
129126
)
130127
continue
131128

132129
# Read imports from the original async class module
133130
source_file = inspect.getsourcefile(async_class)
134131
if source_file is None:
135132
warnings.warn(
136-
f"Could not find source file for {async_class.__name__}. Skipping stub generation."
133+
f"Could not find source file for {async_class.__name__}. Skipping stub generation.",
134+
stacklevel=2,
137135
)
138136
continue
139137

140138
orig_path = pathlib.Path(source_file).resolve()
141139
imports = extract_imports(orig_path)
142-
for imp in imports:
143-
new_module_imports.append(imp)
140+
new_module_imports = new_module_imports + imports
144141

145142
# Class docstring
146143
raw_doc = inspect.getdoc(cls) or ""
@@ -183,7 +180,7 @@ def generate_stubs_for_module(path_arg: str | pathlib.Path) -> dict[pathlib.Path
183180
lines.append(stub)
184181

185182
unique_imports = list(OrderedDict.fromkeys(new_module_imports))
186-
lines = [HEADER] + unique_imports + lines
183+
lines = [HEADER, *unique_imports, *lines]
187184
pyi_file = py_file.with_suffix(".pyi")
188185

189186
stub_files[pyi_file] = "\n".join(lines)
@@ -264,14 +261,18 @@ def generate_method_stub(name: str, f: Callable, module, decorator: str = "") ->
264261

265262
# Method docstring
266263
raw_mdoc = inspect.getdoc(f) or ""
267-
meth_doc = raw_mdoc.replace('"""', '\\"\\"\\"').replace("\n", "\n ")
264+
if raw_mdoc and raw_mdoc.strip():
265+
meth_doc = raw_mdoc.replace('"""', '\\"\\"\\"').replace("\n", "\n ")
266+
docstring_section = f' """\n {meth_doc}\n """\n'
267+
else:
268+
docstring_section = ""
268269

269270
return METHOD_TEMPLATE.format(
270271
decorator=decorator,
271272
meth_name=name,
272273
params=params_txt,
273274
ret_ann=ret_txt,
274-
meth_doc=meth_doc,
275+
docstring_section=docstring_section,
275276
)
276277

277278

python/lib/sift_client/_internal/low_level_wrappers/assets.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,21 @@
1717
from sift_client._internal.low_level_wrappers.base import (
1818
LowLevelClientBase,
1919
)
20+
from sift_client.sift_types.asset import Asset, AssetUpdate
2021
from sift_client.transport import GrpcClient, WithGrpcClient
21-
from sift_client.types.asset import Asset, AssetUpdate
2222

2323
# Configure logging
2424
logger = logging.getLogger(__name__)
2525

2626

2727
class AssetsLowLevelClient(LowLevelClientBase, WithGrpcClient):
28-
"""
29-
Low-level client for the AssetsAPI.
28+
"""Low-level client for the AssetsAPI.
3029
3130
This class provides a thin wrapper around the autogenerated bindings for the AssetsAPI.
3231
"""
3332

3433
def __init__(self, grpc_client: GrpcClient):
35-
"""
36-
Initialize the AssetsLowLevelClient.
34+
"""Initialize the AssetsLowLevelClient.
3735
3836
Args:
3937
grpc_client: The gRPC client to use for making API calls.
@@ -43,7 +41,7 @@ def __init__(self, grpc_client: GrpcClient):
4341
async def get_asset(self, asset_id: str) -> Asset:
4442
request = GetAssetRequest(asset_id=asset_id)
4543
response = await self._grpc_client.get_stub(AssetServiceStub).GetAsset(request)
46-
grpc_asset = cast(GetAssetResponse, response).asset
44+
grpc_asset = cast("GetAssetResponse", response).asset
4745
return Asset._from_proto(grpc_asset)
4846

4947
async def list_all_assets(
@@ -53,8 +51,7 @@ async def list_all_assets(
5351
max_results: int | None = None,
5452
page_size: int | None = None,
5553
) -> list[Asset]:
56-
"""
57-
List all results matching the given query.
54+
"""List all results matching the given query.
5855
5956
Args:
6057
query_filter: The CEL query filter.
@@ -93,14 +90,14 @@ async def list_assets(
9390

9491
request = ListAssetsRequest(**request_kwargs)
9592
response = await self._grpc_client.get_stub(AssetServiceStub).ListAssets(request)
96-
response = cast(ListAssetsResponse, response)
93+
response = cast("ListAssetsResponse", response)
9794
return [Asset._from_proto(asset) for asset in response.assets], response.next_page_token
9895

9996
async def update_asset(self, update: AssetUpdate) -> Asset:
10097
grpc_asset, update_mask = update.to_proto_with_mask()
10198
request = UpdateAssetRequest(asset=grpc_asset, update_mask=update_mask)
10299
response = await self._grpc_client.get_stub(AssetServiceStub).UpdateAsset(request)
103-
updated_grpc_asset = cast(UpdateAssetResponse, response).asset
100+
updated_grpc_asset = cast("UpdateAssetResponse", response).asset
104101
return Asset._from_proto(updated_grpc_asset)
105102

106103
async def delete_asset(self, asset_id: str, archive_runs: bool = False) -> None:

python/lib/sift_client/_internal/low_level_wrappers/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ class LowLevelClientBase(ABC):
88
@staticmethod
99
async def _handle_pagination(
1010
func: Callable,
11-
kwargs: dict[str, Any] = {},
11+
kwargs: dict[str, Any] | None = None,
1212
page_size: int | None = None,
1313
page_token: str | None = None,
1414
order_by: str | None = None,
1515
max_results: int | None = None,
1616
) -> list[Any]:
17-
"""
18-
Handle pagination for a given function by calling the function until all results are retrieved or the max_results is reached.
17+
"""Handle pagination for a given function by calling the function until all results are retrieved or the max_results is reached.
1918
2019
Args:
2120
func: The function to call.
@@ -28,6 +27,9 @@ async def _handle_pagination(
2827
Returns:
2928
A list of all matching results.
3029
"""
30+
if kwargs is None:
31+
kwargs = {}
32+
3133
results: list[Any] = []
3234
if page_token is None:
3335
page_token = ""

0 commit comments

Comments
 (0)