Skip to content

Commit

Permalink
Have the names of matcher attributes match regexes.yaml
Browse files Browse the repository at this point in the history
It's kinda dumb that I didn't do that in the first place and I'm not
entirely sure why I missed it...

Anyway there is no reason to make `pattern` be the string pattern and
`regex` be the compiled pattern, that unnecessarily diverges from the
regexes.yaml naming for the corresponding attribute which is a shame,
and the compiled regex in python is... `re.Pattern`, so it makes more
sense on both axis to have `pattern: re.Pattern[str]` and `regex:
str`.

Also add a `regex_flag` attribute/property on the device matchers for
the string version of the flags.

With that, maybe we could build the matchers straight from the source
records by direct unpacking, as well as simplify the codegen through
the magic of doing less? This probably requires benching to see if

     foo(**{'foo': 'bar'})

has the same efficiency as

    foo(foo=bar)

if it does, then there's no reason to bother with reformatting parser
records to the current

    foo(bar)
  • Loading branch information
masklinn committed Jul 13, 2024
1 parent bb74478 commit 0e6a587
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 45 deletions.
2 changes: 1 addition & 1 deletion src/ua_parser/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def __call__(self, ua: str) -> Optional[T]:

@property
@abc.abstractmethod
def pattern(self) -> str:
def regex(self) -> str:
"""Returns the matcher's pattern."""
...

Expand Down
46 changes: 25 additions & 21 deletions src/ua_parser/lazy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class UserAgentMatcher(Matcher[UserAgent]):
"""

pattern: str = ""
regex: str = ""
family: str
major: Optional[str]
minor: Optional[str]
Expand All @@ -33,15 +33,15 @@ def __init__(
patch: Optional[str] = None,
patch_minor: Optional[str] = None,
) -> None:
self.pattern = regex
self.regex = regex
self.family = family or "$1"
self.major = major
self.minor = minor
self.patch = patch
self.patch_minor = patch_minor

def __call__(self, ua: str) -> Optional[UserAgent]:
if m := self.regex.search(ua):
if m := self.pattern.search(ua):
return UserAgent(
family=(
self.family.replace("$1", m[1])
Expand All @@ -56,8 +56,8 @@ def __call__(self, ua: str) -> Optional[UserAgent]:
return None

@cached_property
def regex(self) -> Pattern[str]:
return re.compile(self.pattern)
def pattern(self) -> Pattern[str]:
return re.compile(self.regex)

def __repr__(self) -> str:
fields = [
Expand All @@ -69,7 +69,7 @@ def __repr__(self) -> str:
]
args = "".join(f", {k}={v!r}" for k, v in fields if v is not None)

return f"UserAgentMatcher({self.pattern!r}{args})"
return f"UserAgentMatcher({self.regex!r}{args})"


class OSMatcher(Matcher[OS]):
Expand All @@ -81,7 +81,7 @@ class OSMatcher(Matcher[OS]):
"""

pattern: str = ""
regex: str = ""
family: str
major: str
minor: str
Expand All @@ -97,15 +97,15 @@ def __init__(
patch: Optional[str] = None,
patch_minor: Optional[str] = None,
) -> None:
self.pattern = regex
self.regex = regex
self.family = family or "$1"
self.major = major or "$2"
self.minor = minor or "$3"
self.patch = patch or "$4"
self.patch_minor = patch_minor or "$5"

def __call__(self, ua: str) -> Optional[OS]:
if m := self.regex.search(ua):
if m := self.pattern.search(ua):
family = replacer(self.family, m)
if family is None:
raise ValueError(f"Unable to find OS family in {ua}")
Expand All @@ -119,8 +119,8 @@ def __call__(self, ua: str) -> Optional[OS]:
return None

@cached_property
def regex(self) -> Pattern[str]:
return re.compile(self.pattern)
def pattern(self) -> Pattern[str]:
return re.compile(self.regex)

