Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf9fed09

Browse files
chore: add EncodedId string class to use to hold URL-encoded paths
Add EncodedId string class. This class returns a URL-encoded stringbut ensures it will only URL-encode it once even if recursivelycalled.Also added some functional tests of 'lazy' objects to make sure theywork.
1 parente6ba4b2 commitf9fed09

File tree

5 files changed

+177
-5
lines changed

5 files changed

+177
-5
lines changed

‎gitlab/utils.py

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,88 @@ def copy_dict(dest: Dict[str, Any], src: Dict[str, Any]) -> None:
5656
dest[k]=v
5757

5858

59+
classEncodedId(str):
60+
"""A custom `str` class that will return the URL-encoded value of the string.
61+
62+
Features:
63+
* Using it recursively will only url-encode the value once.
64+
* Can accept either `str` or `int` as input value.
65+
* Can be used in an f-string and output the URL-encoded string.
66+
67+
Reference to documentation on why this is necessary.
68+
69+
https://docs.gitlab.com/ee/api/index.html#namespaced-path-encoding
70+
71+
If using namespaced API requests, make sure that the NAMESPACE/PROJECT_PATH is
72+
URL-encoded. For example, / is represented by %2F
73+
74+
https://docs.gitlab.com/ee/api/index.html#path-parameters
75+
76+
Path parameters that are required to be URL-encoded must be followed. If not, it
77+
doesn’t match an API endpoint and responds with a 404. If there’s something in
78+
front of the API (for example, Apache), ensure that it doesn’t decode the
79+
URL-encoded path parameters.
80+
81+
82+
When creating an EncodedId instance `__new__` will be called first and then
83+
`__init__`.
84+
"""
85+
86+
# `original_str` will contain the original string value that was used to create the
87+
# first instance of EncodedId. We will use this original value to generate the
88+
# URL-encoded value each time.
89+
original_str:str
90+
91+
def__new__(cls,value:Union[str,int,"EncodedId"])->"EncodedId":
92+
ifisinstance(value,int):
93+
value=str(value)
94+
# Make sure isinstance() for `EncodedId` comes before check for `str` as
95+
# `EncodedId` is an instance of `str` and would pass that check.
96+
elifisinstance(value,EncodedId):
97+
# We use the original string value to URL-encode
98+
value=value.original_str
99+
elifisinstance(value,str):
100+
pass
101+
else:
102+
raiseValueError(f"Unsupported type received:{type(value)}")
103+
# Set the value our string will return
104+
value=urllib.parse.quote(value,safe="")
105+
returnsuper().__new__(cls,value)
106+
107+
def__init__(self,value:Union[int,str])->None:
108+
# At this point `super().__str__()` returns the URL-encoded value. Which means
109+
# when using this as a `str` it will return the URL-encoded value.
110+
#
111+
# But `value` contains the original value passed in `EncodedId(value)`. We use
112+
# this to always keep the original string that was received so that no matter
113+
# how many times we recurse we only URL-encode our original string once.
114+
ifisinstance(value,int):
115+
value=str(value)
116+
# Make sure isinstance() for `EncodedId` comes before check for `str` as
117+
# `EncodedId` is an instance of `str` and would pass that check.
118+
elifisinstance(value,EncodedId):
119+
# This is the key part as we are always keeping the original string even
120+
# through multiple recursions.
121+
value=value.original_str
122+
elifisinstance(value,str):
123+
pass
124+
else:
125+
raiseValueError(f"Unsupported type received:{type(value)}")
126+
self.original_str=value
127+
super().__init__()
128+
129+
59130
@overload
60131
def_url_encode(id:int)->int:
61132
...
62133

63134

64135
@overload
65-
def_url_encode(id:str)->str:
136+
def_url_encode(id:Union[str,EncodedId])->EncodedId:
66137
...
67138

68139

69-
def_url_encode(id:Union[int,str])->Union[int,str]:
140+
def_url_encode(id:Union[int,str,EncodedId])->Union[int,EncodedId]:
70141
"""Encode/quote the characters in the string so that they can be used in a path.
71142
72143
Reference to documentation on why this is necessary.
@@ -84,9 +155,9 @@ def _url_encode(id: Union[int, str]) -> Union[int, str]:
84155
parameters.
85156
86157
"""
87-
ifisinstance(id,int):
158+
ifisinstance(id,(int,EncodedId)):
88159
returnid
89-
returnurllib.parse.quote(id,safe="")
160+
returnEncodedId(id)
90161

91162

92163
defremove_none_from_dict(data:Dict[str,Any])->Dict[str,Any]:

‎tests/functional/api/test_groups.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def test_groups(gl):
100100
member=group1.members.get(user2.id)
101101
assertmember.access_level==gitlab.const.OWNER_ACCESS
102102

103+
gl.auth()
103104
group2.members.delete(gl.user.id)
104105

