Skip to content

Commit

Permalink
new api first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
masklinn committed May 3, 2022
1 parent 7a73e3e commit 45b50f7
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 6 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ on:
jobs:
checks:
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- name: Checkout working copy
uses: actions/checkout@v3
Expand All @@ -21,11 +19,14 @@ jobs:
- name: Install checkers
run: |
python -mpip install --upgrade pip
python -mpip install black flake8
python -mpip install black flake8 mypy types-PyYaml
- name: typechecking
run: mypy --check-untyped-defs --no-implicit-optional ua_parser
- name: flake
run: flake8 .
- name: black
run: black --check --diff --color --quiet .


test:
runs-on: ubuntu-latest
Expand Down
9 changes: 8 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py27, py36, py37, py38, py39, py310, docs, flake8, black
envlist = py27, py36, py37, py38, py39, py310, docs, flake8, black, typecheck
skipsdist = True

[testenv]
Expand Down Expand Up @@ -29,6 +29,13 @@ skip_install = True
deps = black
commands = black --check --diff .

[testenv:typecheck]
skip_install = True
deps =
types-PyYaml
mypy
commands = mypy --check-untyped-defs --no-implicit-optional ua_parser

[flake8]
max_line_length = 88
filename = ua_parser/
200 changes: 200 additions & 0 deletions ua_parser/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,201 @@
import abc
from collections.abc import Iterable
from dataclasses import dataclass
from typing import *
from typing import IO

VERSION = (0, 15, 0)


@dataclass
class UserAgent:
family: str
major: Optional[str] = None
minor: Optional[str] = None
patch: Optional[str] = None

def __str__(self):
"""Replaces PrettyUserAgent"""
...


# or Pattern?
class RegexUserAgentMatcher:
def __call__(self, uas: str) -> Optional[UserAgent]:
...


@dataclass
class OS:
family: str
major: Optional[str] = None
minor: Optional[str] = None
patch: Optional[str] = None
patch_minor: Optional[str] = None

def __str__(self):
"""Replaces PrettyOS"""
...


class RegexOsMatcher:
def __call__(self, uas: str) -> Optional[OS]:
...


@dataclass
class Device:
name: str
brand: Optional[str] = None
model: Optional[str] = None

def __str__(self):
"""Would replace PrettyDevice if that was a thing"""
...


class RegexDeviceMatcher:
def __call__(self, uas: str) -> Optional[Device]:
...


T = TypeVar("T")
# TODO: checking types: https://sobolevn.me/2019/08/testing-mypy-types
Matcher = Callable[[str], Optional[T]]
Matchers = Tuple[
Iterable[Matcher[UserAgent]],
Iterable[Matcher[OS]],
Iterable[Matcher[Device]],
]


@dataclass
class ParseResult:
user_agent: UserAgent
os: OS
device: Device
string: str


@dataclass
class CacheEntry:
string: str
user_agent: Optional[UserAgent] = None
os: Optional[OS] = None
device: Optional[Device] = None

def to_result(self) -> Optional[ParseResult]:
if self.user_agent and self.os and self.device:
return ParseResult(self.user_agent, self.os, self.device, self.string)
return None


# FIXME: should probably be a typing_extensions.Protocol with just
# "lookup", with this as a base helper
class Cache(abc.ABC):
@abc.abstractmethod
def get(self, key) -> Optional[CacheEntry]:
...

@abc.abstractmethod
def store(self, key, CacheEntry) -> CacheEntry:
...

@abc.abstractmethod
def make_room(self):
...

@property
@abc.abstractmethod
def maxsize(self) -> int:
...

@abc.abstractmethod
def __len__(self) -> int:
...

def check_space(self):
if len(self) >= self.maxsize:
self.make_room()

def key(
self, ua: str, args: dict
): # TODO: keys are internal cache stuff, how can they type?
return (ua, tuple(sorted(args.items())))

def lookup(self, ua: str, args: dict) -> CacheEntry:
key = self.key(ua, args)
entry = self.get(key)
if entry:
return entry

self.check_space()
return self.store(key, CacheEntry(string=ua))


class Parser:
def __init__(
self,
*matchers: Tuple[Union[Matchers, str, IO]],
cache: Optional[Cache] = None,
):
ms: Matchers
if not matchers:
from . import _regexes

ms = (
_regexes.USER_AGENT_PARSERS,
_regexes.OS_PARSERS,
_regexes.DEVICE_PARSERS,
)
elif not isinstance(matchers[0], tuple):
ms = Parser.yaml_to_matchers(matchers)
else:
ms = cast(Matchers, matchers[0])

self._ua_matchers, self._os_matchers, self._device_matchers = ms
self.cache = (
cache or ...
) # should default to something similar to current, possibly via function

@staticmethod
def yaml_to_matchers(yaml_file: Union[str, IO]) -> "Matchers":
...

def __call__(self, uas: str) -> ParseResult:
...

def parse_user_agent(self, uas: str) -> UserAgent:
...

def parse_os(self, uas: str) -> OS:
...

def parse_device(self, uas: str) -> Device:
...


_parser: Optional[Parser] = None
# can be used to force the init of the default parser eagerly
def get_parser() -> Parser:
global _parser
if _parser is None:
_parser = Parser()

return _parser


def parse(uas: str) -> ParseResult:
return get_parser()(uas)


def parse_user_agent(uas: str) -> UserAgent:
return get_parser().parse_user_agent(uas)


def parse_os(uas: str) -> OS:
return get_parser().parse_os(uas)


def parse_device(uas: str) -> Device:
return get_parser().parse_device(uas)
11 changes: 10 additions & 1 deletion ua_parser/user_agent_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def Parse(self, user_agent_string):

return family, v1, v2, v3

def __call__(self, uas):
return None


class OSParser(object):
def __init__(
Expand Down Expand Up @@ -146,6 +149,9 @@ def Parse(self, user_agent_string):

return os, os_v1, os_v2, os_v3, os_v4

def __call__(self, uas):
return None


def MultiReplace(string, match):
def _repl(m):
Expand Down Expand Up @@ -214,6 +220,9 @@ def Parse(self, user_agent_string):

return device, brand, model

def __call__(self, uas):
return None


MAX_CACHE_SIZE = 200
_PARSE_CACHE = {}
Expand Down Expand Up @@ -513,7 +522,7 @@ def GetFilters(
# pyyaml doesn't do it by default (yaml/pyyaml#436)
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from yaml import SafeLoader # type: ignore

with open(UA_PARSER_YAML, "rb") as fp:
regexes = yaml.load(fp, Loader=SafeLoader)
Expand Down
2 changes: 1 addition & 1 deletion ua_parser/user_agent_parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
# Try and use libyaml bindings if available since faster
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from yaml import SafeLoader # type: ignore

from ua_parser import user_agent_parser

Expand Down

0 comments on commit 45b50f7

Please sign in to comment.