mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2025-02-01 15:55:28 +00:00
Revamp header components in language
This commit is contained in:
parent
fffee660e5
commit
24437ba180
@ -4,7 +4,6 @@ import os
|
|||||||
import copy
|
import copy
|
||||||
import abc
|
import abc
|
||||||
import contrib.pyparsing as pp
|
import contrib.pyparsing as pp
|
||||||
from netlib import http_uastrings
|
|
||||||
|
|
||||||
from .. import utils
|
from .. import utils
|
||||||
from . import generators, exceptions
|
from . import generators, exceptions
|
||||||
@ -234,91 +233,29 @@ class _Component(_Token):
|
|||||||
return "".join(i[:] for i in self.values(settings or {}))
|
return "".join(i[:] for i in self.values(settings or {}))
|
||||||
|
|
||||||
|
|
||||||
class _Header(_Component):
|
class KeyValue(_Component):
|
||||||
|
"""
|
||||||
|
A key/value pair.
|
||||||
|
klass.preamble: leader
|
||||||
|
"""
|
||||||
def __init__(self, key, value):
|
def __init__(self, key, value):
|
||||||
self.key, self.value = key, value
|
self.key, self.value = key, value
|
||||||
|
|
||||||
def values(self, settings):
|
|
||||||
return [
|
|
||||||
self.key.get_generator(settings),
|
|
||||||
": ",
|
|
||||||
self.value.get_generator(settings),
|
|
||||||
"\r\n",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Header(_Header):
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def expr(klass):
|
def expr(klass):
|
||||||
e = pp.Literal("h").suppress()
|
e = pp.Literal(klass.preamble).suppress()
|
||||||
e += Value
|
e += Value
|
||||||
e += pp.Literal("=").suppress()
|
e += pp.Literal("=").suppress()
|
||||||
e += Value
|
e += Value
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
return e.setParseAction(lambda x: klass(*x))
|
||||||
|
|
||||||
def spec(self):
|
def spec(self):
|
||||||
return "h%s=%s"%(self.key.spec(), self.value.spec())
|
return "%s%s=%s"%(self.preamble, self.key.spec(), self.value.spec())
|
||||||
|
|
||||||
def freeze(self, settings):
|
def freeze(self, settings):
|
||||||
return Header(self.key.freeze(settings), self.value.freeze(settings))
|
return self.__class__(
|
||||||
|
self.key.freeze(settings), self.value.freeze(settings)
|
||||||
|
|
||||||
class ShortcutContentType(_Header):
|
|
||||||
def __init__(self, value):
|
|
||||||
_Header.__init__(self, ValueLiteral("Content-Type"), value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def expr(klass):
|
|
||||||
e = pp.Literal("c").suppress()
|
|
||||||
e = e + Value
|
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
|
||||||
|
|
||||||
def spec(self):
|
|
||||||
return "c%s"%(self.value.spec())
|
|
||||||
|
|
||||||
def freeze(self, settings):
|
|
||||||
return ShortcutContentType(self.value.freeze(settings))
|
|
||||||
|
|
||||||
|
|
||||||
class ShortcutLocation(_Header):
|
|
||||||
def __init__(self, value):
|
|
||||||
_Header.__init__(self, ValueLiteral("Location"), value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def expr(klass):
|
|
||||||
e = pp.Literal("l").suppress()
|
|
||||||
e = e + Value
|
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
|
||||||
|
|
||||||
def spec(self):
|
|
||||||
return "l%s"%(self.value.spec())
|
|
||||||
|
|
||||||
def freeze(self, settings):
|
|
||||||
return ShortcutLocation(self.value.freeze(settings))
|
|
||||||
|
|
||||||
|
|
||||||
class ShortcutUserAgent(_Header):
|
|
||||||
def __init__(self, value):
|
|
||||||
self.specvalue = value
|
|
||||||
if isinstance(value, basestring):
|
|
||||||
value = ValueLiteral(http_uastrings.get_by_shortcut(value)[2])
|
|
||||||
_Header.__init__(self, ValueLiteral("User-Agent"), value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def expr(klass):
|
|
||||||
e = pp.Literal("u").suppress()
|
|
||||||
u = reduce(
|
|
||||||
operator.or_,
|
|
||||||
[pp.Literal(i[1]) for i in http_uastrings.UASTRINGS]
|
|
||||||
)
|
)
|
||||||
e += u | Value
|
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
|
||||||
|
|
||||||
def spec(self):
|
|
||||||
return "u%s"%self.specvalue
|
|
||||||
|
|
||||||
def freeze(self, settings):
|
|
||||||
return ShortcutUserAgent(self.value.freeze(settings))
|
|
||||||
|
|
||||||
|
|
||||||
class PathodSpec(_Token):
|
class PathodSpec(_Token):
|
||||||
@ -407,12 +344,15 @@ class OptionsOrValue(_Component):
|
|||||||
"""
|
"""
|
||||||
Can be any of a specified set of options, or a value specifier.
|
Can be any of a specified set of options, or a value specifier.
|
||||||
"""
|
"""
|
||||||
|
preamble = ""
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
# If it's a string, we were passed one of the options, so we upper-case
|
# If it's a string, we were passed one of the options, so we upper-case
|
||||||
# it to be canonical. The user can specify a different case by using a
|
# it to be canonical. The user can specify a different case by using a
|
||||||
# string value literal.
|
# string value literal.
|
||||||
|
self.option_used = False
|
||||||
if isinstance(value, basestring):
|
if isinstance(value, basestring):
|
||||||
value = ValueLiteral(value.upper())
|
value = ValueLiteral(value.upper())
|
||||||
|
self.option_used = True
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -421,6 +361,8 @@ class OptionsOrValue(_Component):
|
|||||||
m = pp.MatchFirst(parts)
|
m = pp.MatchFirst(parts)
|
||||||
spec = m | Value.copy()
|
spec = m | Value.copy()
|
||||||
spec = spec.setParseAction(lambda x: klass(*x))
|
spec = spec.setParseAction(lambda x: klass(*x))
|
||||||
|
if klass.preamble:
|
||||||
|
spec = pp.Literal(klass.preamble).suppress() + spec
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
def values(self, settings):
|
def values(self, settings):
|
||||||
@ -432,7 +374,7 @@ class OptionsOrValue(_Component):
|
|||||||
s = self.value.spec()
|
s = self.value.spec()
|
||||||
if s[1:-1].lower() in self.options:
|
if s[1:-1].lower() in self.options:
|
||||||
s = s[1:-1].lower()
|
s = s[1:-1].lower()
|
||||||
return "%s"%s
|
return "%s%s"%(self.preamble, s)
|
||||||
|
|
||||||
def freeze(self, settings):
|
def freeze(self, settings):
|
||||||
return self.__class__(self.value.freeze(settings))
|
return self.__class__(self.value.freeze(settings))
|
||||||
@ -617,10 +559,6 @@ class _Message(object):
|
|||||||
def actions(self):
|
def actions(self):
|
||||||
return self.toks(_Action)
|
return self.toks(_Action)
|
||||||
|
|
||||||
@property
|
|
||||||
def headers(self):
|
|
||||||
return self.toks(_Header)
|
|
||||||
|
|
||||||
def length(self, settings):
|
def length(self, settings):
|
||||||
"""
|
"""
|
||||||
Calculate the length of the base message without any applied
|
Calculate the length of the base message without any applied
|
||||||
|
@ -4,7 +4,7 @@ import abc
|
|||||||
import contrib.pyparsing as pp
|
import contrib.pyparsing as pp
|
||||||
|
|
||||||
import netlib.websockets
|
import netlib.websockets
|
||||||
from netlib import http_status
|
from netlib import http_status, http_uastrings
|
||||||
from . import base, generators, exceptions
|
from . import base, generators, exceptions
|
||||||
|
|
||||||
|
|
||||||
@ -45,6 +45,49 @@ class Method(base.OptionsOrValue):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _HeaderMixin(object):
|
||||||
|
def format_header(self, key, value):
|
||||||
|
return [key, ": ", value, "\r\n"]
|
||||||
|
|
||||||
|
def values(self, settings):
|
||||||
|
return self.format_header(
|
||||||
|
self.key.get_generator(settings),
|
||||||
|
self.value.get_generator(settings),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Header(_HeaderMixin, base.KeyValue):
|
||||||
|
preamble = "h"
|
||||||
|
|
||||||
|
|
||||||
|
class ShortcutContentType(_HeaderMixin, base.PreValue):
|
||||||
|
preamble = "c"
|
||||||
|
key = base.ValueLiteral("Content-Type")
|
||||||
|
|
||||||
|
|
||||||
|
class ShortcutLocation(_HeaderMixin, base.PreValue):
|
||||||
|
preamble = "l"
|
||||||
|
key = base.ValueLiteral("Location")
|
||||||
|
|
||||||
|
|
||||||
|
class ShortcutUserAgent(_HeaderMixin, base.OptionsOrValue):
|
||||||
|
preamble = "u"
|
||||||
|
options = [i[1] for i in http_uastrings.UASTRINGS]
|
||||||
|
key = base.ValueLiteral("User-Agent")
|
||||||
|
|
||||||
|
def values(self, settings):
|
||||||
|
if self.option_used:
|
||||||
|
value = http_uastrings.get_by_shortcut(
|
||||||
|
self.value.val.lower()
|
||||||
|
)[2]
|
||||||
|
else:
|
||||||
|
value = self.value
|
||||||
|
return self.format_header(
|
||||||
|
self.key.get_generator(settings),
|
||||||
|
value
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_header(val, headers):
|
def get_header(val, headers):
|
||||||
"""
|
"""
|
||||||
Header keys may be Values, so we have to "generate" them as we try the
|
Header keys may be Values, so we have to "generate" them as we try the
|
||||||
@ -72,6 +115,10 @@ class _HTTPMessage(base._Message):
|
|||||||
def preamble(self, settings): # pragma: no cover
|
def preamble(self, settings): # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
return self.toks(_HeaderMixin)
|
||||||
|
|
||||||
def values(self, settings):
|
def values(self, settings):
|
||||||
vals = self.preamble(settings)
|
vals = self.preamble(settings)
|
||||||
vals.append("\r\n")
|
vals.append("\r\n")
|
||||||
@ -86,12 +133,12 @@ class _HTTPMessage(base._Message):
|
|||||||
class Response(_HTTPMessage):
|
class Response(_HTTPMessage):
|
||||||
comps = (
|
comps = (
|
||||||
Body,
|
Body,
|
||||||
base.Header,
|
Header,
|
||||||
base.PauseAt,
|
base.PauseAt,
|
||||||
base.DisconnectAt,
|
base.DisconnectAt,
|
||||||
base.InjectAt,
|
base.InjectAt,
|
||||||
base.ShortcutContentType,
|
ShortcutContentType,
|
||||||
base.ShortcutLocation,
|
ShortcutLocation,
|
||||||
Raw,
|
Raw,
|
||||||
Reason
|
Reason
|
||||||
)
|
)
|
||||||
@ -145,7 +192,7 @@ class Response(_HTTPMessage):
|
|||||||
for i in hdrs.lst:
|
for i in hdrs.lst:
|
||||||
if not get_header(i[0], self.headers):
|
if not get_header(i[0], self.headers):
|
||||||
tokens.append(
|
tokens.append(
|
||||||
base.Header(
|
Header(
|
||||||
base.ValueLiteral(i[0]),
|
base.ValueLiteral(i[0]),
|
||||||
base.ValueLiteral(i[1]))
|
base.ValueLiteral(i[1]))
|
||||||
)
|
)
|
||||||
@ -156,7 +203,7 @@ class Response(_HTTPMessage):
|
|||||||
else:
|
else:
|
||||||
length = len(self.body.value.get_generator(settings))
|
length = len(self.body.value.get_generator(settings))
|
||||||
tokens.append(
|
tokens.append(
|
||||||
base.Header(
|
Header(
|
||||||
base.ValueLiteral("Content-Length"),
|
base.ValueLiteral("Content-Length"),
|
||||||
base.ValueLiteral(str(length)),
|
base.ValueLiteral(str(length)),
|
||||||
)
|
)
|
||||||
@ -193,12 +240,12 @@ class Response(_HTTPMessage):
|
|||||||
class Request(_HTTPMessage):
|
class Request(_HTTPMessage):
|
||||||
comps = (
|
comps = (
|
||||||
Body,
|
Body,
|
||||||
base.Header,
|
Header,
|
||||||
base.PauseAt,
|
base.PauseAt,
|
||||||
base.DisconnectAt,
|
base.DisconnectAt,
|
||||||
base.InjectAt,
|
base.InjectAt,
|
||||||
base.ShortcutContentType,
|
ShortcutContentType,
|
||||||
base.ShortcutUserAgent,
|
ShortcutUserAgent,
|
||||||
Raw,
|
Raw,
|
||||||
base.PathodSpec,
|
base.PathodSpec,
|
||||||
)
|
)
|
||||||
@ -241,7 +288,7 @@ class Request(_HTTPMessage):
|
|||||||
for i in netlib.websockets.client_handshake_headers().lst:
|
for i in netlib.websockets.client_handshake_headers().lst:
|
||||||
if not get_header(i[0], self.headers):
|
if not get_header(i[0], self.headers):
|
||||||
tokens.append(
|
tokens.append(
|
||||||
base.Header(
|
Header(
|
||||||
base.ValueLiteral(i[0]),
|
base.ValueLiteral(i[0]),
|
||||||
base.ValueLiteral(i[1])
|
base.ValueLiteral(i[1])
|
||||||
)
|
)
|
||||||
@ -251,7 +298,7 @@ class Request(_HTTPMessage):
|
|||||||
if self.body:
|
if self.body:
|
||||||
length = len(self.body.value.get_generator(settings))
|
length = len(self.body.value.get_generator(settings))
|
||||||
tokens.append(
|
tokens.append(
|
||||||
base.Header(
|
Header(
|
||||||
base.ValueLiteral("Content-Length"),
|
base.ValueLiteral("Content-Length"),
|
||||||
base.ValueLiteral(str(length)),
|
base.ValueLiteral(str(length)),
|
||||||
)
|
)
|
||||||
@ -259,7 +306,7 @@ class Request(_HTTPMessage):
|
|||||||
if settings.request_host:
|
if settings.request_host:
|
||||||
if not get_header("Host", self.headers):
|
if not get_header("Host", self.headers):
|
||||||
tokens.append(
|
tokens.append(
|
||||||
base.Header(
|
Header(
|
||||||
base.ValueLiteral("Host"),
|
base.ValueLiteral("Host"),
|
||||||
base.ValueLiteral(settings.request_host)
|
base.ValueLiteral(settings.request_host)
|
||||||
)
|
)
|
||||||
@ -302,7 +349,7 @@ class PathodErrorResponse(Response):
|
|||||||
def make_error_response(reason, body=None):
|
def make_error_response(reason, body=None):
|
||||||
tokens = [
|
tokens = [
|
||||||
Code("800"),
|
Code("800"),
|
||||||
base.Header(
|
Header(
|
||||||
base.ValueLiteral("Content-Type"),
|
base.ValueLiteral("Content-Type"),
|
||||||
base.ValueLiteral("text/plain")
|
base.ValueLiteral("text/plain")
|
||||||
),
|
),
|
||||||
|
@ -225,9 +225,20 @@ class TestMisc:
|
|||||||
assert v2.value.val == v3.value.val
|
assert v2.value.val == v3.value.val
|
||||||
|
|
||||||
|
|
||||||
class TestHeaders:
|
class TKeyValue(base.KeyValue):
|
||||||
def test_header(self):
|
preamble = "h"
|
||||||
e = base.Header.expr()
|
def values(self, settings):
|
||||||
|
return [
|
||||||
|
self.key.get_generator(settings),
|
||||||
|
": ",
|
||||||
|
self.value.get_generator(settings),
|
||||||
|
"\r\n",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestKeyValue:
|
||||||
|
def test_simple(self):
|
||||||
|
e = TKeyValue.expr()
|
||||||
v = e.parseString("h'foo'='bar'")[0]
|
v = e.parseString("h'foo'='bar'")[0]
|
||||||
assert v.key.val == "foo"
|
assert v.key.val == "foo"
|
||||||
assert v.value.val == "bar"
|
assert v.value.val == "bar"
|
||||||
@ -239,69 +250,14 @@ class TestHeaders:
|
|||||||
s = v.spec()
|
s = v.spec()
|
||||||
assert s == e.parseString(s)[0].spec()
|
assert s == e.parseString(s)[0].spec()
|
||||||
|
|
||||||
def test_header_freeze(self):
|
def test_freeze(self):
|
||||||
e = base.Header.expr()
|
e = TKeyValue.expr()
|
||||||
v = e.parseString("h@10=@10'")[0]
|
v = e.parseString("h@10=@10'")[0]
|
||||||
v2 = v.freeze({})
|
v2 = v.freeze({})
|
||||||
v3 = v2.freeze({})
|
v3 = v2.freeze({})
|
||||||
assert v2.key.val == v3.key.val
|
assert v2.key.val == v3.key.val
|
||||||
assert v2.value.val == v3.value.val
|
assert v2.value.val == v3.value.val
|
||||||
|
|
||||||
def test_ctype_shortcut(self):
|
|
||||||
e = base.ShortcutContentType.expr()
|
|
||||||
v = e.parseString("c'foo'")[0]
|
|
||||||
assert v.key.val == "Content-Type"
|
|
||||||
assert v.value.val == "foo"
|
|
||||||
|
|
||||||
s = v.spec()
|
|
||||||
assert s == e.parseString(s)[0].spec()
|
|
||||||
|
|
||||||
e = base.ShortcutContentType.expr()
|
|
||||||
v = e.parseString("c@100")[0]
|
|
||||||
v2 = v.freeze({})
|
|
||||||
v3 = v2.freeze({})
|
|
||||||
assert v2.value.val == v3.value.val
|
|
||||||
|
|
||||||
def test_location_shortcut(self):
|
|
||||||
e = base.ShortcutLocation.expr()
|
|
||||||
v = e.parseString("l'foo'")[0]
|
|
||||||
assert v.key.val == "Location"
|
|
||||||
assert v.value.val == "foo"
|
|
||||||
|
|
||||||
s = v.spec()
|
|
||||||
assert s == e.parseString(s)[0].spec()
|
|
||||||
|
|
||||||
e = base.ShortcutLocation.expr()
|
|
||||||
v = e.parseString("l@100")[0]
|
|
||||||
v2 = v.freeze({})
|
|
||||||
v3 = v2.freeze({})
|
|
||||||
assert v2.value.val == v3.value.val
|
|
||||||
|
|
||||||
def test_shortcuts(self):
|
|
||||||
assert language.parse_response("400:c'foo'").headers[0].key.val == "Content-Type"
|
|
||||||
assert language.parse_response("400:l'foo'").headers[0].key.val == "Location"
|
|
||||||
|
|
||||||
assert 'Android' in parse_request("get:/:ua").headers[0].value.val
|
|
||||||
assert parse_request("get:/:ua").headers[0].key.val == "User-Agent"
|
|
||||||
|
|
||||||
|
|
||||||
class TestShortcutUserAgent:
|
|
||||||
def test_location_shortcut(self):
|
|
||||||
e = base.ShortcutUserAgent.expr()
|
|
||||||
v = e.parseString("ua")[0]
|
|
||||||
assert "Android" in str(v.value)
|
|
||||||
assert v.spec() == "ua"
|
|
||||||
assert v.key.val == "User-Agent"
|
|
||||||
|
|
||||||
v = e.parseString("u'foo'")[0]
|
|
||||||
assert "foo" in str(v.value)
|
|
||||||
assert "foo" in v.spec()
|
|
||||||
|
|
||||||
v = e.parseString("u@100'")[0]
|
|
||||||
assert len(str(v.freeze({}).value)) > 100
|
|
||||||
v2 = v.freeze({})
|
|
||||||
v3 = v2.freeze({})
|
|
||||||
assert v2.value.val == v3.value.val
|
|
||||||
|
|
||||||
|
|
||||||
class Test_Action:
|
class Test_Action:
|
||||||
|
@ -9,6 +9,12 @@ def parse_request(s):
|
|||||||
return language.parse_requests(s)[0]
|
return language.parse_requests(s)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def render(r, settings=language.Settings()):
|
||||||
|
s = cStringIO.StringIO()
|
||||||
|
assert language.serve(r, s, settings)
|
||||||
|
return s.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def test_make_error_response():
|
def test_make_error_response():
|
||||||
d = cStringIO.StringIO()
|
d = cStringIO.StringIO()
|
||||||
s = http.make_error_response("foo")
|
s = http.make_error_response("foo")
|
||||||
@ -258,3 +264,59 @@ class TestResponse:
|
|||||||
tutils.raises("no websocket key", r.resolve, language.Settings())
|
tutils.raises("no websocket key", r.resolve, language.Settings())
|
||||||
res = r.resolve(language.Settings(websocket_key="foo"))
|
res = r.resolve(language.Settings(websocket_key="foo"))
|
||||||
assert res.code.string() == "101"
|
assert res.code.string() == "101"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ctype_shortcut():
|
||||||
|
e = http.ShortcutContentType.expr()
|
||||||
|
v = e.parseString("c'foo'")[0]
|
||||||
|
assert v.key.val == "Content-Type"
|
||||||
|
assert v.value.val == "foo"
|
||||||
|
|
||||||
|
s = v.spec()
|
||||||
|
assert s == e.parseString(s)[0].spec()
|
||||||
|
|
||||||
|
e = http.ShortcutContentType.expr()
|
||||||
|
v = e.parseString("c@100")[0]
|
||||||
|
v2 = v.freeze({})
|
||||||
|
v3 = v2.freeze({})
|
||||||
|
assert v2.value.val == v3.value.val
|
||||||
|
|
||||||
|
|
||||||
|
def test_location_shortcut():
|
||||||
|
e = http.ShortcutLocation.expr()
|
||||||
|
v = e.parseString("l'foo'")[0]
|
||||||
|
assert v.key.val == "Location"
|
||||||
|
assert v.value.val == "foo"
|
||||||
|
|
||||||
|
s = v.spec()
|
||||||
|
assert s == e.parseString(s)[0].spec()
|
||||||
|
|
||||||
|
e = http.ShortcutLocation.expr()
|
||||||
|
v = e.parseString("l@100")[0]
|
||||||
|
v2 = v.freeze({})
|
||||||
|
v3 = v2.freeze({})
|
||||||
|
assert v2.value.val == v3.value.val
|
||||||
|
|
||||||
|
|
||||||
|
def test_shortcuts():
|
||||||
|
assert language.parse_response("400:c'foo'").headers[0].key.val == "Content-Type"
|
||||||
|
assert language.parse_response("400:l'foo'").headers[0].key.val == "Location"
|
||||||
|
|
||||||
|
assert "Android" in render(parse_request("get:/:ua"))
|
||||||
|
assert "User-Agent" in render(parse_request("get:/:ua"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_agent():
|
||||||
|
e = http.ShortcutUserAgent.expr()
|
||||||
|
v = e.parseString("ua")[0]
|
||||||
|
assert "Android" in str(v.values({})[2])
|
||||||
|
|
||||||
|
e = http.ShortcutUserAgent.expr()
|
||||||
|
v = e.parseString("u'a'")[0]
|
||||||
|
assert "Android" not in str(v.values({})[2])
|
||||||
|
|
||||||
|
v = e.parseString("u@100'")[0]
|
||||||
|
assert len(str(v.freeze({}).value)) > 100
|
||||||
|
v2 = v.freeze({})
|
||||||
|
v3 = v2.freeze({})
|
||||||
|
assert v2.value.val == v3.value.val
|
||||||
|
Loading…
Reference in New Issue
Block a user