Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-devpi-server for
openSUSE:Factory checked in at 2021-11-17 01:13:42
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-devpi-server (Old)
and /work/SRC/openSUSE:Factory/.python-devpi-server.new.1890 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-devpi-server"
Wed Nov 17 01:13:42 2021 rev:7 rq:931517 version:6.2.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-devpi-server/python-devpi-server.changes
2021-08-03 22:49:32.148439328 +0200
+++
/work/SRC/openSUSE:Factory/.python-devpi-server.new.1890/python-devpi-server.changes
2021-11-17 01:14:40.662181641 +0100
@@ -1,0 +2,11 @@
+Mon Nov 8 11:58:00 UTC 2021 - Dirk M??ller <[email protected]>
+
+- update to 6.2.0:
+ * Optimized some database access patterns. A new index is added to the
+ database on first startup. For large databases that can take a while.
+ * Improved performance of loads from database.
+ * Optimized memory and cache use for database access.
+ * Use frozenset for project name cache of mirror indexes. This mitigates
+ memory fragmentation on some Linux distributions.
+
+-------------------------------------------------------------------
Old:
----
devpi-server-6.1.0.tar.gz
New:
----
devpi-server-6.2.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-devpi-server.spec ++++++
--- /var/tmp/diff_new_pack.2lriwT/_old 2021-11-17 01:14:41.222181851 +0100
+++ /var/tmp/diff_new_pack.2lriwT/_new 2021-11-17 01:14:41.226181853 +0100
@@ -20,7 +20,7 @@
%define commands export fsck gen-config import init passwd server gen-secret
%define skip_python2 1
Name: python-devpi-server
-Version: 6.1.0
+Version: 6.2.0
Release: 0
Summary: Private PyPI caching server
License: MIT
++++++ devpi-server-6.1.0.tar.gz -> devpi-server-6.2.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/CHANGELOG
new/devpi-server-6.2.0/CHANGELOG
--- old/devpi-server-6.1.0/CHANGELOG 2021-07-11 17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/CHANGELOG 2021-08-12 11:05:00.000000000 +0200
@@ -2,6 +2,21 @@
.. towncrier release notes start
+6.2.0 (2021-08-12)
+==================
+
+Bug Fixes
+---------
+
+- Optimized some database access patterns. A new index is added to the
database on first startup. For large databases that can take a while.
+
+- Improved performance of loads from database.
+
+- Optimized memory and cache use for database access.
+
+- Use frozenset for project name cache of mirror indexes. This mitigates
memory fragmentation on some Linux distributions.
+
+
6.1.0 (2021-07-11)
==================
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/PKG-INFO
new/devpi-server-6.2.0/PKG-INFO
--- old/devpi-server-6.1.0/PKG-INFO 2021-07-11 17:55:43.020612000 +0200
+++ new/devpi-server-6.2.0/PKG-INFO 2021-08-12 11:05:02.939989600 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: devpi-server
-Version: 6.1.0
+Version: 6.2.0
Summary: devpi-server: reliable private and pypi.org caching server
Home-page: https://devpi.net
Maintainer: Holger Krekel, Florian Schulze
@@ -102,6 +102,21 @@
.. towncrier release notes start
+6.2.0 (2021-08-12)
+==================
+
+Bug Fixes
+---------
+
+- Optimized some database access patterns. A new index is added to the
database on first startup. For large databases that can take a while.
+
+- Improved performance of loads from database.
+
+- Optimized memory and cache use for database access.
+
+- Use frozenset for project name cache of mirror indexes. This mitigates
memory fragmentation on some Linux distributions.
+
+
6.1.0 (2021-07-11)
==================
@@ -222,13 +237,4 @@
- Pin to pyramid<2.
-5.5.0 (2020-05-04)
-==================
-
-Features
---------
-
-- Proxy requests from replica to master are now streamed if possible. This
improves reliability of large uploads through replicas and reduces RAM usage on
the replica.
-
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/__init__.py
new/devpi-server-6.2.0/devpi_server/__init__.py
--- old/devpi-server-6.1.0/devpi_server/__init__.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/__init__.py 2021-08-12
11:05:00.000000000 +0200
@@ -1 +1 @@
-__version__ = '6.1.0'
+__version__ = '6.2.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/exceptions.py
new/devpi-server-6.2.0/devpi_server/exceptions.py
--- old/devpi-server-6.1.0/devpi_server/exceptions.py 1970-01-01
01:00:00.000000000 +0100
+++ new/devpi-server-6.2.0/devpi_server/exceptions.py 2021-08-12
11:05:00.000000000 +0200
@@ -0,0 +1,33 @@
+import traceback
+
+
+class LazyExceptionFormatter:
+ __slots__ = ('e',)
+
+ def __init__(self, e):
+ self.e = e
+
+ def __str__(self):
+ return "%s:%s:%s %s" % (
+ *traceback.extract_tb(self.e.__traceback__, 2)[-1][:3],
+ ''.join(traceback.format_exception_only(
+ self.e.__class__, self.e)).strip())
+
+
+class LazyExceptionOnlyFormatter:
+ __slots__ = ('e',)
+
+ def __init__(self, e):
+ self.e = e
+
+ def __str__(self):
+ return ''.join(traceback.format_exception_only(
+ self.e.__class__, self.e)).strip()
+
+
+def lazy_format_exception(e):
+ return LazyExceptionFormatter(e)
+
+
+def lazy_format_exception_only(e):
+ return LazyExceptionOnlyFormatter(e)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/extpypi.py
new/devpi-server-6.2.0/devpi_server/extpypi.py
--- old/devpi-server-6.1.0/devpi_server/extpypi.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/extpypi.py 2021-08-12
11:05:00.000000000 +0200
@@ -96,8 +96,8 @@
@property
def releaselinks(self):
- l = map(BasenameMeta, self.basename2link.values())
- return [x.obj for x in l]
+ # the BasenameMeta wrapping essentially does link validation
+ return [BasenameMeta(x).obj for x in self.basename2link.values()]
def parse_index(self, disturl, html):
p = HTMLPage(html, disturl.url)
@@ -366,7 +366,7 @@
self.cache_retrieve_times.refresh(project)
# make project appear in projects list even
# before we next check up the full list with remote
- self.cache_projectnames.get_inplace().add(project)
+ self.cache_projectnames.add(project)
self.keyfs.tx.on_commit_success(on_commit)
@@ -480,7 +480,7 @@
# parse simple index's link
assert response.text is not None, response.text
result = parse_index(response.url, response.text)
- releaselinks = list(result.releaselinks)
+ releaselinks = result.releaselinks
# first we try to process mirror links without an explicit write
transaction.
# if all links already exist in storage we might then return our
already
@@ -600,7 +600,7 @@
""" Helper class for maintaining project names from a mirror. """
def __init__(self):
self._timestamp = -1
- self._data = set()
+ self._data = frozenset()
def exists(self):
return self._timestamp != -1
@@ -610,16 +610,20 @@
def get(self):
""" Get a copy of the cached data. """
- return set(self._data)
-
- def get_inplace(self):
- """ Get cached data in-place. """
return self._data
+ def add(self, project):
+ """ Add project to cache. """
+ self._data = self._data.union({project})
+
+ def discard(self, project):
+ """ Remove project from cache. """
+ self._data = self._data.difference({project})
+
def set(self, data):
""" Set data and update timestamp. """
if data is not self._data:
- self._data = data.copy()
+ self._data = frozenset(data)
self.mark_current()
def mark_current(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/filestore.py
new/devpi-server-6.2.0/devpi_server/filestore.py
--- old/devpi-server-6.1.0/devpi_server/filestore.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/filestore.py 2021-08-12
11:05:00.000000000 +0200
@@ -9,17 +9,12 @@
from wsgiref.handlers import format_date_time
import py
import re
-import sys
from devpi_common.metadata import splitbasename
from devpi_common.types import cached_property, parse_hash_spec
from .log import threadlog
+from urllib.parse import unquote
-if sys.version_info >= (3, 0):
- from urllib.parse import unquote
-else:
- from urllib import unquote
-
_nodefault = object()
@@ -35,6 +30,27 @@
return hash_value[:3], hash_value[3:16]
+def key_from_link(keyfs, link, user, index):
+ if link.hash_spec:
+ # we can only create 32K entries per directory
+ # so let's take the first 3 bytes which gives
+ # us a maximum of 16^3 = 4096 entries in the root dir
+ a, b = make_splitdir(link.hash_spec)
+ return keyfs.STAGEFILE(
+ user=user, index=index,
+ hashdir_a=a, hashdir_b=b,
+ filename=link.basename)
+ else:
+ parts = link.torelpath().split("/")
+ assert parts
+ dirname = "_".join(parts[:-1])
+ dirname = re.sub('[^a-zA-Z0-9_.-]', '_', dirname)
+ return keyfs.PYPIFILE_NOMD5(
+ user=user, index=index,
+ dirname=unquote(dirname),
+ basename=link.basename)
+
+
def unicode_if_bytes(val):
if isinstance(val, py.builtin.bytes):
val = py.builtin._totext(val)
@@ -48,24 +64,7 @@
self.keyfs = keyfs
def maplink(self, link, user, index, project):
- parts = link.torelpath().split("/")
- assert parts
- basename = unquote(parts[-1])
- if link.hash_spec:
- # we can only create 32K entries per directory
- # so let's take the first 3 bytes which gives
- # us a maximum of 16^3 = 4096 entries in the root dir
- a, b = make_splitdir(link.hash_spec)
- key = self.keyfs.STAGEFILE(user=user, index=index,
- hashdir_a=a, hashdir_b=b,
- filename=link.basename)
- else:
- dirname = "_".join(parts[:-1])
- dirname = re.sub('[^a-zA-Z0-9_.-]', '_', dirname)
- key = self.keyfs.PYPIFILE_NOMD5(
- user=user, index=index,
- dirname=unquote(dirname),
- basename=basename)
+ key = key_from_link(self.keyfs, link, user, index)
entry = FileEntry(key, readonly=False)
entry.url = link.geturl_nofragment().url
# verify checksum if the entry is fresh, a file exists
@@ -82,7 +81,7 @@
entry.project = project
version = None
try:
- (projectname, version, ext) = splitbasename(basename)
+ (projectname, version, ext) = splitbasename(link.basename)
except ValueError:
pass
# only store version on entry if we can determine it
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/fileutil.py
new/devpi-server-6.2.0/devpi_server/fileutil.py
--- old/devpi-server-6.1.0/devpi_server/fileutil.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/fileutil.py 2021-08-12
11:05:00.000000000 +0200
@@ -1,8 +1,10 @@
import errno
import os.path
import sys
-from execnet.gateway_base import Unserializer, _Serializer
+from execnet.gateway_base import LoadError, Unserializer, _Serializer
+from functools import partial
from io import BytesIO
+from struct import unpack
_nodefault = object()
@@ -20,9 +22,77 @@
def loads(data):
- return Unserializer(
- BytesIO(data),
- strconfig=(False, False)).load(versioned=False)
+ read = BytesIO(data).read
+ _unpack_int4 = partial(unpack, "!i")
+ _unpack_float8 = partial(unpack, "!d")
+ stack = []
+ stack_append = stack.append
+ stack_pop = stack.pop
+
+ def _load_collection(type_):
+ length = _unpack_int4(read(4))[0]
+ if length:
+ res = type_(stack[-length:])
+ del stack[-length:]
+ stack_append(res)
+ else:
+ stack_append(type_())
+
+ stopped = False
+ while True:
+ opcode = read(1)
+ if not opcode:
+ raise EOFError
+ if opcode == b'@': # tuple
+ _load_collection(tuple)
+ elif opcode == b'A': # bytes
+ stack_append(read(_unpack_int4(read(4))[0]))
+ elif opcode == b'B': # Channel
+ raise NotImplementedError("%s" % Unserializer.num2func[opcode])
+ elif opcode == b'C': # False
+ stack_append(False)
+ elif opcode == b'D': # float
+ stack_append(_unpack_float8(read(8))[0])
+ elif opcode == b'E': # frozenset
+ _load_collection(frozenset)
+ elif opcode in (b'F', b'G'): # int, long
+ stack_append(_unpack_int4(read(4))[0])
+ elif opcode in (b'H', b'I'): # longint, longlong
+ stack_append(int(read(_unpack_int4(read(4))[0])))
+ elif opcode == b'J': # dict
+ stack_append({})
+ elif opcode == b'K': # list
+ stack_append([None] * _unpack_int4(read(4))[0])
+ elif opcode == b'L': # None
+ stack_append(None)
+ elif opcode == b'M': # Python 2 string
+ stack_append(read(_unpack_int4(read(4))[0]))
+ elif opcode in (b'N', b'S'): # Python 3 string, unicode
+ stack_append(read(_unpack_int4(read(4))[0]).decode('utf-8'))
+ elif opcode == b'O': # set
+ _load_collection(set)
+ elif opcode == b'P': # setitem
+ try:
+ value = stack_pop()
+ key = stack_pop()
+ except IndexError:
+ raise LoadError("not enough items for setitem")
+ stack[-1][key] = value
+ elif opcode == b'Q': # stop
+ stopped = True
+ break
+ elif opcode == b'R': # True
+ stack_append(True)
+ elif opcode == b'T': # complex
+ stack_append(complex(_unpack_float8(read(8))[0],
_unpack_float8(read(8))[0]))
+ else:
+ raise LoadError(
+ "unknown opcode %r - wire protocol corruption?" % opcode)
+ if not stopped:
+ raise LoadError("didn't get STOP")
+ if len(stack) != 1:
+ raise LoadError("internal unserialization error")
+ return stack_pop(0)
def dumps(obj):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/interfaces.py
new/devpi-server-6.2.0/devpi_server/interfaces.py
--- old/devpi-server-6.1.0/devpi_server/interfaces.py 1970-01-01
01:00:00.000000000 +0100
+++ new/devpi-server-6.2.0/devpi_server/interfaces.py 2021-08-12
11:05:00.000000000 +0200
@@ -0,0 +1,82 @@
+from contextlib import closing
+from zope.interface import Interface
+from zope.interface import classImplements
+from zope.interface.interface import adapter_hooks
+from zope.interface.verify import verifyObject
+
+
+class IStorageConnection(Interface):
+ def db_read_last_changelog_serial():
+ """ Return last stored serial.
+ Returns -1 if nothing is stored yet. """
+
+ def db_read_typedkey(relpath):
+ """ Return key name and serial for given relpath.
+ Raises KeyError if not found. """
+
+ def get_changes(serial):
+ """ Returns deserialized readonly changes for given serial. """
+
+ def get_raw_changelog_entry(serial):
+ """ Returns serializes changes for given serial. """
+
+
+class IStorageConnection2(IStorageConnection):
+ def get_relpath_at(relpath, serial):
+ """ Get tuple of (last_serial, back_serial, value) for given relpath
+ at given serial.
+ Raises KeyError if not found. """
+
+ def iter_relpaths_at(typedkeys, at_serial):
+ """ Iterate over all relpaths of the given typed keys starting
+ from at_serial until the first serial in the database.
+ Yields RelpathInfo objects."""
+
+
+# some adapters for legacy plugins
+
+
+def unwrap_connection_obj(obj):
+ if isinstance(obj, closing):
+ obj = obj.thing
+ return obj
+
+
+def get_connection_class(obj):
+ return unwrap_connection_obj(obj).__class__
+
+
+def verify_connection_interface(obj):
+ verifyObject(IStorageConnection2, unwrap_connection_obj(obj))
+
+
+@adapter_hooks.append
+def adapt(iface, obj):
+ # this is not traditional adaption which would return a new object,
+ # but for performance reasons we directly patch the class, so the next
+ # time no adaption call is necessary
+ if iface is IStorageConnection:
+ _obj = unwrap_connection_obj(obj)
+ cls = get_connection_class(_obj)
+ # any storage connection which needs to be adapted to this
+ # interface is a legacy one and we can say that it provides
+ # the original interface directly
+ classImplements(cls, IStorageConnection)
+ # make sure the object now actually provides this interface
+ verifyObject(IStorageConnection, _obj)
+ return obj
+ elif iface is IStorageConnection2:
+ from .keyfs import get_relpath_at
+ from .keyfs import iter_relpaths_at
+ # first make sure the old connection interface is implemented
+ obj = IStorageConnection(obj)
+ _obj = unwrap_connection_obj(obj)
+ cls = get_connection_class(_obj)
+ # now add fallback method directly to the class
+ cls.get_relpath_at = get_relpath_at
+ cls.iter_relpaths_at = iter_relpaths_at
+ # and add the interface
+ classImplements(cls, IStorageConnection2)
+ # make sure the object now actually provides this interface
+ verifyObject(IStorageConnection2, _obj)
+ return obj
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/keyfs.py
new/devpi-server-6.2.0/devpi_server/keyfs.py
--- old/devpi-server-6.1.0/devpi_server/keyfs.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/keyfs.py 2021-08-12
11:05:00.000000000 +0200
@@ -12,6 +12,7 @@
import py
from . import mythread
from .fileutil import loads
+from .interfaces import IStorageConnection2
from .log import threadlog, thread_push_log, thread_pop_log
from .readonly import get_mutable_deepcopy, ensure_deeply_readonly, \
is_deeply_readonly
@@ -141,7 +142,7 @@
def _execute_hooks(self, event_serial, log, raising=False):
log.debug("calling hooks for tx%s", event_serial)
- with self.keyfs._storage.get_connection() as conn:
+ with self.keyfs.get_connection() as conn:
changes = conn.get_changes(event_serial)
# we first check for missing files before we call subscribers
for relpath, (keyname, back_serial, val) in changes.items():
@@ -229,12 +230,19 @@
cache_size=cache_size)
self._readonly = readonly
+ def get_connection(self, closing=True, write=False):
+ conn = IStorageConnection2(
+ self._storage.get_connection(closing=False, write=write))
+ if closing:
+ return contextlib.closing(conn)
+ return conn
+
def finalize_init(self):
self._storage.perform_crash_recovery()
def import_changes(self, serial, changes):
subscriber_task_infos = []
- with self._storage.get_connection(write=True) as conn:
+ with self.get_connection(write=True) as conn:
with conn.write_transaction() as fswriter:
next_serial = conn.last_changelog_serial + 1
assert next_serial == serial, (next_serial, serial)
@@ -292,7 +300,7 @@
recheck = timeout
with threadlog.around("debug", "waiting for tx-serial %s", serial):
with self._cv_new_transaction:
- with self._storage.get_connection() as conn:
+ with self.get_connection() as conn:
while serial > conn.db_read_last_changelog_serial():
if timeout is not None and time_spent >= timeout:
return False
@@ -304,7 +312,7 @@
return self.get_current_serial() + 1
def get_current_serial(self):
- with self._storage.get_connection() as conn:
+ with self.get_connection() as conn:
return conn.last_changelog_serial
def get_last_commit_timestamp(self):
@@ -501,6 +509,54 @@
value = attr.ib()
+def get_relpath_at(self, relpath, serial):
+ """ Fallback method for legacy storage connections. """
+ (keyname, last_serial) = self.db_read_typedkey(relpath)
+ serials_and_values = iter_serial_and_value_backwards(
+ self, relpath, last_serial)
+ try:
+ (last_serial, back_serial, val) = next(serials_and_values)
+ while last_serial >= 0:
+ if last_serial > serial:
+ (last_serial, back_serial, val) = next(serials_and_values)
+ continue
+ return (last_serial, back_serial, val)
+ except StopIteration:
+ pass
+ raise KeyError(relpath)
+
+
+def iter_serial_and_value_backwards(conn, relpath, last_serial):
+ while last_serial >= 0:
+ tup = conn.get_changes(last_serial).get(relpath)
+ if tup is None:
+ raise RuntimeError("no transaction entry at %s" % (last_serial))
+ keyname, back_serial, val = tup
+ yield (last_serial, back_serial, val)
+ last_serial = back_serial
+
+ # we could not find any change below at_serial which means
+ # the key didn't exist at that point in time
+ return
+
+
+def iter_relpaths_at(self, typedkeys, at_serial):
+ keynames = frozenset(k.name for k in typedkeys)
+ seen = set()
+ for serial in range(at_serial, -1, -1):
+ raw_entry = self.get_raw_changelog_entry(serial)
+ changes = loads(raw_entry)[0]
+ for relpath, (keyname, back_serial, val) in changes.items():
+ if keyname not in keynames:
+ continue
+ if relpath not in seen:
+ seen.add(relpath)
+ yield RelpathInfo(
+ relpath=relpath, keyname=keyname,
+ serial=serial, back_serial=back_serial,
+ value=val)
+
+
class Transaction(object):
def __init__(self, keyfs, at_serial=None, write=False):
self.keyfs = keyfs
@@ -521,64 +577,30 @@
@cached_property
def conn(self):
- return self.keyfs._storage.get_connection(
+ return self.keyfs.get_connection(
write=self.write, closing=False)
def iter_relpaths_at(self, typedkeys, at_serial):
- keynames = frozenset(k.name for k in typedkeys)
- seen = set()
- for serial in range(at_serial, -1, -1):
- raw_entry = self.conn.get_raw_changelog_entry(serial)
- changes = loads(raw_entry)[0]
- for relpath, (keyname, back_serial, val) in changes.items():
- if keyname not in keynames:
- continue
- if relpath not in seen:
- seen.add(relpath)
- yield RelpathInfo(
- relpath=relpath, keyname=keyname,
- serial=serial, back_serial=back_serial,
- value=val)
+ return self.conn.iter_relpaths_at(typedkeys, at_serial)
def iter_serial_and_value_backwards(self, relpath, last_serial):
while last_serial >= 0:
- tup = self.conn.get_changes(last_serial).get(relpath)
- if tup is None:
- raise RuntimeError("no transaction entry at %s" %
(last_serial))
- keyname, back_serial, val = tup
+ (last_serial, back_serial, val) = self.conn.get_relpath_at(
+ relpath, last_serial)
yield (last_serial, val)
last_serial = back_serial
- # we could not find any change below at_serial which means
- # the key didn't exist at that point in time
- return
-
def get_last_serial_and_value_at(self, typedkey, at_serial,
raise_on_error=True):
relpath = typedkey.relpath
try:
- (keyname, last_serial) = self.conn.db_read_typedkey(relpath)
+ (last_serial, back_serial, val) =
self.conn.get_relpath_at(relpath, at_serial)
except KeyError:
- if raise_on_error:
- raise
- return None
- serials_and_values = self.iter_serial_and_value_backwards(
- relpath, last_serial)
- try:
- (last_serial, val) = next(serials_and_values)
- while last_serial >= 0:
- if last_serial > at_serial:
- (last_serial, val) = next(serials_and_values)
- continue
- if val is not None or not raise_on_error:
- return (last_serial, val)
- raise KeyError(relpath) # was deleted
- except StopIteration:
- pass
-
- if raise_on_error:
- # we could not find any change below at_serial which means
- # the key didn't exist at that point in time
- raise KeyError(relpath)
+ if not raise_on_error:
+ return None
+ raise
+ if val is None and raise_on_error:
+ raise KeyError(relpath) # was deleted
+ return (last_serial, val)
def get_value_at(self, typedkey, at_serial):
(last_serial, val) = self.get_last_serial_and_value_at(typedkey,
at_serial)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/keyfs_sqlite.py
new/devpi-server-6.2.0/devpi_server/keyfs_sqlite.py
--- old/devpi-server-6.1.0/devpi_server/keyfs_sqlite.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/keyfs_sqlite.py 2021-08-12
11:05:00.000000000 +0200
@@ -1,10 +1,14 @@
from devpi_common.types import cached_property
from .config import hookimpl
from .fileutil import dumps, loads
+from .interfaces import IStorageConnection2
+from .keyfs import RelpathInfo
+from .keyfs import get_relpath_at
from .log import threadlog, thread_push_log, thread_pop_log
from .readonly import ReadonlyView
from .readonly import ensure_deeply_readonly, get_mutable_deepcopy
from repoze.lru import LRUCache
+from zope.interface import implementer
import contextlib
import os
import py
@@ -12,6 +16,9 @@
import time
+absent = object()
+
+
class BaseConnection:
def __init__(self, sqlconn, basedir, storage):
self._sqlconn = sqlconn
@@ -20,6 +27,35 @@
self.storage = storage
self._changelog_cache = storage._changelog_cache
+ def _explain(self, query, *args):
+ # for debugging
+ c = self._sqlconn.cursor()
+ r = c.execute("EXPLAIN " + query, *args)
+ return r.fetchall()
+
+ def _explain_query_plan(self, query, *args):
+ # for debugging
+ c = self._sqlconn.cursor()
+ r = c.execute("EXPLAIN QUERY PLAN " + query, *args)
+ return r.fetchall()
+
+ def fetchall(self, query, *args):
+ c = self._sqlconn.cursor()
+ # print(query)
+ # pprint(self._explain(query, *args))
+ # for row in self._explain_query_plan(query, *args):
+ # print(row)
+ r = c.execute(query, *args)
+ return r.fetchall()
+
+ def iterall(self, query, *args):
+ c = self._sqlconn.cursor()
+ # print(query)
+ # pprint(self._explain(query, *args))
+ # for row in self._explain_query_plan(query, *args):
+ # print(row)
+ return c.execute(query, *args)
+
def close(self):
self._sqlconn.close()
@@ -76,7 +112,46 @@
self._changelog_cache.put(serial, changes)
return changes
+ def get_relpath_at(self, relpath, serial):
+ result = self._changelog_cache.get((serial, relpath), absent)
+ if result is absent:
+ changes = self._changelog_cache.get(serial, absent)
+ if changes is not absent and relpath in changes:
+ (keyname, back_serial, value) = changes[relpath]
+ result = (serial, back_serial, value)
+ if result is absent:
+ result = get_relpath_at(self, relpath, serial)
+ self._changelog_cache.put((serial, relpath), result)
+ return result
+
+ def iter_relpaths_at(self, typedkeys, at_serial):
+ keynames = frozenset(k.name for k in typedkeys)
+ keyname_id_values = {"keynameid%i" % i: k for i, k in
enumerate(keynames)}
+ q = """
+ SELECT key, keyname, serial
+ FROM kv
+ WHERE serial=:serial AND keyname IN (:keynames)
+ """
+ q = q.replace(':keynames', ", ".join(':' + x for x in
keyname_id_values))
+ for serial in range(at_serial, -1, -1):
+ rows = self.fetchall(q, dict(
+ serial=serial,
+ **keyname_id_values))
+ if not rows:
+ continue
+ changes = self._changelog_cache.get(serial, absent)
+ if changes is absent:
+ changes = loads(
+ self.get_raw_changelog_entry(serial))[0]
+ for relpath, keyname, serial in rows:
+ (keyname, back_serial, val) = changes[relpath]
+ yield RelpathInfo(
+ relpath=relpath, keyname=keyname,
+ serial=serial, back_serial=back_serial,
+ value=val)
+
+@implementer(IStorageConnection2)
class Connection(BaseConnection):
def io_file_os_path(self, path):
return None
@@ -230,41 +305,73 @@
return contextlib.closing(conn)
return conn
-
-class Storage(BaseStorage):
- Connection = Connection
- db_filename = ".sqlite_db"
-
- def perform_crash_recovery(self):
- pass
+ def _reflect_schema(self):
+ result = {}
+ with self.get_connection(write=False) as conn:
+ c = conn._sqlconn.cursor()
+ rows = c.execute("""
+ SELECT type, name, sql FROM sqlite_master""")
+ for row in rows:
+ result.setdefault(row[0], {})[row[1]] = row[2]
+ return result
def ensure_tables_exist(self):
- if self.sqlpath.exists():
+ schema = self._reflect_schema()
+ missing = dict()
+ for kind, objs in self.expected_schema.items():
+ for name, q in objs.items():
+ if name not in schema.get(kind, set()):
+ missing.setdefault(kind, dict())[name] = q
+ if not missing:
return
with self.get_connection(write=True) as conn:
- threadlog.info("DB: Creating schema")
+ if not schema:
+ threadlog.info("DB: Creating schema")
+ else:
+ threadlog.info("DB: Updating schema")
c = conn._sqlconn.cursor()
- c.execute("""
+ for kind in ('table', 'index'):
+ objs = missing.pop(kind, {})
+ for name in list(objs):
+ q = objs.pop(name)
+ c.execute(q)
+ assert not objs
+ conn.commit()
+ assert not missing
+
+
+class Storage(BaseStorage):
+ Connection = Connection
+ db_filename = ".sqlite_db"
+ expected_schema = dict(
+ index=dict(
+ kv_serial_idx="""
+ CREATE INDEX kv_serial_idx ON kv (serial);
+ """),
+ table=dict(
+ changelog="""
+ CREATE TABLE changelog (
+ serial INTEGER PRIMARY KEY,
+ data BLOB NOT NULL
+ )
+ """,
+ kv="""
CREATE TABLE kv (
key TEXT NOT NULL PRIMARY KEY,
keyname TEXT,
serial INTEGER
)
- """)
- c.execute("""
- CREATE TABLE changelog (
- serial INTEGER PRIMARY KEY,
- data BLOB NOT NULL
- )
- """)
- c.execute("""
+ """,
+ files="""
CREATE TABLE files (
path TEXT PRIMARY KEY,
size INTEGER NOT NULL,
data BLOB NOT NULL
)
- """)
- conn.commit()
+ """))
+
+ def perform_crash_recovery(self):
+ pass
@hookimpl
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/keyfs_sqlite_fs.py
new/devpi-server-6.2.0/devpi_server/keyfs_sqlite_fs.py
--- old/devpi-server-6.1.0/devpi_server/keyfs_sqlite_fs.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/keyfs_sqlite_fs.py 2021-08-12
11:05:00.000000000 +0200
@@ -1,5 +1,6 @@
from .config import hookimpl
from .fileutil import BytesForHardlink
+from .interfaces import IStorageConnection2
from .keyfs_sqlite import BaseConnection
from .keyfs_sqlite import BaseStorage
from .log import threadlog, thread_push_log, thread_pop_log
@@ -7,6 +8,7 @@
from .readonly import get_mutable_deepcopy
from .fileutil import get_write_file_ensure_dir, rename, loads
from hashlib import sha256
+from zope.interface import implementer
import errno
import os
import re
@@ -44,6 +46,7 @@
f.write(content)
+@implementer(IStorageConnection2)
class Connection(BaseConnection):
def rollback(self):
BaseConnection.rollback(self)
@@ -130,6 +133,25 @@
class Storage(BaseStorage):
Connection = Connection
db_filename = ".sqlite"
+ expected_schema = dict(
+ index=dict(
+ kv_serial_idx="""
+ CREATE INDEX kv_serial_idx ON kv (serial);
+ """),
+ table=dict(
+ changelog="""
+ CREATE TABLE changelog (
+ serial INTEGER PRIMARY KEY,
+ data BLOB NOT NULL
+ )
+ """,
+ kv="""
+ CREATE TABLE kv (
+ key TEXT NOT NULL PRIMARY KEY,
+ keyname TEXT,
+ serial INTEGER
+ )
+ """))
def perform_crash_recovery(self):
# get last changes and verify all renames took place
@@ -140,27 +162,6 @@
changes, rel_renames = loads(data)
check_pending_renames(str(self.basedir), rel_renames)
- def ensure_tables_exist(self):
- if self.sqlpath.exists():
- return
- with self.get_connection(write=True) as conn:
- threadlog.info("DB: Creating schema")
- c = conn._sqlconn.cursor()
- c.execute("""
- CREATE TABLE kv (
- key TEXT NOT NULL PRIMARY KEY,
- keyname TEXT,
- serial INTEGER
- )
- """)
- c.execute("""
- CREATE TABLE changelog (
- serial INTEGER PRIMARY KEY,
- data BLOB NOT NULL
- )
- """)
- conn.commit()
-
@hookimpl
def devpiserver_storage_backend(settings):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/log.py
new/devpi-server-6.2.0/devpi_server/log.py
--- old/devpi-server-6.1.0/devpi_server/log.py 2021-07-11 17:55:40.000000000
+0200
+++ new/devpi-server-6.2.0/devpi_server/log.py 2021-08-12 11:05:00.000000000
+0200
@@ -31,7 +31,7 @@
if config_args.logger_cfg.endswith(".json"):
logger_cfg = json.loads(f.read())
else:
- logger_cfg = yaml.safe_load(f.read())
+ logger_cfg = yaml.YAML(typ='safe', pure=True).load(f.read())
logging.config.dictConfig(logger_cfg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/replica.py
new/devpi-server-6.2.0/devpi_server/replica.py
--- old/devpi-server-6.1.0/devpi_server/replica.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/replica.py 2021-08-12
11:05:00.000000000 +0200
@@ -780,7 +780,7 @@
with self.xom.keyfs.transaction(write=False):
mirror_stage = self.xom.model.getstage(username, index)
if mirror_stage and mirror_stage.ixconfig["type"] == "mirror":
- cache_projectnames =
mirror_stage.cache_projectnames.get_inplace()
+ cache_projectnames = mirror_stage.cache_projectnames
if cache is None: # deleted
cache_projectnames.discard(project)
else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server/views.py
new/devpi-server-6.2.0/devpi_server/views.py
--- old/devpi-server-6.1.0/devpi_server/views.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server/views.py 2021-08-12
11:05:00.000000000 +0200
@@ -572,8 +572,11 @@
whitelist_info = stage.get_mirror_whitelist_info(project)
embed_form = whitelist_info['has_mirror_base']
blocked_index = whitelist_info['blocked_by_mirror_whitelist']
- response = Response(body=b"".join(self._simple_list_project(
- stage, project, result, embed_form, blocked_index)))
+ body = b"".join(self._simple_list_project(
+ stage, project, result, embed_form, blocked_index))
+ response = Response(
+ content_length=len(body),
+ body=body)
if stage.ixconfig['type'] == 'mirror':
serial = stage.key_projsimplelinks(project).get().get("serial")
if serial > 0:
@@ -581,9 +584,6 @@
return response
def _simple_list_project(self, stage, project, result, embed_form,
blocked_index):
- response = self.request.response
- response.content_type = "text/html ; charset=utf-8"
-
title = "%s: links for %s" % (stage.name, project)
yield ("<!DOCTYPE
html><html><head><title>%s</title></head><body><h1>%s</h1>\n" %
(title, title)).encode("utf-8")
@@ -659,17 +659,20 @@
self.log.info("starting +simple")
stage = self.context.stage
try:
+ # list is called to force iteration over all results in this
+ # try/except block
stage_results = list(stage.list_projects())
except stage.UpstreamError as e:
threadlog.error(e.msg)
abort(self.request, 502, e.msg)
# at this point we are sure we can produce the data without
# depending on remote networks
- return Response(body=b"".join(self._simple_list_all(stage,
stage_results)))
+ body = b"".join(self._simple_list_all(stage, stage_results))
+ return Response(
+ content_length=len(body),
+ body=body)
def _simple_list_all(self, stage, stage_results):
- response = self.request.response
- response.content_type = "text/html ; charset=utf-8"
title = "%s: simple list (including inherited indices)" % (stage.name)
yield ("<!DOCTYPE
html><html><head><title>%s</title></head><body><h1>%s</h1>" %(
title, title)).encode("utf-8")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server.egg-info/PKG-INFO
new/devpi-server-6.2.0/devpi_server.egg-info/PKG-INFO
--- old/devpi-server-6.1.0/devpi_server.egg-info/PKG-INFO 2021-07-11
17:55:42.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server.egg-info/PKG-INFO 2021-08-12
11:05:02.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: devpi-server
-Version: 6.1.0
+Version: 6.2.0
Summary: devpi-server: reliable private and pypi.org caching server
Home-page: https://devpi.net
Maintainer: Holger Krekel, Florian Schulze
@@ -102,6 +102,21 @@
.. towncrier release notes start
+6.2.0 (2021-08-12)
+==================
+
+Bug Fixes
+---------
+
+- Optimized some database access patterns. A new index is added to the
database on first startup. For large databases that can take a while.
+
+- Improved performance of loads from database.
+
+- Optimized memory and cache use for database access.
+
+- Use frozenset for project name cache of mirror indexes. This mitigates
memory fragmentation on some Linux distributions.
+
+
6.1.0 (2021-07-11)
==================
@@ -222,13 +237,4 @@
- Pin to pyramid<2.
-5.5.0 (2020-05-04)
-==================
-
-Features
---------
-
-- Proxy requests from replica to master are now streamed if possible. This
improves reliability of large uploads through replicas and reduces RAM usage on
the replica.
-
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/devpi_server.egg-info/SOURCES.txt
new/devpi-server-6.2.0/devpi_server.egg-info/SOURCES.txt
--- old/devpi-server-6.1.0/devpi_server.egg-info/SOURCES.txt 2021-07-11
17:55:42.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server.egg-info/SOURCES.txt 2021-08-12
11:05:02.000000000 +0200
@@ -13,6 +13,7 @@
devpi_server/auth_basic.py
devpi_server/auth_devpi.py
devpi_server/config.py
+devpi_server/exceptions.py
devpi_server/extpypi.py
devpi_server/filestore.py
devpi_server/fileutil.py
@@ -21,6 +22,7 @@
devpi_server/hookspecs.py
devpi_server/importexport.py
devpi_server/init.py
+devpi_server/interfaces.py
devpi_server/keyfs.py
devpi_server/keyfs_sqlite.py
devpi_server/keyfs_sqlite_fs.py
@@ -61,6 +63,7 @@
test_devpi_server/test_conftest.py
test_devpi_server/test_extpypi.py
test_devpi_server/test_filestore.py
+test_devpi_server/test_fileutil.py
test_devpi_server/test_genconfig.py
test_devpi_server/test_importexport.py
test_devpi_server/test_keyfs.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/devpi-server-6.1.0/devpi_server.egg-info/requires.txt
new/devpi-server-6.2.0/devpi_server.egg-info/requires.txt
--- old/devpi-server-6.1.0/devpi_server.egg-info/requires.txt 2021-07-11
17:55:42.000000000 +0200
+++ new/devpi-server-6.2.0/devpi_server.egg-info/requires.txt 2021-08-12
11:05:02.000000000 +0200
@@ -10,6 +10,6 @@
waitress>=1.0.1
repoze.lru>=0.6
passlib[argon2]
-pluggy<1.0,>=0.6.0
+pluggy<2.0,>=0.6.0
ruamel.yaml
strictyaml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/setup.py
new/devpi-server-6.2.0/setup.py
--- old/devpi-server-6.1.0/setup.py 2021-07-11 17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/setup.py 2021-08-12 11:05:00.000000000 +0200
@@ -38,7 +38,7 @@
"waitress>=1.0.1",
"repoze.lru>=0.6",
"passlib[argon2]",
- "pluggy>=0.6.0,<1.0",
+ "pluggy>=0.6.0,<2.0",
'ruamel.yaml',
"strictyaml",
]
@@ -56,7 +56,7 @@
'Documentation': 'https://doc.devpi.net',
'Source Code': 'https://github.com/devpi/devpi'
},
- version='6.1.0',
+ version='6.2.0',
maintainer="Holger Krekel, Florian Schulze",
maintainer_email="[email protected]",
packages=[
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/test_devpi_server/conftest.py
new/devpi-server-6.2.0/test_devpi_server/conftest.py
--- old/devpi-server-6.1.0/test_devpi_server/conftest.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/test_devpi_server/conftest.py 2021-08-12
11:05:00.000000000 +0200
@@ -155,10 +155,8 @@
return xom
[email protected](autouse=True, scope="session")
-def speed_up_sqlite():
- from devpi_server.keyfs_sqlite import Storage
- old = Storage.ensure_tables_exist
+def _speed_up_sqlite(cls):
+ old = cls.ensure_tables_exist
def make_unsynchronous(self, old=old):
conn = old(self)
@@ -166,7 +164,14 @@
conn._sqlconn.execute("PRAGMA synchronous=OFF")
return
- Storage.ensure_tables_exist = make_unsynchronous
+ cls.ensure_tables_exist = make_unsynchronous
+ return old
+
+
[email protected](autouse=True, scope="session")
+def speed_up_sqlite():
+ from devpi_server.keyfs_sqlite import Storage
+ old = _speed_up_sqlite(Storage)
yield
Storage.ensure_tables_exist = old
@@ -174,15 +179,7 @@
@pytest.fixture(autouse=True, scope="session")
def speed_up_sqlite_fs():
from devpi_server.keyfs_sqlite_fs import Storage
- old = Storage.ensure_tables_exist
-
- def make_unsynchronous(self, old=old):
- conn = old(self)
- with self.get_connection() as conn:
- conn._sqlconn.execute("PRAGMA synchronous=OFF")
- return
-
- Storage.ensure_tables_exist = make_unsynchronous
+ old = _speed_up_sqlite(Storage)
yield
Storage.ensure_tables_exist = old
@@ -203,6 +200,8 @@
if backend is None:
backend = 'devpi_server.keyfs_sqlite_fs'
plugin = locate(backend)
+ if plugin is None:
+ raise RuntimeError("Couldn't find storage backend '%s'" % backend)
result = plugin.devpiserver_storage_backend(settings=None)
result["_test_plugin"] = plugin
return result
@@ -222,6 +221,7 @@
from devpi_server import replica
from devpi_server import view_auth
from devpi_server import views
+ from devpi_server.interfaces import verify_connection_interface
plugins = [
plugin[0] if isinstance(plugin, tuple) else plugin
for plugin in plugins]
@@ -263,6 +263,9 @@
monkeypatch.setattr(extpypi.PyPIStage, "_get_remote_projects",
lambda self: set())
add_pypistage_mocks(monkeypatch, httpget)
+ # verify storage interface
+ with xom.keyfs.get_connection() as conn:
+ verify_connection_interface(conn)
# initialize default indexes
from devpi_server.main import init_default_indexes
if not xom.config.args.master_url:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/devpi-server-6.1.0/test_devpi_server/test_extpypi.py
new/devpi-server-6.2.0/test_devpi_server/test_extpypi.py
--- old/devpi-server-6.1.0/test_devpi_server/test_extpypi.py 2021-07-11
17:55:40.000000000 +0200
+++ new/devpi-server-6.2.0/test_devpi_server/test_extpypi.py 2021-08-12
11:05:00.000000000 +0200
@@ -665,9 +665,10 @@
cache.set(s)
s.add(4)
assert cache.get() != s
- s2 = cache.get_inplace()
- s2.add(5)
+ cache.add(5)
assert 5 in cache.get()
+ cache.discard(5)
+ assert 5 not in cache.get()
def test_is_expired(self, cache, monkeypatch):
expiry_time = 100
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/devpi-server-6.1.0/test_devpi_server/test_fileutil.py
new/devpi-server-6.2.0/test_devpi_server/test_fileutil.py
--- old/devpi-server-6.1.0/test_devpi_server/test_fileutil.py 1970-01-01
01:00:00.000000000 +0100
+++ new/devpi-server-6.2.0/test_devpi_server/test_fileutil.py 2021-08-12
11:05:00.000000000 +0200
@@ -0,0 +1,69 @@
+from devpi_server.fileutil import BytesIO
+from devpi_server.fileutil import Unserializer
+from devpi_server.fileutil import dumps
+from devpi_server.fileutil import loads
+import pytest
+
+
+# the original function
+def _loads(data):
+ return Unserializer(
+ BytesIO(data),
+ strconfig=(False, False)).load(versioned=False)
+
+
+def test_execnet_opcodes():
+ # we need to make sure execnet doesn't change
+ assert list(Unserializer.num2func.keys()) == [
+ b'@', b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K',
+ b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T']
+
+
[email protected]('data, expected', [
+ (b'@\x00\x00\x00\x00Q', ()),
+ (b'F\x00\x00\x00\x01@\x00\x00\x00\x01Q', (1,)),
+ (b'F\x00\x00\x00\x01F\x00\x00\x00\x02@\x00\x00\x00\x02Q', (1, 2)),
+ (b'A\x00\x00\x00\x01aQ', b'a'),
+ (b'A\x00\x00\x00\x02abQ', b'ab'),
+ (b'CQ', False),
+ (b'D\x00\x00\x00\x00\x00\x00\x00\x00Q', 0.0),
+ (b'D\x3f\xf0\x00\x00\x00\x00\x00\x00Q', 1.0),
+ (b'E\x00\x00\x00\x00Q', frozenset()),
+ (b'F\x00\x00\x00\x01E\x00\x00\x00\x01Q', frozenset((1,))),
+ (b'F\x00\x00\x00\x01F\x00\x00\x00\x02E\x00\x00\x00\x02Q', frozenset((1,
2))),
+ (b'F\x00\x00\x00\x01Q', int(1)),
+ (b'G\x00\x00\x00\x01Q', int(1)),
+ (b'H\x00\x00\x00\x011Q', int(1)),
+ (b'I\x00\x00\x00\x011Q', int(1)),
+ (b'JQ', {}),
+
(b'JF\x00\x00\x00\x00F\x00\x00\x00\x00PF\x00\x00\x00\x01F\x00\x00\x00\x02PQ',
{0: 0, 1: 2}),
+ (b'K\x00\x00\x00\x00Q', []),
+ (b'K\x00\x00\x00\x01Q', [None]),
+ (b'K\x00\x00\x00\x02Q', [None, None]),
+ (b'K\x00\x00\x00\x01F\x00\x00\x00\x00F\x00\x00\x00\x01PQ', [1]),
+
(b'K\x00\x00\x00\x02F\x00\x00\x00\x00F\x00\x00\x00\x01PF\x00\x00\x00\x01F\x00\x00\x00\x02PQ',
[1, 2]),
+ (b'K\x00\x00\x00\x01F\x00\x00\x00\x00RPQ', [True]),
+ (b'K\x00\x00\x00\x01F\x00\x00\x00\x00CPQ', [False]),
+ (b'LQ', None),
+ (b'M\x00\x00\x00\x01aQ', b'a'),
+ (b'M\x00\x00\x00\x02abQ', b'ab'),
+ (b'N\x00\x00\x00\x01aQ', 'a'),
+ (b'N\x00\x00\x00\x02\xc3\xa4Q', '??'),
+ (b'O\x00\x00\x00\x00Q', set()),
+ (b'F\x00\x00\x00\x01O\x00\x00\x00\x01Q', set((1,))),
+ (b'F\x00\x00\x00\x01F\x00\x00\x00\x02O\x00\x00\x00\x02Q', set((1, 2))),
+ (b'RQ', True),
+ (b'S\x00\x00\x00\x01aQ', 'a'),
+ (b'S\x00\x00\x00\x02\xc3\xa4Q', '??'),
+ (b'T\x3f\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q',
complex(1, 0)),
+ (b'T\x00\x00\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00Q',
complex(0, 1)),
+ (b'T\x3f\xf0\x00\x00\x00\x00\x00\x00\x3f\xf0\x00\x00\x00\x00\x00\x00Q',
complex(1, 1))])
+def test_loads(data, expected):
+ result = loads(data)
+ assert result == expected
+ assert type(result) == type(expected)
+ # try round-trip
+ assert loads(dumps(expected)) == expected
+ # compare to original
+ assert result == _loads(data)
+ assert _loads(dumps(expected)) == expected