10000 refactor!(url[svn]): Examples and further parsing tests · vcs-python/libvcs@1921043 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1921043

Browse files
committed
refactor!(url[svn]): Examples and further parsing tests
1 parent 8ca1edd commit 1921043

File tree

2 files changed

+271
-23
lines changed

2 files changed

+271
-23
lines changed

src/libvcs/url/svn.py

Lines changed: 266 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,17 @@
2323
from typing import Optional
2424

2525
from libvcs._internal.dataclasses import SkipDefaultFieldsReprMixin
26+
from libvcs.url.git import RE_PIP_REV, SCP_REGEX
2627

2728
from .base import Rule, RuleMap, URLProtocol
2829

2930
RE_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

@@ -56,10 +57,25 @@
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

@@ -72,8 +88,7 @@
7288
(
7389
svn\+ssh|
7490
svn\+https|
75-
svn\+http|
76-
svn\+file
91+
svn\+http
7792
)
7893
)
7994
"""
@@ -84,12 +99,14 @@
8499
description="pip-style svn URL",
85100
pattern=re.compile(
86101
rf"""
87-
{RE_PIP_SCHEME< 6D4E /span>}
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(
@@ -102,6 +119,7 @@
102119
""",
103120
re.VERBOSE,
104121
),
122+
is_explicit=True,
105123
),
106124
]
107125
"""pip-style svn URLs.
@@ -125,19 +143,20 @@
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

Comments
 (0)
0