105106

@@ -198,6 +199,11 @@ def test_group_subgroups_projects(gl, user):
198199
assertgr1_project.namespace["id"]==group1.id
199200
assertgr2_project.namespace["parent_id"]==group1.id
200201

202+
gr1_project.delete()
203+
gr2_project.delete()
204+
group3.delete()
205+
group4.delete()
206+
201207

202208
@pytest.mark.skip
203209
deftest_group_wiki(group):
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
importpytest
2+
3+
importgitlab
4+
5+
6+
@pytest.fixture
7+
deflazy_project(gl,project):
8+
assert"/"inproject.path_with_namespace
9+
returngl.projects.get(project.path_with_namespace,lazy=True)
10+
11+
12+
deftest_lazy_id(project,lazy_project):
13+
assertisinstance(lazy_project.id,str)
14+
assertisinstance(lazy_project.id,gitlab.utils.EncodedId)
15+
assertlazy_project.id==gitlab.utils._url_encode(project.path_with_namespace)
16+
17+
18+
deftest_refresh_after_lazy_get_with_path(project,lazy_project):
19+
lazy_project.refresh()
20+
assertlazy_project.id==project.id
21+
22+
23+
deftest_save_after_lazy_get_with_path(project,lazy_project):
24+
lazy_project.description="A new description"
25+
lazy_project.save()
26+
assertlazy_project.id==project.id
27+
assertlazy_project.description=="A new description"
28+
29+
30+
deftest_delete_after_lazy_get_with_path(gl,group,wait_for_sidekiq):
31+
project=gl.projects.create({"name":"lazy_project","namespace_id":group.id})
32+
result=wait_for_sidekiq(timeout=60)
33+
assertresultisTrue,"sidekiq process should have terminated but did not"
34+
lazy_project=gl.projects.get(project.path_with_namespace,lazy=True)
35+
lazy_project.delete()
36+
37+
38+
deftest_method_call(gl,lazy_project):
39+
lazy_project.mergerequests.list()

‎tests/functional/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,8 @@ def user(gl):
406406
yielduser
407407

408408
try:
409-
user.delete()
409+
# Use `hard_delete=True` or a 'Ghost User' may be created.
410+
user.delete(hard_delete=True)
410411
exceptgitlab.exceptions.GitlabDeleteErrorase:
411412
print(f"User already deleted:{e}")
412413

‎tests/unit/test_utils.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18+
importjson
19+
1820
fromgitlabimportutils
1921

2022

@@ -35,3 +37,56 @@ def test_url_encode():
3537
src="docs/README.md"
3638
dest="docs%2FREADME.md"
3739
assertdest==utils._url_encode(src)
40+
41+
42+
classTestEncodedId:
43+
deftest_init_str(self):
44+
obj=utils.EncodedId("Hello")
45+
assert"Hello"==str(obj)
46+
assert"Hello"==f"{obj}"
47+
48+
obj=utils.EncodedId("this/is a/path")
49+
assert"this%2Fis%20a%2Fpath"==str(obj)
50+
assert"this%2Fis%20a%2Fpath"==f"{obj}"
51+
52+
deftest_init_int(self):
53+
obj=utils.EncodedId(23)
54+
assert"23"==str(obj)
55+
assert"23"==f"{obj}"
56+
57+
deftest_init_encodeid_str(self):
58+
value="Goodbye"
59+
obj_init=utils.EncodedId(value)
60+
obj=utils.EncodedId(obj_init)
61+
assertvalue==str(obj)
62+
assertvalue==f"{obj}"
63+
assertvalue==obj.original_str
64+
65+
value="we got/a/path"
66+
expected="we%20got%2Fa%2Fpath"
67+
obj_init=utils.EncodedId(value)
68+
assertvalue==obj_init.original_str
69+
# Show that no matter how many times we recursively call it we still only
70+
# URL-encode it once.
71+
obj=utils.EncodedId(
72+
utils.EncodedId(utils.EncodedId(utils.EncodedId(utils.EncodedId(obj_init))))
73+
)
74+
assertexpected==str(obj)
75+
assertexpected==f"{obj}"
76+
# We have stored a copy of our original string
77+
assertvalue==obj.original_str
78+
79+
deftest_init_encodeid_int(self):
80+
value=23
81+
expected=f"{value}"
82+
obj_init=utils.EncodedId(value)
83+
obj=utils.EncodedId(obj_init)
84+
assertexpected==str(obj)
85+
assertexpected==f"{obj}"
86+
87+
deftest_json_serializable(self):
88+
obj=utils.EncodedId("someone")
89+
assert'"someone"'==json.dumps(obj)
90+
91+
obj=utils.EncodedId("we got/a/path")
92+
assert'"we%20got%2Fa%2Fpath"'==json.dumps(obj)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp