Skip to content

Commit eb8f596

Browse files
authored
Use ffi.gc to handle object lifecycle (#5)
1 parent 7bfe993 commit eb8f596

File tree

3 files changed

+87
-103
lines changed

3 files changed

+87
-103
lines changed

Diff for: README.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ This package exposes a ``URL`` class that is intended to match the one described
4040
.. code-block:: python
4141
4242
>>> import ada_url
43-
>>> with ada_url.URL('https://example.org/path/../file.txt') as urlobj:
44-
... urlobj.host = 'example.com'
45-
... new_url = urlobj.href
43+
>>> ada_url.URL('https://example.org/path/../file.txt') as urlobj:
44+
>>> urlobj.host = 'example.com'
45+
>>> new_url = urlobj.href
4646
>>> new_url
4747
'https://example.com/file.txt'
4848

Diff for: ada_url/ada_adapter.py

+50-64
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
SET_ATTRIBUTES = frozenset(URL_ATTRIBUTES)
1919

2020

21+
def _get_urlobj(constructor, *args):
22+
urlobj = constructor(*args)
23+
24+
return ffi.gc(urlobj, lib.ada_free)
25+
26+
2127
def _get_str(x):
2228
ret = ffi.string(x.data, x.length).decode('utf-8') if x.length else ''
2329
return ret
@@ -32,19 +38,14 @@ class URL:
3238
3339
>>> from ada_url import URL
3440
>>> old_url = 'https://example.org:443/file.txt?q=1'
35-
>>> with URL(old_url) as urlobj:
36-
... old_host = urlobj.host
37-
... urlobj.host = 'example.com'
38-
... new_url = urlobj.href
39-
>>> old_host
41+
>>> urlobj = URL(old_url)
42+
>>> urlobj.host
4043
'example.org'
44+
>>> urlobj.host = 'example.com'
45+
>>> new_url = urlobj.href
4146
>>> new_url
4247
'https://example.com:443/file.txt?q=1'
4348
44-
Note that you should use this class as a context manager to ensure
45-
that resources are freed. If you use it without a ``with``
46-
statement, call the ``close()`` method manually.
47-
4849
You can read and write the following attributes:
4950
5051
* ``href``
@@ -78,11 +79,15 @@ def __init__(self, url, base=None):
7879
url_bytes = url.encode('utf-8')
7980

8081
if base is None:
81-
self.urlobj = lib.ada_parse(url_bytes, len(url_bytes))
82+
self.urlobj = _get_urlobj(lib.ada_parse, url_bytes, len(url_bytes))
8283
else:
8384
base_bytes = base.encode('utf-8')
84-
self.urlobj = lib.ada_parse_with_base(
85-
url_bytes, len(url_bytes), base_bytes, len(base_bytes)
85+
self.urlobj = _get_urlobj(
86+
lib.ada_parse_with_base,
87+
url_bytes,
88+
len(url_bytes),
89+
base_bytes,
90+
len(base_bytes),
8691
)
8792

8893
if not lib.ada_is_valid(self.urlobj):
@@ -119,15 +124,6 @@ def __setattr__(self, attr, value):
119124

120125
return super().__setattr__(attr, value)
121126

122-
def close(self):
123-
lib.ada_free(self.urlobj)
124-
125-
def __enter__(self, *args, **kwargs):
126-
return self
127-
128-
def __exit__(self, *args, **kwargs):
129-
self.close()
130-
131127
@staticmethod
132128
def can_parse(url, base=None):
133129
try:
@@ -166,11 +162,8 @@ def check_url(s):
166162
except Exception:
167163
return False
168164

169-
urlobj = lib.ada_parse(s_bytes, len(s_bytes))
170-
try:
171-
return lib.ada_is_valid(urlobj)
172-
finally:
173-
lib.ada_free(urlobj)
165+
urlobj = _get_urlobj(lib.ada_parse, s_bytes, len(s_bytes))
166+
return lib.ada_is_valid(urlobj)
174167

175168

176169
def join_url(base_url, s):
@@ -192,14 +185,13 @@ def join_url(base_url, s):
192185
except Exception:
193186
raise ValueError('Invalid URL') from None
194187

195-
urlobj = lib.ada_parse_with_base(s_bytes, len(s_bytes), base_bytes, len(base_bytes))
196-
try:
197-
if not lib.ada_is_valid(urlobj):
198-
raise ValueError('Invalid URL') from None
188+
urlobj = _get_urlobj(
189+
lib.ada_parse_with_base, s_bytes, len(s_bytes), base_bytes, len(base_bytes)
190+
)
191+
if not lib.ada_is_valid(urlobj):
192+
raise ValueError('Invalid URL') from None
199193

200-
return _get_str(lib.ada_get_href(urlobj))
201-
finally:
202-
lib.ada_free(urlobj)
194+
return _get_str(lib.ada_get_href(urlobj))
203195

204196

205197
def normalize_url(s):
@@ -260,19 +252,16 @@ def parse_url(s, attributes=PARSE_ATTRIBUTES):
260252
raise ValueError('Invalid URL') from None
261253

262254
ret = {}
263-
urlobj = lib.ada_parse(s_bytes, len(s_bytes))
264-
try:
265-
if not lib.ada_is_valid(urlobj):
266-
raise ValueError('Invalid URL') from None
255+
urlobj = _get_urlobj(lib.ada_parse, s_bytes, len(s_bytes))
256+
if not lib.ada_is_valid(urlobj):
257+
raise ValueError('Invalid URL') from None
267258

268-
for attr in attributes:
269-
get_func = getattr(lib, f'ada_get_{attr}')
270-
data = get_func(urlobj)
271-
ret[attr] = _get_str(data)
272-
if attr == 'origin':
273-
lib.ada_free_owned_string(data)
274-
finally:
275-
lib.ada_free(urlobj)
259+
for attr in attributes:
260+
get_func = getattr(lib, f'ada_get_{attr}')
261+
data = get_func(urlobj)
262+
ret[attr] = _get_str(data)
263+
if attr == 'origin':
264+
lib.ada_free_owned_string(data)
276265

277266
return ret
278267

@@ -300,26 +289,23 @@ def replace_url(s, **kwargs):
300289
except Exception:
301290
raise ValueError('Invalid URL') from None
302291

303-
urlobj = lib.ada_parse(s_bytes, len(s_bytes))
304-
try:
305-
if not lib.ada_is_valid(urlobj):
306-
raise ValueError('Invalid URL') from None
292+
urlobj = _get_urlobj(lib.ada_parse, s_bytes, len(s_bytes))
293+
if not lib.ada_is_valid(urlobj):
294+
raise ValueError('Invalid URL') from None
307295

308-
for attr in URL_ATTRIBUTES:
309-
value = kwargs.get(attr)
310-
if value is None:
311-
continue
296+
for attr in URL_ATTRIBUTES:
297+
value = kwargs.get(attr)
298+
if value is None:
299+
continue
312300

313-
try:
314-
value_bytes = value.encode()
315-
except Exception:
316-
raise ValueError(f'Invalid value for {attr}') from None
301+
try:
302+
value_bytes = value.encode()
303+
except Exception:
304+
raise ValueError(f'Invalid value for {attr}') from None
317305

318-
set_func = getattr(lib, f'ada_set_{attr}')
319-
set_result = set_func(urlobj, value_bytes, len(value_bytes))
320-
if (set_result is not None) and (not set_result):
321-
raise ValueError(f'Invalid value for {attr}') from None
306+
set_func = getattr(lib, f'ada_set_{attr}')
307+
set_result = set_func(urlobj, value_bytes, len(value_bytes))
308+
if (set_result is not None) and (not set_result):
309+
raise ValueError(f'Invalid value for {attr}') from None
322310

323-
return _get_str(lib.ada_get_href(urlobj))
324-
finally:
325-
lib.ada_free(urlobj)
311+
return _get_str(lib.ada_get_href(urlobj))

Diff for: tests/test_ada_url.py

+34-36
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,54 @@
1414
class ADAURLTests(TestCase):
1515
def test_class_get(self):
1616
url = 'https://user_1:[email protected]:8080/dir/../api?q=1#frag'
17-
with URL(url) as urlobj:
18-
self.assertEqual(
19-
urlobj.href, 'https://user_1:[email protected]:8080/api?q=1#frag'
20-
)
21-
self.assertEqual(urlobj.username, 'user_1')
22-
self.assertEqual(urlobj.password, 'password_1')
23-
self.assertEqual(urlobj.protocol, 'https:')
24-
self.assertEqual(urlobj.port, '8080')
25-
self.assertEqual(urlobj.hostname, 'example.org')
26-
self.assertEqual(urlobj.host, 'example.org:8080')
27-
self.assertEqual(urlobj.pathname, '/api')
28-
self.assertEqual(urlobj.search, '?q=1')
29-
self.assertEqual(urlobj.hash, '#frag')
30-
self.assertEqual(urlobj.origin, 'https://example.org:8080')
17+
urlobj = URL(url)
18+
self.assertEqual(
19+
urlobj.href, 'https://user_1:[email protected]:8080/api?q=1#frag'
20+
)
21+
self.assertEqual(urlobj.username, 'user_1')
22+
self.assertEqual(urlobj.password, 'password_1')
23+
self.assertEqual(urlobj.protocol, 'https:')
24+
self.assertEqual(urlobj.port, '8080')
25+
self.assertEqual(urlobj.hostname, 'example.org')
26+
self.assertEqual(urlobj.host, 'example.org:8080')
27+
self.assertEqual(urlobj.pathname, '/api')
28+
self.assertEqual(urlobj.search, '?q=1')
29+
self.assertEqual(urlobj.hash, '#frag')
30+
self.assertEqual(urlobj.origin, 'https://example.org:8080')
3131

32-
with self.assertRaises(AttributeError):
33-
urlobj.bogus
32+
with self.assertRaises(AttributeError):
33+
urlobj.bogus
3434

3535
def test_class_set(self):
3636
url = 'https://username:[email protected]:8080/'
37-
with URL(url) as urlobj:
38-
urlobj.href = 'https://www.yagiz.co'
39-
urlobj.hash = 'new-hash'
40-
urlobj.hostname = 'new-host'
41-
urlobj.host = 'changed-host:9090'
42-
urlobj.pathname = 'new-pathname'
43-
urlobj.search = 'new-search'
44-
urlobj.protocol = 'wss'
45-
actual = urlobj.href
37+
urlobj = URL(url)
38+
urlobj.href = 'https://www.yagiz.co'
39+
urlobj.hash = 'new-hash'
40+
urlobj.hostname = 'new-host'
41+
urlobj.host = 'changed-host:9090'
42+
urlobj.pathname = 'new-pathname'
43+
urlobj.search = 'new-search'
44+
urlobj.protocol = 'wss'
45+
actual = urlobj.href
4646

47-
with self.assertRaises(ValueError):
48-
urlobj.hostname = 1
47+
with self.assertRaises(ValueError):
48+
urlobj.hostname = 1
4949

50-
with self.assertRaises(ValueError):
51-
urlobj.hostname = '127.0.0.0.0.1'
50+
with self.assertRaises(ValueError):
51+
urlobj.hostname = '127.0.0.0.0.1'
5252

5353
expected = 'wss://changed-host:9090/new-pathname?new-search#new-hash'
5454
self.assertEqual(actual, expected)
5555

5656
def test_class_with_base(self):
5757
url = '../example.txt'
5858
base = 'https://example.org/path/'
59-
with URL(url, base) as urlobj:
60-
self.assertEqual(urlobj.href, 'https://example.org/example.txt')
59+
urlobj = URL(url, base)
60+
self.assertEqual(urlobj.href, 'https://example.org/example.txt')
6161

6262
def test_class_invalid(self):
6363
with self.assertRaises(ValueError):
64-
with URL('bogus'):
65-
pass
64+
URL('bogus')
6665

6766
def test_class_can_parse(self):
6867
for url, expected in (
@@ -88,9 +87,8 @@ def test_class_can_parse_with_base(self):
8887
self.assertEqual(actual, expected)
8988

9089
def test_class_dir(self):
91-
with URL('https://example.org') as urlobj:
92-
actual = set(dir(urlobj))
93-
90+
urlobj = URL('https://example.org')
91+
actual = set(dir(urlobj))
9492
self.assertTrue(actual.issuperset(GET_ATTRIBUTES))
9593

9694
def test_check_url(self):

0 commit comments

Comments
 (0)