diff --git a/.gitignore b/.gitignore index 7f368fc..50efb09 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,9 @@ dist/ *~ .tox/ -nosetests.xml hypatia/coverage.xml -distribute-*.gz +coverage-py*.xml +coverage.xml env*/ .build/ bin/ diff --git a/.travis.yml b/.travis.yml index 5dc7023..87049a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,6 @@ matrix: include: - python: 2.7 env: TOXENV=py27 - - python: 3.4 - env: TOXENV=py34 - - python: 3.5 - env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: 3.7 diff --git a/CHANGES.rst b/CHANGES.rst index fa3f02b..617c2e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,25 @@ 0.4 (unreleased) ---------------- -- Add support for Python 3.5. +- Drop (unreleased) support for Python 3.3 and Python 3.4 (the ``persistent`` + package no longer supports these versions). -- Drop support for Python 2.6 and 3.2. +- Drop (unreleased) support for Python 3.5 (too difficult to build correctly -- + even with pyenv -- to bother). + +- Drop (released) support for Python 2.6 and 3.2. + +- Add support for Python 3.6, 3.7, 3.8, 3.9, 3.10 (thanks to Thierry Florac). + +- Use pytest instead of python setup.py test -q in tox.ini. + +- Change how cross-version coverage is computed in tests. + +- Remove unused hypatia/text/ricecode.py file. + +- Change building of docs slightly to cope with modified github protocols. + Some warnings are unearthed with newer Sphinx, which I've not + caught by turning warnings into errors yet, because I don't understand them. - Don't modify queries attribute when optimizing And or Or, return a new instance instead. This change is needed to open new optimization diff --git a/docs/changes.rst b/docs/changes.rst index 3b92413..ba112b9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,4 +1,4 @@ :mod:`hypatia` Change History ============================= -.. literalinclude:: ../CHANGES.txt +.. include:: ../CHANGES.rst diff --git a/docs/conf.py b/docs/conf.py index d2b6061..23cbddf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ _themes = os.path.join(cwd, '_themes') if not os.path.isdir(_themes): - call([git, 'clone', 'git://github.com/Pylons/pylons_sphinx_theme.git', + call([git, 'clone', 'https://github.com/Pylons/pylons_sphinx_theme.git', '_themes']) else: os.chdir(_themes) diff --git a/hypatia/text/ricecode.py b/hypatia/text/ricecode.py deleted file mode 100644 index a7c60da..0000000 --- a/hypatia/text/ricecode.py +++ /dev/null @@ -1,214 +0,0 @@ -############################################################################## -# -# Copyright (c) 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Rice coding (a variation of Golomb coding) - -Based on a Java implementation by Glen McCluskey described in a Usenix - ;login: article at -http://www.usenix.org/publications/login/2000-4/features/java.html - -McCluskey's article explains the approach as follows. The encoding -for a value x is represented as a unary part and a binary part. The -unary part is a sequence of 1 bits followed by a 0 bit. The binary -part encodes some of the lower bits of x-1. - -The encoding is parameterized by a value m that describes how many -bits to store in the binary part. If most of the values are smaller -than 2**m then they can be stored in only m+1 bits. - -Compute the length of the unary part, q, where - q = math.floor((x-1)/ 2 ** m) - - Emit q 1 bits followed by a 0 bit. - -Emit the lower m bits of x-1, treating x-1 as a binary value. -""" - -import array - -class BitArray(object): - - def __init__(self, buf=None): - self.bytes = array.array('B') - self.nbits = 0 - self.bitsleft = 0 - self.tostring = self.bytes.tostring - - def __getitem__(self, i): - byte, offset = divmod(i, 8) - mask = 2 ** offset - if self.bytes[byte] & mask: - return 1 - else: - return 0 - - def __setitem__(self, i, val): - byte, offset = divmod(i, 8) - mask = 2 ** offset - if val: - self.bytes[byte] |= mask - else: - self.bytes[byte] &= ~mask - - def __len__(self): - return self.nbits - - def append(self, bit): - """Append a 1 if bit is true or 1 if it is false.""" - if self.bitsleft == 0: - self.bytes.append(0) - self.bitsleft = 8 - self.__setitem__(self.nbits, bit) - self.nbits += 1 - self.bitsleft -= 1 - - def __getstate__(self): - return self.nbits, self.bitsleft, self.tostring() - - def __setstate__(self, nbits_bitsleft_s): - nbits, bitsleft, s = nbits_bitsleft_s - self.bytes = array.array('B', s) - self.nbits = nbits - self.bitsleft = bitsleft - -class RiceCode(object): - def __init__(self, m): - """Constructor a RiceCode for m-bit values.""" - if not (0 <= m <= 16): - raise ValueError("m must be between 0 and 16") - self.init(m) - self.bits = BitArray() - self.len = 0 - - def init(self, m): - self.m = m - self.lower = (1 << m) - 1 - self.mask = 1 << (m - 1) - - def append(self, val): - """Append an item to the list.""" - if val < 1: - raise ValueError("value >= 1 expected, got %s" % repr(val)) - val -= 1 - # emit the unary part of the code - q = val >> self.m - for i in range(q): - self.bits.append(1) - self.bits.append(0) - # emit the binary part - r = val & self.lower - mask = self.mask - while mask: - self.bits.append(r & mask) - mask >>= 1 - self.len += 1 - - def __len__(self): - return self.len - - def tolist(self): - """Return the items as a list.""" - l = [] - i = 0 # bit offset - binary_range = range(self.m) - for j in range(self.len): - unary = 0 - while self.bits[i] == 1: - unary += 1 - i += 1 - assert self.bits[i] == 0 - i += 1 - binary = 0 - for k in binary_range: - binary = (binary << 1) | self.bits[i] - i += 1 - l.append((unary << self.m) + (binary + 1)) - return l - - def tostring(self): - """Return a binary string containing the encoded data. - - The binary string may contain some extra zeros at the end. - """ - return self.bits.tostring() - - def __getstate__(self): - return self.m, self.bits - - def __setstate__(self, m_bits): - m, bits = m_bits - self.init(m) - self.bits = bits - -def encode(m, l): - c = RiceCode(m) - for elt in l: - c.append(elt) - assert c.tolist() == l - return c - -def encode_deltas(l): - if len(l) == 1: - return l[0], [] - deltas = RiceCode(6) - deltas.append(l[1] - l[0]) - for i in range(2, len(l)): - deltas.append(l[i] - l[i - 1]) - return l[0], deltas - -def decode_deltas(start, enc_deltas): - deltas = enc_deltas.tolist() - l = [start] - for i in range(1, len(deltas)): - l.append(l[i-1] + deltas[i]) - l.append(l[-1] + deltas[-1]) - return l - -def _print(x, newline=True): - import sys - fmt = newline and '%s\n' or '%s' - sys.stdout.write(fmt % x) - -def test(): - import random - for size in [10, 20, 50, 100, 200]: - l = [random.randint(1, size) for i in range(50)] - c = encode(random.randint(1, 16), l) - assert c.tolist() == l - for size in [10, 20, 50, 100, 200]: - l = range(random.randint(1, size), size + random.randint(1, size)) - t = encode_deltas(l) - l2 = decode_deltas(*t) - assert l == l2 - if l != l2: - _print(l) - _print(l2) - -def pickle_efficiency(): - import pickle - import random - for m in [4, 8, 12]: - for size in [10, 20, 50, 100, 200, 500, 1000, 2000, 5000]: - for elt_range in [10, 20, 50, 100, 200, 500, 1000]: - l = [random.randint(1, elt_range) for i in range(size)] - raw = pickle.dumps(l, 1) - enc = pickle.dumps(encode(m, l), 1) - _print("m=%2d size=%4d range=%4d" % (m, size, elt_range), False) - _print("%5d %5d" % (len(raw), len(enc)), False) - if len(raw) > len(enc): - _print("win") - else: - _print("lose") - -if __name__ == "__main__": - test() diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..92d5165 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +python_files = test*.py +addopts = --deselect=hypatia/tests/test_functional.py::TestCatalogQueryBase + diff --git a/setup.py b/setup.py index bead19e..ce649d3 100644 --- a/setup.py +++ b/setup.py @@ -70,8 +70,12 @@ def errprint(x): 'zope.interface', ] -testing_extras = ['nose', 'coverage', 'nosexcover'] -docs_extras = ['Sphinx'] +testing_extras = ['pytest', 'coverage'] + +docs_extras = [ + 'Sphinx >= 3.0.0', # Force RTD to use >= 3.0.0 +] + setup(name='hypatia', version='0.4.dev0', @@ -83,8 +87,6 @@ def errprint(x): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tox.ini b/tox.ini index 5ced065..dee35fd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,55 @@ [tox] envlist = - py27,py34,py35,py36,py37,py38,py39,py310,cover,docs + py27,py36,py37,py38,py39,py310,{py2,py3}-cover,coverage,docs [testenv] commands = - python setup.py -q test -q + pip install -e .[testing] + pytest +extras = + testing -[testenv:cover] -basepython = - python3.9 -commands = - pip install hypatia[testing] - nosetests --with-xunit --with-xcoverage +[testenv:py2-cover] +basepython = python2.7 +commands = + #coverage run setup.py -q test -q + pip install -e .[testing] + coverage run --source=hypatia {envbindir}/pytest + coverage xml -o coverage-py2.xml +setenv = + COVERAGE_FILE=.coverage.py2 +extras = + testing -# we separate coverage into its own testenv because a) "last run wins" wrt -# cobertura jenkins reporting and b) pypy and jython can't handle any -# combination of versions of coverage and nosexcover that i can find. +[testenv:py3-cover] +basepython = python3.10 +commands = + #coverage run setup.py -q test -q + pip install -e .[testing] + coverage run --source=hypatia {envbindir}/pytest + coverage xml -o coverage-py3.xml +setenv = + COVERAGE_FILE=.coverage.py3 +extras = + testing -[testenv:docs] +[testenv:coverage] basepython = - python3.9 -commands = - sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html + python2.7 +skip_install = true +commands = + coverage erase + coverage combine + coverage xml + coverage report -m --fail-under=100 deps = - Sphinx + coverage +setenv = + COVERAGE_FILE=.coverage + +[testenv:docs] +whitelist_externals = make +commands = + make -C docs {posargs:html} BUILDDIR={envdir} "SPHINXOPTS=-E" +extras = + docs