def __repr__(self) -> str:
fields = [
Expand All @@ -132,7 +132,7 @@ def __repr__(self) -> str:
]
args = "".join(f", {k}={v!r}" for k, v in fields if v is not None)

return f"OSMatcher({self.pattern!r}{args})"
return f"OSMatcher({self.regex!r}{args})"


class DeviceMatcher(Matcher[Device]):
Expand All @@ -144,8 +144,8 @@ class DeviceMatcher(Matcher[Device]):
"""

pattern: str = ""
flags: int = 0
regex: str = ""
regex_flag: Optional[Literal["i"]] = None
family: str
brand: str
model: str
Expand All @@ -158,14 +158,14 @@ def __init__(
brand: Optional[str] = None,
model: Optional[str] = None,
) -> None:
self.pattern = regex
self.flags = re.IGNORECASE if regex_flag == "i" else 0
self.regex = regex
self.regex_flag = regex_flag
self.family = family or "$1"
self.brand = brand or ""
self.model = model or "$1"

def __call__(self, ua: str) -> Optional[Device]:
if m := self.regex.search(ua):
if m := self.pattern.search(ua):
family = replacer(self.family, m)
if family is None:
raise ValueError(f"Unable to find device family in {ua}")
Expand All @@ -176,17 +176,21 @@ def __call__(self, ua: str) -> Optional[Device]:
)
return None

@property
def flags(self) -> int:
return re.IGNORECASE if self.regex_flag == "i" else 0

@cached_property
def regex(self) -> Pattern[str]:
return re.compile(self.pattern, flags=self.flags)
def pattern(self) -> Pattern[str]:
return re.compile(self.regex, flags=self.flags)

def __repr__(self) -> str:
fields = [
("family", self.family if self.family != "$1" else None),
("brand", self.brand or None),
("model", self.model if self.model != "$1" else None),
]
iflag = ', "i"' if self.flags & re.IGNORECASE else ""
iflag = ', "i"' if self.regex_flag else ""
args = iflag + "".join(f", {k}={v!r}" for k, v in fields if v is not None)

return f"DeviceMatcher({self.pattern!r}{args})"
return f"DeviceMatcher({self.regex!r}{args})"
44 changes: 25 additions & 19 deletions src/ua_parser/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class UserAgentMatcher(Matcher[UserAgent]):
"""

regex: Pattern[str]
pattern: Pattern[str]
family: str
major: Optional[str]
minor: Optional[str]
Expand All @@ -29,15 +29,15 @@ def __init__(
patch: Optional[str] = None,
patch_minor: Optional[str] = None,
) -> None:
self.regex = re.compile(regex)
self.pattern = re.compile(regex)
self.family = family or "$1"
self.major = major
self.minor = minor
self.patch = patch
self.patch_minor = patch_minor

