Skip to content

Commit 029aa1e

Browse files
committed
refactor: move some of code from version.py to dedicated files
1 parent a0be473 commit 029aa1e

File tree

3 files changed

+104
-85
lines changed

3 files changed

+104
-85
lines changed

version_query/parser.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Functions for parsing version strings into their components and verifying the format."""
2+
3+
import logging
4+
import typing as t
5+
6+
from . import patterns
7+
8+
_LOG = logging.getLogger(__name__)
9+
10+
11+
def parse_release_str(release: str) -> tuple:
12+
"""Parse a release string into major, minor, and patch version numbers."""
13+
match = patterns.RELEASE.fullmatch(release)
14+
assert match is not None
15+
major_match = match.group('major')
16+
assert major_match is not None
17+
major = int(major_match)
18+
minor_match = match.group('minor')
19+
if minor_match is not None:
20+
minor = int(minor_match)
21+
else:
22+
minor = None
23+
patch_match = match.group('patch')
24+
if patch_match is not None:
25+
patch = int(patch_match)
26+
else:
27+
patch = None
28+
return major, minor, patch
29+
30+
31+
def parse_pre_release_str(pre_release: str) -> t.Sequence[
32+
t.Tuple[t.Optional[str], t.Optional[str], t.Optional[int]]]:
33+
"""Parse a pre-release string into a sequence of tuples."""
34+
parts = patterns.PRE_RELEASE.findall(pre_release)
35+
_LOG.debug('parsed pre-release string %s into %s',
36+
repr(pre_release), parts)
37+
tuples = []
38+
for part in parts:
39+
match = patterns.PRE_RELEASE_PART.fullmatch(part)
40+
assert match is not None
41+
pre_patch_match = match.group('prepatch')
42+
if pre_patch_match is not None:
43+
pre_patch = int(pre_patch_match)
44+
else:
45+
pre_patch = None
46+
tuples.append((match.group('preseparator'), match.group('pretype'), pre_patch))
47+
return tuples
48+
49+
50+
def parse_local_str(local: str) -> tuple:
51+
"""Parse a local version suffix string into a sequence."""
52+
match = patterns.LOCAL.fullmatch(local)
53+
assert match is not None
54+
return tuple([_ for _ in match.groups() if _ is not None])

version_query/patterns.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Patterns for recognising parts of version strings and parsing them."""
2+
3+
import re
4+
5+
# pylint: disable = consider-using-f-string
6+
7+
_NUMBER = r'(?:0|[123456789][0123456789]*)'
8+
# _SHA = r'[0123456789abcdef]+'
9+
_LETTERS = r'(?:[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]+)'
10+
LETTERS = re.compile(_LETTERS)
11+
_ALPHANUMERIC = r'(?:[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]+)'
12+
ALPHANUMERIC = re.compile(_ALPHANUMERIC)
13+
_SEP = r'(?:[\.-])'
14+
15+
_RELEASE_PARTS = \
16+
r'(?P<major>{n})(?:\.(?P<minor>{n}))?(?:\.(?P<patch>{n}))?'.format(n=_NUMBER)
17+
RELEASE = re.compile(_RELEASE_PARTS)
18+
19+
_PRE_SEPARATOR = rf'(?P<preseparator>{_SEP})'
20+
_PRE_TYPE = rf'(?P<pretype>{_LETTERS})'
21+
_PRE_PATCH = rf'(?P<prepatch>{_NUMBER})'
22+
_PRE_RELEASE_PART = rf'{_PRE_SEPARATOR}?{_PRE_TYPE}?{_PRE_PATCH}?'
23+
PRE_RELEASE_PART = re.compile(_PRE_RELEASE_PART)
24+
_PRE_RELEASE_PARTS = r'(?:{0}{2})|(?:{0}?{1}{2}?)'.format(_SEP, _LETTERS, _NUMBER)
25+
PRE_RELEASE = re.compile(_PRE_RELEASE_PARTS)
26+
# PRE_RELEASE_CHECK = re.compile(rf'(?:{_PRE_RELEASE_PARTS})+')
27+
28+
_LOCAL_SEPARATOR = rf'({_SEP})'
29+
_LOCAL_PART = rf'({_ALPHANUMERIC})'
30+
_LOCAL_PARTS = rf'\+{_LOCAL_PART}(?:{_LOCAL_SEPARATOR}{_LOCAL_PART})*'
31+
LOCAL = re.compile(_LOCAL_PARTS)
32+
33+
34+
_RELEASE = r'(?P<release>{n}(?:\.{n})?(?:\.{n})?)'.format(n=_NUMBER)
35+
_PRE_RELEASE = r'(?P<prerelease>(?:(?:{0}{2})|(?:{0}?{1}{2}?))+)'.format(_SEP, _LETTERS, _NUMBER)
36+
_LOCAL = r'(?P<local>\+{0}([\.-]{0})*)'.format(_ALPHANUMERIC)
37+
# _NAMED_PARTS_COUNT = 3 + 3
38+
_VERSION = rf'{_RELEASE}{_PRE_RELEASE}?{_LOCAL}?'
39+
VERSION = re.compile(_VERSION)

