2323from typing import Optional
2424
2525from libvcs ._internal .dataclasses import SkipDefaultFieldsReprMixin
26+ from libvcs .url .git import RE_PIP_REV , SCP_REGEX
2627
2728from .base import Rule , RuleMap , URLProtocol
2829
2930RE_PATH = r"""
30- ((?P<user>.* )@)?
31- (?P<hostname>([^/:]+))
31+ ((?P<user>[^/:@]+ )@)?
32+ (?P<hostname>([^/:@ ]+))
3233 (:(?P<port>\d{1,5}))?
33- (?P<separator>/ )?
34+ (?P<separator>[:,/] )?
3435 (?P<path>
35- (\w[^:.]*)
36+ (\w[^:.@ ]*)
3637 )?
3738"""
3839
5657 ^{ RE_SCHEME }
5758 ://
5859 { RE_PATH }
60+ { RE_PIP_REV } ?
5961 """ ,
6062 re .VERBOSE ,
6163 ),
6264 ),
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@
6379]
6480"""Core regular expressions. These are patterns understood by ``svn(1)``"""
6581
7288 (
7389 svn\+ssh|
7490 svn\+https|
75- svn\+http|
76- svn\+file
91+ svn\+http
7792 )
7893 )
7994"""
8499 description = "pip-style svn URL" ,
85100 pattern = re .compile (
86101 rf"""
87- { RE_PIP_SCHEME }
102+ ^ { RE_PIP_SCHEME }
88103 ://
89104 { RE_PATH }
105+ { RE_PIP_REV } ?
90106 """ ,
91107 re .VERBOSE ,
92108 ),
109+ is_explicit = True ,
93110 ),
94111 # file://, RTC 8089, File:// https://datatracker.ietf.org/doc/html/rfc8089
95112 Rule (
102119 """ ,
103120 re .VERBOSE ,
104121 ),
122+ is_explicit = True ,
105123 ),
106124]
107125"""pip-style svn URLs.
125143
126144
127145@dataclasses .dataclass (repr = False )
128- class SvnURL (URLProtocol , SkipDefaultFieldsReprMixin ):
146+ class SvnBaseURL (URLProtocol , SkipDefaultFieldsReprMixin ):
129147 """SVN repository location. Parses URLs on initialization.
130148
131149 Examples
132150 --------
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,
135154 scheme=svn+ssh,
136155 hostname=svn.debian.org,
137156 path=svn/aliothproj/path/in/project/repository,
138157 rule=core-svn)
139158
140- >>> myrepo = SvnURL (
159+ >>> myrepo = SvnBaseURL (
141160 ... url='svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository'
142161 ... )
143162
@@ -147,8 +166,8 @@ class SvnURL(URLProtocol, SkipDefaultFieldsReprMixin):
147166 >>> myrepo.path
148167 'svn/aliothproj/path/in/project/repository'
149168
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()`
152171
153172 Attributes
154173 ----------
@@ -194,12 +213,12 @@ def is_valid(cls, url: str, is_explicit: Optional[bool] = None) -> bool:
194213 Examples
195214 --------
196215
197- >>> SvnURL .is_valid(
216+ >>> SvnBaseURL .is_valid(
198217 ... url='svn+ssh://svn.debian.org/svn/aliothproj/path/in/project/repository'
199218 ... )
200219 True
201220
202- >>> SvnURL .is_valid(url='notaurl')
221+ >>> SvnBaseURL .is_valid(url='notaurl')
203222 False
204223 """
205224 if is_explicit is not None :
@@ -216,12 +235,12 @@ def to_url(self) -> str:
216235 Examples
217236 --------
218237
219- >>> svn_url = SvnURL (
238+ >>> svn_url = SvnBaseURL (
220239 ... url='svn+ssh://my-username@my-server/vcs-python/libvcs'
221240 ... )
222241
223242 >>> 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,
225244 scheme=svn+ssh,
226245 user=my-username,
227246 hostname=my-server,
@@ -242,15 +261,241 @@ def to_url(self) -> str:
242261 >>> svn_url.to_url()
243262 'svn+ssh://tom@my-server/vcs-python/vcspull'
244263 """
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 , "://" ]
248266
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 ]
250272
251273 if self .port is not None :
252274 parts .extend ([":" , f"{ self .port } " ])
253275
254276 parts .extend ([self .separator , self .path ])
255277
256278 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