Skip to content

Commit 01471e9

Browse files
committed
Separate out parent= into parent= or parent_id=. Use overloads to help create combinations of each parameter
1 parent 7c1dfce commit 01471e9

File tree

3 files changed

+46
-40
lines changed

3 files changed

+46
-40
lines changed

src/posit/connect/permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def destroy(self, *permissions: str | Group | User | Permission) -> list[Permiss
158158
Returns
159159
-------
160160
list[Permission]
161-
The removed permissions. If a permission is not found, there is nothing to remove and
161+
The removed permissions. If a permission is not found, there is nothing to remove and
162162
it is not included in the returned list.
163163
164164
Examples

src/posit/connect/tags.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Optional
3+
from typing import TYPE_CHECKING, Optional, overload
44

55
from typing_extensions import NotRequired, TypedDict, Unpack
66

@@ -266,42 +266,37 @@ def get(self, tag_id: str) -> Tag:
266266
response = self._ctx.session.get(url)
267267
return Tag(self._ctx, path, **response.json())
268268

269-
class _NameParentAttrs(TypedDict, total=False):
270-
name: str
271-
"""
272-
The name of the tag.
273-
274-
Note: tag names are only unique within the scope of a parent, which
275-
means that it is possible to have multiple results when querying by
276-
name; however, querying by both `name` and `parent` ensures a single
277-
result.
278-
"""
279-
parent: NotRequired[str | Tag | None]
280-
"""The identifier for the parent tag. If there is no parent, the tag is a top-level tag."""
281-
282269
def _update_parent_kwargs(self, kwargs: dict) -> dict:
283270
parent = kwargs.get("parent", None)
284271
if parent is None:
272+
# No parent to upgrade, return the kwargs as is
285273
return kwargs
286274

275+
if not isinstance(parent, Tag):
276+
raise TypeError(
277+
"`parent=` must be a Tag instance. If using a string, please use `parent_id=`"
278+
)
279+
280+
parent_id = kwargs.get("parent_id", None)
281+
if parent_id:
282+
raise ValueError("Cannot provide both `parent=` and `parent_id=`")
283+
287284
ret_kwargs = {**kwargs}
288285

289286
# Remove `parent` from ret_kwargs
290287
# and store the `parent_id` in the ret_kwargs below
291288
del ret_kwargs["parent"]
292289

293-
if isinstance(parent, Tag):
294-
parent: str = parent["id"]
290+
ret_kwargs["parent_id"] = parent["id"]
291+
return ret_kwargs
295292

296-
if isinstance(parent, str):
297-
if parent == "":
298-
raise ValueError("Tag `parent` cannot be an empty string")
299-
ret_kwargs["parent_id"] = parent
300-
return ret_kwargs
293+
# Allow for every combination of `name` and (`parent` or `parent_id`)
294+
@overload
295+
def find(self, /, *, name: str = ..., parent: Tag = ...) -> list[Tag]: ...
296+
@overload
297+
def find(self, /, *, name: str = ..., parent_id: str = ...) -> list[Tag]: ...
301298

302-
raise TypeError("`parent=` must be a string or Tag instance")
303-
304-
def find(self, /, **kwargs: Unpack[Tags._NameParentAttrs]) -> list[Tag]:
299+
def find(self, /, **kwargs) -> list[Tag]:
305300
"""
306301
Find tags by name and/or parent.
307302
@@ -313,7 +308,10 @@ def find(self, /, **kwargs: Unpack[Tags._NameParentAttrs]) -> list[Tag]:
313308
----------
314309
name : str, optional
315310
The name of the tag.
316-
parent : str, Tag, optional
311+
parent : Tag, optional
312+
The parent `Tag` object. If there is no parent, the tag is a top-level tag. Only one of
313+
`parent` or `parent_id` can be provided.
314+
parent_id : str, optional
317315
The identifier for the parent tag. If there is no parent, the tag is a top-level tag.
318316
319317
Returns
@@ -348,15 +346,25 @@ def find(self, /, **kwargs: Unpack[Tags._NameParentAttrs]) -> list[Tag]:
348346
results = response.json()
349347
return [Tag(self._ctx, f"{self._path}/{result['id']}", **result) for result in results]
350348

351-
def create(self, /, **kwargs: Unpack[Tags._NameParentAttrs]) -> Tag:
349+
@overload
350+
def create(self, /, *, name: str) -> Tag: ...
351+
@overload
352+
def create(self, /, *, name: str, parent: Tag) -> Tag: ...
353+
@overload
354+
def create(self, /, *, name: str, parent_id: str) -> Tag: ...
355+
356+
def create(self, /, **kwargs) -> Tag:
352357
"""
353358
Create a tag.
354359
355360
Parameters
356361
----------
357362
name : str
358363
The name of the tag.
359-
parent : str, Tag, optional
364+
parent : Tag, optional
365+
The parent `Tag` object. If there is no parent, the tag is a top-level tag. Only one of
366+
`parent` or `parent_id` can be provided.
367+
parent_id : str, optional
360368
The identifier for the parent tag. If there is no parent, the tag is a top-level tag.
361369
362370
Returns
@@ -456,8 +464,7 @@ def add(self, *tags: str | Tag) -> None:
456464

457465
url = self._ctx.url + self._path
458466
for tag_id in tag_ids:
459-
_ = self._ctx.session.post(url, json={"tag_id": tag_id})
460-
return
467+
self._ctx.session.post(url, json={"tag_id": tag_id})
461468

462469
def delete(self, *tags: str | Tag) -> None:
463470
"""
@@ -481,4 +488,3 @@ def delete(self, *tags: str | Tag) -> None:
481488
for tag_id in tag_ids:
482489
tag_url = f"{url}/{tag_id}"
483490
self._ctx.session.delete(tag_url)
484-
return

tests/posit/connect/test_tags.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,14 @@ def test_find_tags_by_parent(self):
7070
client = Client(api_key="12345", url="https://connect.example")
7171

7272
# invoke
73-
by_str_tags = client.tags.find(parent="3")
73+
by_str_tags = client.tags.find(parent_id="3")
7474
parent_tag = Tag(client._ctx, "/v1/tags/3", id="3", name="Parent")
7575
by_tag_tags = client.tags.find(parent=parent_tag)
76-
by_parent_id_tags = client.tags.find(
77-
parent_id=3, # pyright: ignore[reportCallIssue]
78-
)
7976

8077
# assert
81-
assert mock_get_tags.call_count == 3
78+
assert mock_get_tags.call_count == 2
8279
assert len(by_str_tags) == 7
8380
assert len(by_tag_tags) == 7
84-
assert len(by_parent_id_tags) == 7
8581

8682
@responses.activate
8783
def test_get(self):
@@ -115,16 +111,20 @@ def test_create_tag(self):
115111
tag = Tag(client._ctx, "/v1/tags/1", id="3", name="Tag")
116112

117113
# invoke
118-
academy_tag_parent_id = client.tags.create(name="academy", parent=tag["id"])
114+
academy_tag_parent_id = client.tags.create(name="academy", parent_id=tag["id"])
119115
academy_tag_parent_tag = client.tags.create(name="academy", parent=tag)
120116

121117
with pytest.raises(TypeError):
122118
client.tags.create(
123119
name="academy",
124-
parent=123, # pyright: ignore[reportArgumentType]
120+
parent="not a tag", # pyright: ignore[reportArgumentType]
125121
)
126122
with pytest.raises(ValueError):
127-
client.tags.create(name="academy", parent="")
123+
client.tags.create( # pyright: ignore[reportCallIssue]
124+
name="academy",
125+
parent=tag,
126+
parent_id="asdf",
127+
)
128128

129129
# assert
130130
assert mock_create_tag.call_count == 2

0 commit comments

Comments
 (0)