version_query/version.py

Lines changed: 11 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import enum
55
import itertools
66
import logging
7-
import re
87
import typing as t
98

109
import packaging.version
1110
import semver
1211

12+
from . import patterns
13+
from .parser import parse_release_str, parse_pre_release_str, parse_local_str
14+
1315
_LOG = logging.getLogger(__name__)
1416

1517
PY_PRE_RELEASE_INDICATORS = {'a', 'b', 'c', 'rc'}
@@ -18,6 +20,7 @@
1820
@enum.unique
1921
class VersionComponent(enum.IntEnum):
2022
"""Enumeration of standard version components."""
23+
2124
# pylint: disable = invalid-name
2225

2326
Major = 1 << 1
@@ -39,87 +42,10 @@ class Version(collections.abc.Hashable): # pylint: disable = too-many-public-me
3942
Definitions of acceptable version formats are provided in readme.
4043
"""
4144

42-
_re_number = r'(?:0|[123456789][0123456789]*)'
43-
# _re_sha = r'[0123456789abcdef]+'
44-
_re_letters = r'(?:[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]+)'
45-
_pattern_letters = re.compile(_re_letters)
46-
_re_alphanumeric = r'(?:[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]+)'
47-
_pattern_alphanumeric = re.compile(_re_alphanumeric)
48-
_re_sep = r'(?:[\.-])'
49-
50-
_re_release_parts = \
51-
r'(?P<major>{n})(?:\.(?P<minor>{n}))?(?:\.(?P<patch>{n}))?'.format(n=_re_number)
52-
_pattern_release = re.compile(_re_release_parts)
53-
54-
@classmethod
55-
def _parse_release_str(cls, release: str) -> tuple:
56-
match = cls._pattern_release.fullmatch(release)
57-
assert match is not None
58-
major_match = match.group('major')
59-
assert major_match is not None
60-
major = int(major_match)
61-
minor_match = match.group('minor')
62-
if minor_match is not None:
63-
minor = int(minor_match)
64-
else:
65-
minor = None
66-
patch_match = match.group('patch')
67-
if patch_match is not None:
68-
patch = int(patch_match)
69-
else:
70-
patch = None
71-
return major, minor, patch
72-
73-
_re_pre_separator = rf'(?P<preseparator>{_re_sep})'
74-
_re_pre_type = rf'(?P<pretype>{_re_letters})'
75-
_re_pre_patch = rf'(?P<prepatch>{_re_number})'
76-
_re_pre_release_part = rf'{_re_pre_separator}?{_re_pre_type}?{_re_pre_patch}?'
77-
_pattern_pre_release_part = re.compile(_re_pre_release_part)
78-
_re_pre_release_parts = r'(?:{0}{2})|(?:{0}?{1}{2}?)'.format(_re_sep, _re_letters, _re_number)
79-
_pattern_pre_release = re.compile(_re_pre_release_parts)
80-
_pattern_pre_release_check = re.compile(rf'(?:{_re_pre_release_parts})+')
81-
82-
@classmethod
83-
def _parse_pre_release_str(cls, pre_release: str) -> t.Sequence[
84-
t.Tuple[t.Optional[str], t.Optional[str], t.Optional[int]]]:
85-
parts = cls._pattern_pre_release.findall(pre_release)
86-
_LOG.debug('parsed pre-release string %s into %s',
87-
repr(pre_release), parts)
88-
tuples = []
89-
for part in parts:
90-
match = cls._pattern_pre_release_part.fullmatch(part)
91-
assert match is not None
92-
pre_patch_match = match.group('prepatch')
93-
if pre_patch_match is not None:
94-
pre_patch = int(pre_patch_match)
95-
else:
96-
pre_patch = None
97-
tuples.append((match.group('preseparator'), match.group('pretype'), pre_patch))
98-
return tuples
99-
100-
_re_local_separator = rf'({_re_sep})'
101-
_re_local_part = rf'({_re_alphanumeric})'
102-
_re_local_parts = rf'\+{_re_local_part}(?:{_re_local_separator}{_re_local_part})*'
103-
_pattern_local = re.compile(_re_local_parts)
104-
105-
@classmethod
106-
def _parse_local_str(cls, local: str) -> tuple:
107-
match = cls._pattern_local.fullmatch(local)
108-
assert match is not None
109-
return tuple([_ for _ in match.groups() if _ is not None])
110-
111-
_re_release = r'(?P<release>{n}(?:\.{n})?(?:\.{n})?)'.format(n=_re_number)
112-
_re_pre_release = r'(?P<prerelease>(?:(?:{0}{2})|(?:{0}?{1}{2}?))+)'.format(
113-
_re_sep, _re_letters, _re_number)
114-
_re_local = r'(?P<local>\+{0}([\.-]{0})*)'.format(_re_alphanumeric)
115-
# _re_named_parts_count = 3 + 3
116-
_re_version = rf'{_re_release}{_re_pre_release}?{_re_local}?'
117-
_pattern_version = re.compile(_re_version)
118-
11945
@classmethod
12046
def from_str(cls, version_str: str):
12147
"""Create version from string."""
122-
match = cls._pattern_version.fullmatch(version_str) # type: t.Optional[t.Match[str]]
48+
match = patterns.VERSION.fullmatch(version_str) # type: t.Optional[t.Match[str]]
12349
if match is None:
12450
raise ValueError(f'version string {repr(version_str)} is invalid')
12551
_LOG.debug('version_query parsed version string %s into %s: %s %s',
@@ -129,9 +55,9 @@ def from_str(cls, version_str: str):
12955
_pre_release = match.group('prerelease')
13056
_local = match.group('local')
13157

132-
major, minor, patch = cls._parse_release_str(_release)
133-
pre_release = None if _pre_release is None else cls._parse_pre_release_str(_pre_release)
134-
local = None if _local is None else cls._parse_local_str(_local)
58+
major, minor, patch = parse_release_str(_release)
59+
pre_release = None if _pre_release is None else parse_pre_release_str(_pre_release)
60+
local = None if _local is None else parse_local_str(_local)
13561

13662
return cls(major=major, minor=minor, patch=patch, pre_release=pre_release, local=local)
13763

@@ -196,7 +122,7 @@ def from_sem_version(cls, sem_version: t.Union[dict, semver.VersionInfo]):
196122
if pre_release is not None:
197123
raise NotImplementedError(sem_version)
198124
if local is not None:
199-
local = cls._parse_local_str(f'+{local}')
125+
local = parse_local_str(f'+{local}')
200126
return cls(major, minor, patch, pre_release=pre_release, local=local)
201127

202128
@classmethod
@@ -357,7 +283,7 @@ def _check_pre_release_parts(self, pre_separator, pre_type, pre_patch):
357283
if pre_type is not None and not isinstance(pre_type, str):
358284
raise TypeError(
359285
f'pre_type={repr(pre_type)} is of wrong type {type(pre_type)} in {repr(self)}')
360-
if pre_type is not None and type(self)._pattern_letters.fullmatch(pre_type) is None:
286+
if pre_type is not None and patterns.LETTERS.fullmatch(pre_type) is None:
361287
raise ValueError(f'pre_type={repr(pre_type)} has wrong value in {repr(self)}')
362288
if pre_patch is not None and not isinstance(pre_patch, int):
363289
raise TypeError(
@@ -396,7 +322,7 @@ def local(self, local: t.Optional[t.Sequence[str]]):
396322
raise TypeError(f'local_part or local_separator {repr(part)} is of wrong type'
397323
f' {type(part)} in {repr(self)}')
398324
if i % 2 == 0:
399-
if type(self)._pattern_alphanumeric.fullmatch(part) is None:
325+
if patterns.ALPHANUMERIC.fullmatch(part) is None:
400326
raise ValueError(f'local_part={repr(part)} has wrong value in {repr(self)}')
401327
elif part not in ('-', '.'):
402328
raise ValueError(f'local_separator={repr(part)} has wrong value in {repr(self)}')

0 commit comments

Comments
 (0)