def __call__(self, ua: str) -> Optional[UserAgent]:
if m := self.regex.search(ua):
if m := self.pattern.search(ua):
return UserAgent(
family=(
self.family.replace("$1", m[1])
Expand All @@ -52,8 +52,8 @@ def __call__(self, ua: str) -> Optional[UserAgent]:
return None

@property
def pattern(self) -> str:
return self.regex.pattern
def regex(self) -> str:
return self.pattern.pattern

def __repr__(self) -> str:
fields = [
Expand All @@ -65,7 +65,7 @@ def __repr__(self) -> str:
]
args = "".join(f", {k}={v!r}" for k, v in fields if v is not None)

return f"UserAgentMatcher({self.pattern!r}{args})"
return f"UserAgentMatcher({self.regex!r}{args})"


class OSMatcher(Matcher[OS]):
Expand All @@ -74,7 +74,7 @@ class OSMatcher(Matcher[OS]):
"""

regex: Pattern[str]
pattern: Pattern[str]
family: str
major: str
minor: str
Expand All @@ -90,15 +90,15 @@ def __init__(
patch: Optional[str] = None,
patch_minor: Optional[str] = None,
) -> None:
self.regex = re.compile(regex)
self.pattern = re.compile(regex)
self.family = family or "$1"
self.major = major or "$2"
self.minor = minor or "$3"
self.patch = patch or "$4"
self.patch_minor = patch_minor or "$5"

def __call__(self, ua: str) -> Optional[OS]:
if m := self.regex.search(ua):
if m := self.pattern.search(ua):
family = replacer(self.family, m)
if family is None:
raise ValueError(f"Unable to find OS family in {ua}")
Expand All @@ -112,8 +112,8 @@ def __call__(self, ua: str) -> Optional[OS]:
return None

@property
def pattern(self) -> str:
return self.regex.pattern
def regex(self) -> str:
return self.pattern.pattern

def __repr__(self) -> str:
fields = [
Expand All @@ -125,7 +125,7 @@ def __repr__(self) -> str:
]
args = "".join(f", {k}={v!r}" for k, v in fields if v is not None)

return f"OSMatcher({self.pattern!r}{args})"
return f"OSMatcher({self.regex!r}{args})"


class DeviceMatcher(Matcher[Device]):
Expand All @@ -134,7 +134,7 @@ class DeviceMatcher(Matcher[Device]):
"""

regex: Pattern[str]
pattern: Pattern[str]
family: str
brand: str
model: str
Expand All @@ -147,13 +147,15 @@ def __init__(
brand: Optional[str] = None,
model: Optional[str] = None,
) -> None:
self.regex = re.compile(regex, flags=re.IGNORECASE if regex_flag == "i" else 0)
self.pattern = re.compile(
regex, flags=re.IGNORECASE if regex_flag == "i" else 0
)
self.family = family or "$1"
self.brand = brand or ""
self.model = model or "$1"

def __call__(self, ua: str) -> Optional[Device]:
if m := self.regex.search(ua):
if m := self.pattern.search(ua):
family = replacer(self.family, m)
if family is None:
raise ValueError(f"Unable to find device family in {ua}")
Expand All @@ -165,12 +167,16 @@ def __call__(self, ua: str) -> Optional[Device]:
return None

@property
def pattern(self) -> str:
return self.regex.pattern
def regex(self) -> str:
return self.pattern.pattern

@property
def regex_flag(self) -> str:
return "i" if self.flags & re.IGNORECASE else ""

@property
def flags(self) -> int:
return self.regex.flags
return self.pattern.flags

def __repr__(self) -> str:
fields = [
Expand All @@ -181,4 +187,4 @@ def __repr__(self) -> str:
iflag = ', "i"' if self.flags & re.IGNORECASE else ""
args = iflag + "".join(f", {k}={v!r}" for k, v in fields if v is not None)

return f"DeviceMatcher({self.pattern!r}{args})"
return f"DeviceMatcher({self.regex!r}{args})"
8 changes: 4 additions & 4 deletions src/ua_parser/re2.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ def __init__(
if self.user_agent_matchers:
self.ua = re2.Filter()
for u in self.user_agent_matchers:
self.ua.Add(u.pattern)
self.ua.Add(u.regex)
self.ua.Compile()
else:
self.ua = DummyFilter()

if self.os_matchers:
self.os = re2.Filter()
for o in self.os_matchers:
self.os.Add(o.pattern)
self.os.Add(o.regex)
self.os.Compile()
else:
self.os = DummyFilter()
Expand All @@ -58,9 +58,9 @@ def __init__(
# no pattern uses global flags, but since they're not
# supported in JS that seems safe.
if d.flags & re.IGNORECASE:
self.devices.Add("(?i)" + d.pattern)
self.devices.Add("(?i)" + d.regex)
else:
self.devices.Add(d.pattern)
self.devices.Add(d.regex)
self.devices.Compile()
else:
self.devices = DummyFilter()
Expand Down

0 comments on commit 0e6a587

Please sign in to comment.