23
23
from typing import Optional
24
24
25
25
from libvcs ._internal .dataclasses import SkipDefaultFieldsReprMixin
26
+ from libvcs .url .git import RE_PIP_REV , SCP_REGEX
26
27
27
28
from .base import Rule , RuleMap , URLProtocol
28
29
29
30
RE_PATH = r"""
30
- ((?P<user>.* )@)?
31
- (?P<hostname>([^/:]+))
31
+ ((?P<user>[^/:@]+ )@)?
32
+ (?P<hostname>([^/:@ ]+))
32
33
(:(?P<port>\d{1,5}))?
33
- (?P<separator>/ )?
34
+ (?P<separator>[:,/] )?
34
35
(?P<path>
35
- (\w[^:.]*)
36
+ (\w[^:.@ ]*)
36
37
)?
37
38
"""
38
39
56
57
^{ RE_SCHEME }
57
58
://
58
59
{ RE_PATH }
60
+ { RE_PIP_REV } ?
59
61
""" ,
60
62
re .VERBOSE ,
61
63
),
62
64
),
65
+ Rule (
66
+ label = "core-svn-scp" ,
67
+ description = "Vanilla scp(1) / ssh(1) type URL" ,
68
+ pattern = re .compile (
69
+ rf"""
70
+ ^(?P<scheme>ssh)?
71
+ { SCP_REGEX }
72
+ { RE_PIP_REV } ?
73
+ """ ,
74
+ re .VERBOSE ,
75
+ ),
76
+ defaults = {"username" : "svn" },
77
+ ),
78
+ # SCP-style URLs, e.g. hg@
63
79
]
64
80
"""Core regular expressions. These are patterns understood by ``svn(1)``"""
65
81
72
88
(
73
89
svn\+ssh|
74
90
svn\+https|
75
- svn\+http|
76
- svn\+file
91
+ svn\+http
77
92
)
78
93
)
79
94
"""
84
99
description = "pip-style svn URL" ,
85
100
pattern = re .compile (
86
101
rf"""
87
- { RE_PIP_SCHEME<
6D4E
/span>}
102
+ ^ { RE_PIP_SCHEME }
88
103
://
89
104
{ RE_PATH }
105
+ { RE_PIP_REV } ?
90
106
""" ,
91
107
re .VERBOSE ,
92
108
),
109
+ is_explicit = True ,
93
110
),
94
111
# file://, RTC 8089, File:// https://datatracker.ietf.org/doc/html/rfc8089
95
112
Rule (
102
119
""" ,
103
120
re .VERBOSE ,
104
121
),
122
+ is_explicit = True ,
105
123
),
106
124
]
107
125
"""pip-style svn URLs.
125
143
126
144
127
145
@dataclasses .dataclass (repr = False )
128
- class SvnURL (URLProtocol , SkipDefaultFieldsReprMixin ):
146
+ class SvnBaseURL (URLProtocol , SkipDefaultFieldsReprMixin ):
129
147
"""SVN repository location. Parses URLs on initialization.
130
148
131
149
Examples
132
150
--------
133
- >>> SvnURL(url='svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository')
134
- SvnURL(url=svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository,
151
+ >>> SvnBaseURL(
152
+ ... url='svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository')
153
+ SvnBaseURL(url=svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository,
135
154
scheme=svn+ssh,
136
155
hostname=svn.debian.org,
137
156
path=svn/aliothproj/path/in/project/repository,
138
157
rule=core-svn)
139
158
140
- >>> myrepo = SvnURL (
159
+ >>> myrepo = SvnBaseURL (
141
160
... url='svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository'
142
161
... )
143
162
@@ -147,8 +166,8 @@ class SvnURL(URLProtocol, SkipDefaultFieldsReprMixin):
147
166
>>> myrepo.path
148
167
'svn/aliothproj/path/in/project/repository'
149
168
150
- - Compatibility checking: :meth:`SvnURL .is_valid()`
151
- - URLs compatible with ``svn(1)``: :meth:`SvnURL .to_url()`
169
+ - Compatibility checking: :meth:`SvnBaseURL .is_valid()`
170
+ - URLs compatible with ``svn(1)``: :meth:`SvnBaseURL .to_url()`
152
171
153
172
Attributes
154
173
----------
@@ -194,12 +213,12 @@ def is_valid(cls, url: str, is_explicit: Optional[bool] = None) -> bool:
194
213
Examples
195
214
--------
196
215
197
- >>> SvnURL .is_valid(
216
+ >>> SvnBaseURL .is_valid(
198
217
... url='svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository'
199
218
... )
200
219
True
201
220
202
- >>> SvnURL .is_valid(url='notaurl')
221
+ >>> SvnBaseURL .is_valid(url='notaurl')
203
222
False
204
223
"""
205
224
if is_explicit is not None :
@@ -216,12 +235,12 @@ def to_url(self) -> str:
216
235
Examples
217
236
--------
218
237
219
- >>> svn_url = SvnURL (
238
+ >>> svn_url = SvnBaseURL (
220
239
... url='svn+ssh://my-username@my-server/vcs-python/libvcs'
221
240
... )
222
241
223
242
>>> svn_url
224
- SvnURL (url=svn+ssh://my-username@my-server/vcs-python/libvcs,
243
+ SvnBaseURL (url=svn+ssh://my-username@my-server/vcs-python/libvcs,
225
244
scheme=svn+ssh,
226
245
user=my-username,
227
246
hostname=my-server,
@@ -242,15 +261,241 @@ def to_url(self) -> str:
242
261
>>> svn_url.to_url()
243
262
'svn+ssh://tom@my-server/vcs-python/vcspull'
244
263
"""
245
- parts = [self .scheme or "ssh" , "://" ]
246
- if self .user :
247
- parts .extend ([self .user , "@" ])
264
+ if self .scheme is not None :
265
+ parts = [self .scheme , "://" ]
248
266
249
- parts .append (self .hostname )
267
+ if self .user is not None :
268
+ parts .append (f"{ self .user } @" )
269
+ parts .append (self .hostname )
270
+ else :
271
+ parts = [self .user or "hg" , "@" , self .hostname ]
250
272
251
273
if self .port is not None :
252
274
parts .extend ([":" , f"{ self .port } " ])
253
275
254
276
parts .extend ([self .separator , self .path ])
255
277
256
278
return "" .join (part for part in parts if isinstance (part , str ))
279
+
280
+
281
+ @dataclasses .dataclass (repr = False )
282
+ class SvnPipURL (SvnBaseURL , URLProtocol , SkipDefaultFieldsReprMixin ):
283
+ """Supports pip svn URLs."""
284
+
285
+ # commit-ish (rev): tag, branch, ref
286
+ rev : Optional [str ] = None
287
+
288
+ rule_map : RuleMap = RuleMap (_rule_map = {m .label : m for m in PIP_DEFAULT_RULES })
289
+
290
+ @classmethod
291
+ def is_valid (cls , url : str , is_explicit : Optional [bool ] = None ) -> bool :
292
+ """Whether URL is compatible with VCS or not.
293
+
294
+ Examples
295
+ --------
296
+
297
+ >>> SvnPipURL.is_valid(
298
+ ... url='svn+https://svn.mozilla.org/mozilla-central'
299
+ ... )
300
+ True
301
+
302
+ >>> SvnPipURL.is_valid(url='svn+ssh://svn@svn.python.org:cpython')
303
+ True
304
+
305
+ >>> SvnPipURL.is_valid(url='notaurl')
306
+ False
307
+ """
308
+ return super ().is_valid (url = url , is_explicit = is_explicit )
309
+
310
+ def to_url (self ) -> str :
311
+ """Return a ``svn(1)``-compatible URL. Can be used with ``svn clone``.
312
+
313
+ Examples
314
+ --------
315
+
316
+ >>> svn_url = SvnPipURL(url='svn+https://svn.mozilla.org/mozilla-central')
317
+
318
+ >>> svn_url
319
+ SvnPipURL(url=svn+https://svn.mozilla.org/mozilla-central,
320
+ scheme=svn+https,
321
+ hostname=svn.mozilla.org,
322
+ path=mozilla-central,
323
+ rule=pip-url)
324
+
325
+ Switch repo mozilla-central -> mobile-browser:
326
+
327
+ >>> svn_url.path = 'mobile-browser'
328
+
329
+ >>> svn_url.to_url()
330
+ 'svn+https://svn.mozilla.org/mobile-browser'
331
+
332
+ Switch them to localhost:
333
+
334
+ >>> svn_url.hostname = 'localhost'
335
+ >>> svn_url.scheme = 'http'
336
+
337
+ >>> svn_url.to_url()
338
+ 'http://localhost/mobile-browser'
339
+
340
+ """
341
+ return super ().to_url ()
342
+
343
+
344
+ @dataclasses .dataclass (repr = False )
345
+ class SvnURL (SvnPipURL , SvnBaseURL , URLProtocol , SkipDefaultFieldsReprMixin ):
346
+ """Batteries included URL Parser. Supports svn(1) and pip URLs.
347
+
348
+ **Ancestors (MRO)**
349
+ This URL parser inherits methods and attributes from the following parsers:
350
+
351
+ - :class:`SvnPipURL`
352
+
353
+ - :meth:`SvnPipURL.to_url`
354
+ - :class:`SvnBaseURL`
355
+
356
+ - :meth:`SvnBaseURL.to_url`
357
+ """
358
+
359
+ rule_map : RuleMap = RuleMap (
360
+ _rule_map = {m .label : m for m in [* DEFAULT_RULES , * PIP_DEFAULT_RULES ]}
361
+ )
362
+
363
+ @classmethod
364
+ def is_valid (cls , url : str , is_explicit : Optional [bool ] = None ) -> bool :
365
+ r"""Whether URL is compatible included Svn URL rule_map or not.
366
+
367
+ Examples
368
+ --------
369
+
370
+ **Will** match normal ``svn(1)`` URLs, use :meth:`SvnURL.is_valid` for that.
371
+
372
+ >>> SvnURL.is_valid(
373
+ ... url='https://svn.mozilla.org/mozilla-central/mozilla-central')
374
+ True
375
+
376
+ >>> SvnURL.is_valid(url='svn@svn.mozilla.org:MyProject/project')
377
+ True
378
+
379
+ Pip-style URLs:
380
+
381
+ >>> SvnURL.is_valid(url='svn+https://svn.mozilla.org/mozilla-central/project')
382
+ True
383
+
384
+ >>> SvnURL.is_valid(url='svn+ssh://svn@svn.mozilla.org:MyProject/project')
385
+ True
386
+
387
+ >>> SvnURL.is_valid(url='notaurl')
388
+ False
389
+
390
+ **Explicit VCS detection**
391
+
392
+ Pip-style URLs are prefixed with the VCS name in front, so its rule_map can
393
+ unambigously narrow the type of VCS:
394
+
395
+ >>> SvnURL.is_valid(
396
+ ... url='svn+ssh://svn@svn.mozilla.org:mozilla-central/image',
397
+ ... is_explicit=True
398
+ ... )
399
+ True
400
+
401
+ Below, while it's svn.mozilla.org, that doesn't necessarily mean that the URL
402
+ itself is conclusively a `svn` URL (e.g. the pattern is too broad):
403
+
404
+ >>> SvnURL.is_valid(
405
+ ... url='svn@svn.mozilla.org:mozilla-central/image', is_explicit=True
406
+ ... )
407
+ False
408
+
409
+ You could create a Mozilla rule that consider svn.mozilla.org hostnames to be
410
+ exclusively svn:
411
+
412
+ >>> MozillaRule = Rule(
413
+ ... # Since svn.mozilla.org exclusively serves svn repos, make explicit
414
+ ... label='mozilla-rule',
415
+ ... description='Matches svn.mozilla.org https URLs, exact VCS match',
416
+ ... pattern=re.compile(
417
+ ... rf'''
418
+ ... ^(?P<scheme>ssh)?
419
+ ... ((?P<user>\w+)@)?
420
+ ... (?P<hostname>(svn.mozilla.org)+):
421
+ ... (?P<path>(\w[^:]+))
422
+ ... ''',
423
+ ... re.VERBOSE,
424
+ ... ),
425
+ ... is_explicit=True,
426
+ ... defaults={
427
+ ... 'hostname': 'svn.mozilla.org'
428
+ ... }
429
+ ... )
430
+
431
+ >>> SvnURL.rule_map.register(MozillaRule)
432
+
433
+ >>> SvnURL.is_valid(
434
+ ... url='svn@svn.mozilla.org:mozilla-central/image', is_explicit=True
435
+ ... )
436
+ True
437
+
438
+ >>> SvnURL(url='svn@svn.mozilla.org:mozilla-central/image').rule
439
+ 'mozilla-rule'
440
+
441
+ This is just us cleaning up:
442
+
443
+ >>> SvnURL.rule_map.unregister('mozilla-rule')
444
+
445
+ >>> SvnURL(url='svn@svn.mozilla.org:mozilla-central/mozilla-rule').rule
446
+ 'core-svn-scp'
447
+ """
448
+ return super ().is_valid (url = url , is_explicit = is_explicit )
449
+
450
+ def to_url (self ) -> str :
451
+ """Return a ``svn(1)``-compatible URL. Can be used with ``svn clone``.
452
+
453
+ Examples
454
+ --------
455
+
456
+ SSH style URL:
457
+
458
+ >>> svn_url = SvnURL(url='svn@svn.mozilla.org:mozilla-central/browser')
459
+
460
+ >>> svn_url.path = 'mozilla-central/gfx'
461
+
462
+ >>> svn_url.to_url()
463
+ 'svn@svn.mozilla.org:mozilla-central/gfx'
464
+
465
+ HTTPs URL:
466
+
467
+ >>> svn_url = SvnURL(url='https://svn.mozilla.org/mozilla-central/memory')
468
+
469
+ >>> svn_url.path = 'mozilla-central/image'
470
+
471
+ >>> svn_url.to_url()
472
+ 'https://svn.mozilla.org/mozilla-central/image'
473
+
474
+ Switch them to svnlab:
475
+
476
+ >>> svn_url.hostname = 'localhost'
477
+ >>> svn_url.scheme = 'http'
478
+
479
+ >>> svn_url.to_url()
480
+ 'http://localhost/mozilla-central/image'
481
+
482
+ Pip style URL, thanks to this class implementing :class:`SvnPipURL`:
483
+
484
+ >>> svn_url = SvnURL(url='svn+ssh://svn@svn.mozilla.org/mozilla-central/image')
485
+
486
+ >>> svn_url.hostname = 'localhost'
487
+
488
+ >>> svn_url.to_url()
489
+ 'svn+ssh://svn@localhost/mozilla-central/image'
490
+
491
+ >>> svn_url.user = None
492
+
493
+ >>> svn_url.to_url()
494
+ 'svn+ssh://localhost/mozilla-central/image'
495
+
496
+ See also
497
+ --------
498
+
499
+ :meth:`SvnBaseURL.to_url`, :meth:`SvnPipURL.to_url`
500
+ """
501
+ return super ().to_url ()
0 commit comments