New Upstream Release - python-boltons
Ready changes
Summary
Merged new upstream version: 23.0.0 (was: 21.0.0).
Resulting package
Built on 2023-03-10T22:38 (took 2m3s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases python3-boltons
Lintian Result
Diff
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 69a6313..7183dc7 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -18,18 +18,18 @@ jobs:
fail-fast: false
matrix:
include:
- - {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39}
- - {name: Windows, python: '3.9', os: windows-latest, tox: py39}
- - {name: Mac, python: '3.9', os: macos-latest, tox: py39}
+ - {name: Linux, python: '3.11', os: ubuntu-latest, tox: py311}
+ - {name: Windows, python: '3.11', os: windows-latest, tox: py311}
+ - {name: Mac, python: '3.11', os: macos-latest, tox: py311}
+ - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
+ - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
- - {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
- - {name: '2.7', python: '2.7', os: ubuntu-latest, tox: py27}
- - {name: 'PyPy2', python: 'pypy2', os: ubuntu-latest, tox: pypy}
- - {name: 'PyPy3', python: 'pypy3', os: ubuntu-latest, tox: pypy3}
+ - {name: '2.7', python: '2.7.18', os: ubuntu-latest, tox: py27}
+ - {name: 'PyPy3', python: 'pypy-3.9', os: ubuntu-latest, tox: pypy3}
steps:
- uses: actions/checkout@v2
- - uses: actions/setup-python@v2
+ - uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: update pip
diff --git a/.gitignore b/.gitignore
index 604a8e8..c6c4daf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
docs/_build
tmp.py
htmlcov/
-
+venv/
*.py[cod]
# emacs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a1a61f..7ff4e32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@ for an average of one 33-commit release about every 9 weeks. Versions
are named according to the [CalVer](https://calver.org) versioning
scheme (`YY.MINOR.MICRO`).
+23.0.0
+------
+*(February 19, 2023)*
+
+* Overdue update for Python 3.10 and 3.11 support ([#294][i294], [#303][i303], [#320][i320], [#323][i323], [#326][i326]/[#327][i327])
+* Add [iterutils.chunk_ranges][iterutils.chunk_ranges] ([#312][i312])
+* Improvements to `SpooledBytesIO`/`SpooledStringIO` ([#305][i305])
+* Bugfix for infinite daterange issue when start and stop is the same ([#302][i302])
+* Fix `Bits.as_list` behavior ([#315][i315])
+
21.0.0
------
*(May 16, 2021)*
@@ -1027,6 +1037,16 @@ added in this release.
[i161]: https://github.com/mahmoud/boltons/issues/161
[i162]: https://github.com/mahmoud/boltons/issues/162
[i164]: https://github.com/mahmoud/boltons/issues/164
+[i294]: https://github.com/mahmoud/boltons/issues/294
+[i302]: https://github.com/mahmoud/boltons/issues/302
+[i303]: https://github.com/mahmoud/boltons/issues/303
+[i305]: https://github.com/mahmoud/boltons/issues/305
+[i312]: https://github.com/mahmoud/boltons/issues/312
+[i315]: https://github.com/mahmoud/boltons/issues/315
+[i320]: https://github.com/mahmoud/boltons/issues/320
+[i323]: https://github.com/mahmoud/boltons/issues/323
+[i326]: https://github.com/mahmoud/boltons/issues/326
+[i327]: https://github.com/mahmoud/boltons/issues/327
[ioutils]: http://boltons.readthedocs.org/en/latest/ioutils.html
[ioutils.MultiFileReader]: http://boltons.readthedocs.org/en/latest/ioutils.html#boltons.ioutils.MultiFileReader
[ioutils.SpooledBytesIO]: http://boltons.readthedocs.org/en/latest/ioutils.html#boltons.ioutils.SpooledBytesIO
@@ -1036,6 +1056,7 @@ added in this release.
[iterutils.backoff_iter]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.backoff_iter
[iterutils.chunked]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.chunked
[iterutils.chunked_iter]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.chunked_iter
+[iterutils.chunk_ranges]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.chunk_ranges
[iterutils.first]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.first
[iterutils.flatten]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.flatten
[iterutils.flatten_iter]: http://boltons.readthedocs.org/en/latest/iterutils.html#boltons.iterutils.flatten_iter
diff --git a/README.md b/README.md
index 082cd7c..665e225 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
<a href="https://boltons.readthedocs.io/en/latest/"><img src="https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat"></a>
<a href="https://pypi.python.org/pypi/boltons"><img src="https://img.shields.io/pypi/v/boltons.svg"></a>
<a href="https://anaconda.org/conda-forge/boltons"><img src="https://img.shields.io/conda/vn/conda-forge/boltons.svg"></a>
-<a href="https://ports.macports.org/port/py-boltons/summary"><img src="https://img.shields.io/badge/macports-v20.2.1-blue?logo=Apple&logoColor=white"></a>
+<a href="https://ports.macports.org/port/py-boltons/summary"><img src="https://repology.org/badge/version-for-repo/macports/python:boltons.svg?header=🍎 MacPorts"></a>
<a href="https://pypi.python.org/pypi/boltons"><img src="https://img.shields.io/pypi/pyversions/boltons.svg"></a>
<a href="http://calver.org"><img src="https://img.shields.io/badge/calver-YY.MINOR.MICRO-22bfda.svg"></a>
diff --git a/boltons/cacheutils.py b/boltons/cacheutils.py
index 4e5e508..27a19cb 100644
--- a/boltons/cacheutils.py
+++ b/boltons/cacheutils.py
@@ -82,7 +82,7 @@ except Exception:
pass
try:
- from boltons.typeutils import make_sentinel
+ from .typeutils import make_sentinel
_MISSING = make_sentinel(var_name='_MISSING')
_KWARG_MARK = make_sentinel(var_name='_KWARG_MARK')
except ImportError:
diff --git a/boltons/debugutils.py b/boltons/debugutils.py
index 772f231..aca42d5 100644
--- a/boltons/debugutils.py
+++ b/boltons/debugutils.py
@@ -47,7 +47,7 @@ except NameError:
from reprlib import Repr
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_UNSET = make_sentinel(var_name='_UNSET')
except ImportError:
_UNSET = object()
diff --git a/boltons/dictutils.py b/boltons/dictutils.py
index 2954bba..31a1951 100644
--- a/boltons/dictutils.py
+++ b/boltons/dictutils.py
@@ -82,7 +82,7 @@ except ImportError:
from itertools import zip_longest as izip_longest
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_MISSING = make_sentinel(var_name='_MISSING')
except ImportError:
_MISSING = object()
diff --git a/boltons/ecoutils.py b/boltons/ecoutils.py
index 0ccad70..91b9412 100644
--- a/boltons/ecoutils.py
+++ b/boltons/ecoutils.py
@@ -354,38 +354,53 @@ def get_profile(**kwargs):
return ret
-_real_safe_repr = pprint._safe_repr
-
-
-def _fake_json_dumps(val, indent=2):
- # never do this. this is a hack for Python 2.4. Python 2.5 added
- # the json module for a reason.
- def _fake_safe_repr(*a, **kw):
- res, is_read, is_rec = _real_safe_repr(*a, **kw)
- if res == 'None':
- res = 'null'
- if res == 'True':
- res = 'true'
- if res == 'False':
- res = 'false'
- if not (res.startswith("'") or res.startswith("u'")):
- res = res
- else:
- if res.startswith('u'):
- res = res[1:]
+try:
+ import json
+
+ def dumps(val, indent):
+ if indent:
+ return json.dumps(val, sort_keys=True, indent=indent)
+ return json.dumps(val, sort_keys=True)
+
+except ImportError:
+ _real_safe_repr = pprint._safe_repr
+
+ def _fake_json_dumps(val, indent=2):
+ # never do this. this is a hack for Python 2.4. Python 2.5 added
+ # the json module for a reason.
+ def _fake_safe_repr(*a, **kw):
+ res, is_read, is_rec = _real_safe_repr(*a, **kw)
+ if res == 'None':
+ res = 'null'
+ if res == 'True':
+ res = 'true'
+ if res == 'False':
+ res = 'false'
+ if not (res.startswith("'") or res.startswith("u'")):
+ res = res
+ else:
+ if res.startswith('u'):
+ res = res[1:]
- contents = res[1:-1]
- contents = contents.replace('"', '').replace(r'\"', '')
- res = '"' + contents + '"'
- return res, is_read, is_rec
+ contents = res[1:-1]
+ contents = contents.replace('"', '').replace(r'\"', '')
+ res = '"' + contents + '"'
+ return res, is_read, is_rec
- pprint._safe_repr = _fake_safe_repr
- try:
- ret = pprint.pformat(val, indent=indent)
- finally:
- pprint._safe_repr = _real_safe_repr
+ pprint._safe_repr = _fake_safe_repr
+ try:
+ ret = pprint.pformat(val, indent=indent)
+ finally:
+ pprint._safe_repr = _real_safe_repr
+
+ return ret
+
+ def dumps(val, indent):
+ ret = _fake_json_dumps(val, indent=indent)
+ if not indent:
+ ret = re.sub(r'\n\s*', ' ', ret)
+ return ret
- return ret
def get_profile_json(indent=False):
@@ -393,20 +408,6 @@ def get_profile_json(indent=False):
indent = 2
else:
indent = 0
- try:
- import json
-
- def dumps(val, indent):
- if indent:
- return json.dumps(val, sort_keys=True, indent=indent)
- return json.dumps(val, sort_keys=True)
-
- except ImportError:
- def dumps(val, indent):
- ret = _fake_json_dumps(val, indent=indent)
- if not indent:
- ret = re.sub(r'\n\s*', ' ', ret)
- return ret
data_dict = get_profile()
return dumps(data_dict, indent)
diff --git a/boltons/formatutils.py b/boltons/formatutils.py
index f77b3ee..a45adb1 100644
--- a/boltons/formatutils.py
+++ b/boltons/formatutils.py
@@ -54,7 +54,7 @@ format strings:
arguments out of a format string.
* :func:`tokenize_format_str`: Tokenize a format string into
literals and :class:`BaseFormatField` objects.
- * :func:`construct_format_field_str`: Assists in progammatic
+ * :func:`construct_format_field_str`: Assists in programmatic
construction of format strings.
* :func:`infer_positional_format_args`: Converts anonymous
references in 2.7+ format strings to explicit positional arguments
diff --git a/boltons/funcutils.py b/boltons/funcutils.py
index 40befee..e46cddc 100644
--- a/boltons/funcutils.py
+++ b/boltons/funcutils.py
@@ -64,7 +64,7 @@ except AttributeError:
try:
- from boltons.typeutils import make_sentinel
+ from .typeutils import make_sentinel
NO_DEFAULT = make_sentinel(var_name='NO_DEFAULT')
except ImportError:
NO_DEFAULT = object()
@@ -588,7 +588,7 @@ def update_wrapper(wrapper, func, injected=None, expected=None, build_from=None,
in the updated function.
In opposition to the built-in :func:`functools.update_wrapper` bolton's
- version returns a copy of the function and does not modifiy anything in place.
+ version returns a copy of the function and does not modify anything in place.
For more in-depth wrapping of functions, see the
:class:`FunctionBuilder` type, on which update_wrapper was built.
"""
diff --git a/boltons/ioutils.py b/boltons/ioutils.py
index ef3947e..dd67aaf 100644
--- a/boltons/ioutils.py
+++ b/boltons/ioutils.py
@@ -40,7 +40,7 @@ are useful when dealing with input, output, and bytestreams in a variety of
ways.
"""
import os
-from io import BytesIO
+from io import BytesIO, IOBase
from abc import (
ABCMeta,
abstractmethod,
@@ -50,6 +50,11 @@ from errno import EINVAL
from codecs import EncodedFile
from tempfile import TemporaryFile
+try:
+ from itertools import izip_longest as zip_longest # Python 2
+except ImportError:
+ from itertools import zip_longest # Python 3
+
try:
text_type = unicode # Python 2
binary_type = str
@@ -66,16 +71,14 @@ value.
"""
-class SpooledIOBase(object):
+class SpooledIOBase(IOBase):
"""
- The SpooledTempoaryFile class doesn't support a number of attributes and
- methods that a StringIO instance does. This brings the api as close to
- compatible as possible with StringIO so that it may be used as a near
- drop-in replacement to save memory.
-
- Another issue with SpooledTemporaryFile is that the spooled file is always
- a cStringIO rather than a StringIO which causes issues with some of our
- tools.
+ A base class shared by the SpooledBytesIO and SpooledStringIO classes.
+
+ The SpooledTemporaryFile class is missing several attributes and methods
+ present in the StringIO implementation. This brings the api as close to
+ parity as possible so that classes derived from SpooledIOBase can be used
+ as near drop-in replacements to save memory.
"""
__metaclass__ = ABCMeta
@@ -83,6 +86,11 @@ class SpooledIOBase(object):
self._max_size = max_size
self._dir = dir
+ def _checkClosed(self, msg=None):
+ """Raise a ValueError if file is closed"""
+ if self.closed:
+ raise ValueError('I/O operation on closed file.'
+ if msg is None else msg)
@abstractmethod
def read(self, n=-1):
"""Read n characters from the buffer"""
@@ -103,6 +111,16 @@ class SpooledIOBase(object):
def readlines(self, sizehint=0):
"""Returns a list of all lines from the current position forward"""
+ def writelines(self, lines):
+ """
+ Write lines to the file from an interable.
+
+ NOTE: writelines() does NOT add line separators.
+ """
+ self._checkClosed()
+ for line in lines:
+ self.write(line)
+
@abstractmethod
def rollover(self):
"""Roll file-like-object over into a real temporary file"""
@@ -139,22 +157,13 @@ class SpooledIOBase(object):
return self.buffer.close()
def flush(self):
+ self._checkClosed()
return self.buffer.flush()
def isatty(self):
+ self._checkClosed()
return self.buffer.isatty()
- def next(self):
- line = self.readline()
- if not line:
- pos = self.buffer.tell()
- self.buffer.seek(0, os.SEEK_END)
- if pos == self.buffer.tell():
- raise StopIteration
- else:
- self.buffer.seek(pos)
- return line
-
@property
def closed(self):
return self.buffer.closed
@@ -173,10 +182,13 @@ class SpooledIOBase(object):
def truncate(self, size=None):
"""
+ Truncate the contents of the buffer.
+
Custom version of truncate that takes either no arguments (like the
real SpooledTemporaryFile) or a single argument that truncates the
value to a certain index location.
"""
+ self._checkClosed()
if size is None:
return self.buffer.truncate()
@@ -191,7 +203,8 @@ class SpooledIOBase(object):
self.seek(pos)
def getvalue(self):
- """Return the entire files contents"""
+ """Return the entire files contents."""
+ self._checkClosed()
pos = self.tell()
self.seek(0)
val = self.read()
@@ -207,15 +220,29 @@ class SpooledIOBase(object):
def writable(self):
return True
- __next__ = next
+ def __next__(self):
+ self._checkClosed()
+ line = self.readline()
+ if not line:
+ pos = self.buffer.tell()
+ self.buffer.seek(0, os.SEEK_END)
+ if pos == self.buffer.tell():
+ raise StopIteration
+ else:
+ self.buffer.seek(pos)
+ return line
+
+ next = __next__
def __len__(self):
return self.len
def __iter__(self):
+ self._checkClosed()
return self
def __enter__(self):
+ self._checkClosed()
return self
def __exit__(self, *args):
@@ -223,7 +250,31 @@ class SpooledIOBase(object):
def __eq__(self, other):
if isinstance(other, self.__class__):
- return self.getvalue() == other.getvalue()
+ self_pos = self.tell()
+ other_pos = other.tell()
+ try:
+ self.seek(0)
+ other.seek(0)
+ eq = True
+ for self_line, other_line in zip_longest(self, other):
+ if self_line != other_line:
+ eq = False
+ break
+ self.seek(self_pos)
+ other.seek(other_pos)
+ except Exception:
+ # Attempt to return files to original position if there were any errors
+ try:
+ self.seek(self_pos)
+ except Exception:
+ pass
+ try:
+ other.seek(other_pos)
+ except Exception:
+ pass
+ raise
+ else:
+ return eq
return False
def __ne__(self, other):
@@ -232,6 +283,13 @@ class SpooledIOBase(object):
def __bool__(self):
return True
+ def __del__(self):
+ """Can fail when called at program exit so suppress traceback."""
+ try:
+ self.close()
+ except Exception:
+ pass
+
__nonzero__ = __bool__
@@ -253,11 +311,13 @@ class SpooledBytesIO(SpooledIOBase):
"""
def read(self, n=-1):
+ self._checkClosed()
return self.buffer.read(n)
def write(self, s):
+ self._checkClosed()
if not isinstance(s, binary_type):
- raise TypeError("{0} expected, got {1}".format(
+ raise TypeError("{} expected, got {}".format(
binary_type.__name__,
type(s).__name__
))
@@ -267,9 +327,11 @@ class SpooledBytesIO(SpooledIOBase):
self.buffer.write(s)
def seek(self, pos, mode=0):
+ self._checkClosed()
return self.buffer.seek(pos, mode)
def readline(self, length=None):
+ self._checkClosed()
if length:
return self.buffer.readline(length)
else:
@@ -314,6 +376,7 @@ class SpooledBytesIO(SpooledIOBase):
return val
def tell(self):
+ self._checkClosed()
return self.buffer.tell()
@@ -340,13 +403,15 @@ class SpooledStringIO(SpooledIOBase):
super(SpooledStringIO, self).__init__(*args, **kwargs)
def read(self, n=-1):
+ self._checkClosed()
ret = self.buffer.reader.read(n, n)
self._tell = self.tell() + len(ret)
return ret
def write(self, s):
+ self._checkClosed()
if not isinstance(s, text_type):
- raise TypeError("{0} expected, got {1}".format(
+ raise TypeError("{} expected, got {}".format(
text_type.__name__,
type(s).__name__
))
@@ -384,6 +449,7 @@ class SpooledStringIO(SpooledIOBase):
def seek(self, pos, mode=0):
"""Traverse from offset to the specified codepoint"""
+ self._checkClosed()
# Seek to position from the start of the file
if mode == os.SEEK_SET:
self.buffer.seek(0)
@@ -406,6 +472,7 @@ class SpooledStringIO(SpooledIOBase):
return self.tell()
def readline(self, length=None):
+ self._checkClosed()
ret = self.buffer.readline(length).decode('utf-8')
self._tell = self.tell() + len(ret)
return ret
@@ -428,7 +495,7 @@ class SpooledStringIO(SpooledIOBase):
return not isinstance(self.buffer.stream, BytesIO)
def rollover(self):
- """Roll the StringIO over to a TempFile"""
+ """Roll the buffer over to a TempFile"""
if not self._rolled:
tmp = EncodedFile(TemporaryFile(dir=self._dir),
data_encoding='utf-8')
@@ -440,6 +507,7 @@ class SpooledStringIO(SpooledIOBase):
def tell(self):
"""Return the codepoint position"""
+ self._checkClosed()
return self._tell
@property
diff --git a/boltons/iterutils.py b/boltons/iterutils.py
index a485faf..eddbeff 100644
--- a/boltons/iterutils.py
+++ b/boltons/iterutils.py
@@ -55,7 +55,7 @@ except ImportError:
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_UNSET = make_sentinel('_UNSET')
_REMAP_EXIT = make_sentinel('_REMAP_EXIT')
except ImportError:
@@ -208,7 +208,7 @@ def split_iter(src, sep=None, maxsplit=None):
def lstrip(iterable, strip_value=None):
"""Strips values from the beginning of an iterable. Stripped items will
- match the value of the argument strip_value. Functionality is analigous
+ match the value of the argument strip_value. Functionality is analogous
to that of the method str.lstrip. Returns a list.
>>> lstrip(['Foo', 'Bar', 'Bam'], 'Foo')
@@ -220,7 +220,7 @@ def lstrip(iterable, strip_value=None):
def lstrip_iter(iterable, strip_value=None):
"""Strips values from the beginning of an iterable. Stripped items will
- match the value of the argument strip_value. Functionality is analigous
+ match the value of the argument strip_value. Functionality is analogous
to that of the method str.lstrip. Returns a generator.
>>> list(lstrip_iter(['Foo', 'Bar', 'Bam'], 'Foo'))
@@ -238,7 +238,7 @@ def lstrip_iter(iterable, strip_value=None):
def rstrip(iterable, strip_value=None):
"""Strips values from the end of an iterable. Stripped items will
- match the value of the argument strip_value. Functionality is analigous
+ match the value of the argument strip_value. Functionality is analogous
to that of the method str.rstrip. Returns a list.
>>> rstrip(['Foo', 'Bar', 'Bam'], 'Bam')
@@ -250,7 +250,7 @@ def rstrip(iterable, strip_value=None):
def rstrip_iter(iterable, strip_value=None):
"""Strips values from the end of an iterable. Stripped items will
- match the value of the argument strip_value. Functionality is analigous
+ match the value of the argument strip_value. Functionality is analogous
to that of the method str.rstrip. Returns a generator.
>>> list(rstrip_iter(['Foo', 'Bar', 'Bam'], 'Bam'))
@@ -279,7 +279,7 @@ def rstrip_iter(iterable, strip_value=None):
def strip(iterable, strip_value=None):
"""Strips values from the beginning and end of an iterable. Stripped items
will match the value of the argument strip_value. Functionality is
- analigous to that of the method str.strip. Returns a list.
+ analogous to that of the method str.strip. Returns a list.
>>> strip(['Fu', 'Foo', 'Bar', 'Bam', 'Fu'], 'Fu')
['Foo', 'Bar', 'Bam']
@@ -291,7 +291,7 @@ def strip(iterable, strip_value=None):
def strip_iter(iterable,strip_value=None):
"""Strips values from the beginning and end of an iterable. Stripped items
will match the value of the argument strip_value. Functionality is
- analigous to that of the method str.strip. Returns a generator.
+ analogous to that of the method str.strip. Returns a generator.
>>> list(strip_iter(['Fu', 'Foo', 'Bar', 'Bam', 'Fu'], 'Fu'))
['Foo', 'Bar', 'Bam']
@@ -323,6 +323,13 @@ def chunked(src, size, count=None, **kw):
return list(itertools.islice(chunk_iter, count))
+def _validate_positive_int(value, name, strictly_positive=True):
+ value = int(value)
+ if value < 0 or (strictly_positive and value == 0):
+ raise ValueError('expected a positive integer ' + name)
+ return value
+
+
def chunked_iter(src, size, **kw):
"""Generates *size*-sized chunks from *src* iterable. Unless the
optional *fill* keyword argument is provided, iterables not evenly
@@ -339,9 +346,7 @@ def chunked_iter(src, size, **kw):
# TODO: add count kwarg?
if not is_iterable(src):
raise TypeError('expected an iterable')
- size = int(size)
- if size <= 0:
- raise ValueError('expected a positive integer chunk size')
+ size = _validate_positive_int(size, 'chunk size')
do_fill = True
try:
fill_val = kw.pop('fill')
@@ -369,6 +374,56 @@ def chunked_iter(src, size, **kw):
return
+def chunk_ranges(input_size, chunk_size, input_offset=0, overlap_size=0, align=False):
+ """Generates *chunk_size*-sized chunk ranges for an input with length *input_size*.
+ Optionally, a start of the input can be set via *input_offset*, and
+ and overlap between the chunks may be specified via *overlap_size*.
+ Also, if *align* is set to *True*, any items with *i % (chunk_size-overlap_size) == 0*
+ are always at the beginning of the chunk.
+
+ Returns an iterator of (start, end) tuples, one tuple per chunk.
+
+ >>> list(chunk_ranges(input_offset=10, input_size=10, chunk_size=5))
+ [(10, 15), (15, 20)]
+ >>> list(chunk_ranges(input_offset=10, input_size=10, chunk_size=5, overlap_size=1))
+ [(10, 15), (14, 19), (18, 20)]
+ >>> list(chunk_ranges(input_offset=10, input_size=10, chunk_size=5, overlap_size=2))
+ [(10, 15), (13, 18), (16, 20)]
+
+ >>> list(chunk_ranges(input_offset=4, input_size=15, chunk_size=5, align=False))
+ [(4, 9), (9, 14), (14, 19)]
+ >>> list(chunk_ranges(input_offset=4, input_size=15, chunk_size=5, align=True))
+ [(4, 5), (5, 10), (10, 15), (15, 19)]
+
+ >>> list(chunk_ranges(input_offset=2, input_size=15, chunk_size=5, overlap_size=1, align=False))
+ [(2, 7), (6, 11), (10, 15), (14, 17)]
+ >>> list(chunk_ranges(input_offset=2, input_size=15, chunk_size=5, overlap_size=1, align=True))
+ [(2, 5), (4, 9), (8, 13), (12, 17)]
+ >>> list(chunk_ranges(input_offset=3, input_size=15, chunk_size=5, overlap_size=1, align=True))
+ [(3, 5), (4, 9), (8, 13), (12, 17), (16, 18)]
+ """
+ input_size = _validate_positive_int(input_size, 'input_size', strictly_positive=False)
+ chunk_size = _validate_positive_int(chunk_size, 'chunk_size')
+ input_offset = _validate_positive_int(input_offset, 'input_offset', strictly_positive=False)
+ overlap_size = _validate_positive_int(overlap_size, 'overlap_size', strictly_positive=False)
+
+ input_stop = input_offset + input_size
+
+ if align:
+ initial_chunk_len = chunk_size - input_offset % (chunk_size - overlap_size)
+ if initial_chunk_len != overlap_size:
+ yield (input_offset, min(input_offset + initial_chunk_len, input_stop))
+ if input_offset + initial_chunk_len >= input_stop:
+ return
+ input_offset = input_offset + initial_chunk_len - overlap_size
+
+ for i in range(input_offset, input_stop, chunk_size - overlap_size):
+ yield (i, min(i + chunk_size, input_stop))
+
+ if i + chunk_size >= input_stop:
+ return
+
+
def pairwise(src):
"""Convenience function for calling :func:`windowed` on *src*, with
*size* set to 2.
@@ -981,7 +1036,7 @@ def remap(root, visit=default_visit, enter=default_enter, exit=default_exit,
**kwargs):
"""The remap ("recursive map") function is used to traverse and
transform nested structures. Lists, tuples, sets, and dictionaries
- are just a few of the data structures nested into heterogenous
+ are just a few of the data structures nested into heterogeneous
tree-like structures that are so common in programming.
Unfortunately, Python's built-in ways to manipulate collections
are almost all flat. List comprehensions may be fast and succinct,
@@ -1451,7 +1506,7 @@ def soft_sorted(iterable, first=None, last=None, key=None, reverse=False):
def untyped_sorted(iterable, key=None, reverse=False):
"""A version of :func:`sorted` which will happily sort an iterable of
- heterogenous types and return a new list, similar to legacy Python's
+ heterogeneous types and return a new list, similar to legacy Python's
behavior.
>>> untyped_sorted(['abc', 2.0, 1, 2, 'def'])
diff --git a/boltons/listutils.py b/boltons/listutils.py
index 290ace1..e1bd3cd 100644
--- a/boltons/listutils.py
+++ b/boltons/listutils.py
@@ -47,7 +47,7 @@ from math import log as math_log
from itertools import chain, islice
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_MISSING = make_sentinel(var_name='_MISSING')
except ImportError:
_MISSING = object()
diff --git a/boltons/mathutils.py b/boltons/mathutils.py
index 145afb6..bf7e95e 100644
--- a/boltons/mathutils.py
+++ b/boltons/mathutils.py
@@ -215,7 +215,7 @@ class Bits(object):
return hash(self.val)
def as_list(self):
- return [c == '1' for c in '{0:b}'.format(self.val)]
+ return [c == '1' for c in self.as_bin()]
def as_bin(self):
return '{{0:0{0}b}}'.format(self.len).format(self.val)
diff --git a/boltons/queueutils.py b/boltons/queueutils.py
index 42adb14..56bf591 100644
--- a/boltons/queueutils.py
+++ b/boltons/queueutils.py
@@ -70,13 +70,13 @@ from bisect import insort
import itertools
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_REMOVED = make_sentinel(var_name='_REMOVED')
except ImportError:
_REMOVED = object()
try:
- from listutils import BList
+ from .listutils import BList
# see BarrelList docstring for notes
except ImportError:
BList = list
diff --git a/boltons/setutils.py b/boltons/setutils.py
index 16e5d74..5ee697e 100644
--- a/boltons/setutils.py
+++ b/boltons/setutils.py
@@ -53,7 +53,7 @@ except ImportError:
from collections import MutableSet
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_MISSING = make_sentinel(var_name='_MISSING')
except ImportError:
_MISSING = object()
diff --git a/boltons/socketutils.py b/boltons/socketutils.py
index d1c7233..4944227 100644
--- a/boltons/socketutils.py
+++ b/boltons/socketutils.py
@@ -82,7 +82,7 @@ except Exception:
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_UNSET = make_sentinel(var_name='_UNSET')
except ImportError:
_UNSET = object()
diff --git a/boltons/strutils.py b/boltons/strutils.py
index f94aefb..5b5b347 100644
--- a/boltons/strutils.py
+++ b/boltons/strutils.py
@@ -1176,7 +1176,7 @@ class MultiReplace(object):
Dictionary Usage::
- from lrmslib import stringutils
+ from boltons import stringutils
s = stringutils.MultiReplace({
'foo': 'zoo',
'cat': 'hat',
@@ -1187,7 +1187,7 @@ class MultiReplace(object):
Iterable Usage::
- from lrmslib import stringutils
+ from boltons import stringutils
s = stringutils.MultiReplace([
('foo', 'zoo'),
('cat', 'hat'),
@@ -1239,10 +1239,7 @@ class MultiReplace(object):
else:
exp = vals[0].pattern
- regex_values.append('(?P<{0}>{1})'.format(
- group_name,
- exp
- ))
+ regex_values.append('(?P<{}>{})'.format(group_name, exp))
self.group_map[group_name] = vals[1]
self.combined_pattern = re.compile(
@@ -1267,7 +1264,18 @@ class MultiReplace(object):
def multi_replace(text, sub_map, **kwargs):
- """Shortcut function to invoke MultiReplace in a single call."""
+ """
+ Shortcut function to invoke MultiReplace in a single call.
+
+ Example Usage::
+
+ from boltons.stringutils import multi_replace
+ new = multi_replace(
+ 'The foo bar cat ate a bat',
+ {'foo': 'zoo', 'cat': 'hat', 'bat': 'kraken'}
+ )
+ new == 'The zoo bar hat ate a kraken'
+ """
m = MultiReplace(sub_map, **kwargs)
return m.sub(text)
diff --git a/boltons/tableutils.py b/boltons/tableutils.py
index 13234a5..22c896f 100644
--- a/boltons/tableutils.py
+++ b/boltons/tableutils.py
@@ -72,7 +72,7 @@ except NameError:
from html import escape as html_escape
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_MISSING = make_sentinel(var_name='_MISSING')
except ImportError:
_MISSING = object()
diff --git a/boltons/timeutils.py b/boltons/timeutils.py
index 8d360d0..65699dc 100644
--- a/boltons/timeutils.py
+++ b/boltons/timeutils.py
@@ -377,10 +377,12 @@ def daterange(start, stop, step=1, inclusive=False):
else:
raise ValueError('step expected int, timedelta, or tuple'
' (year, month, day), not: %r' % step)
+
+ m_step += y_step * 12
if stop is None:
finished = lambda now, stop: False
- elif start < stop:
+ elif start <= stop:
finished = operator.gt if inclusive else operator.ge
else:
finished = operator.lt if inclusive else operator.le
@@ -388,10 +390,10 @@ def daterange(start, stop, step=1, inclusive=False):
while not finished(now, stop):
yield now
- if y_step or m_step:
- m_y_step, cur_month = divmod(now.month + m_step, 12)
- now = now.replace(year=now.year + y_step + m_y_step,
- month=cur_month or 12)
+ if m_step:
+ m_y_step, cur_month = divmod((now.month - 1) + m_step, 12)
+ now = now.replace(year=now.year + m_y_step,
+ month=(cur_month + 1))
now = now + d_step
return
diff --git a/boltons/urlutils.py b/boltons/urlutils.py
index 6016e8a..f21e505 100644
--- a/boltons/urlutils.py
+++ b/boltons/urlutils.py
@@ -685,7 +685,8 @@ class URL(object):
if dest.path.startswith(u'/'): # absolute path
new_path_parts = list(dest.path_parts)
else: # relative path
- new_path_parts = self.path_parts[:-1] + dest.path_parts
+ new_path_parts = list(self.path_parts[:-1]) \
+ + list(dest.path_parts)
else:
new_path_parts = list(self.path_parts)
if not query_params:
@@ -994,7 +995,7 @@ except ImportError:
from itertools import zip_longest as izip_longest
try:
- from typeutils import make_sentinel
+ from .typeutils import make_sentinel
_MISSING = make_sentinel(var_name='_MISSING')
except ImportError:
_MISSING = object()
@@ -1537,7 +1538,7 @@ class OrderedMultiDict(dict):
try:
# try to import the built-in one anyways
- from boltons.dictutils import OrderedMultiDict
+ from .dictutils import OrderedMultiDict
except ImportError:
pass
diff --git a/debian/changelog b/debian/changelog
index 5bf440b..a10a39e 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+python-boltons (23.0.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+ * Drop patch address-ecoutils-import-issue-fixes-294.patch, present upstream.
+ * Drop patch python3.11.patch, present upstream.
+
+ -- Debian Janitor <janitor@jelmer.uk> Fri, 10 Mar 2023 22:36:41 -0000
+
python-boltons (21.0.0-2) unstable; urgency=medium
* Team upload.
diff --git a/debian/patches/address-ecoutils-import-issue-fixes-294.patch b/debian/patches/address-ecoutils-import-issue-fixes-294.patch
deleted file mode 100644
index d6bdd9f..0000000
--- a/debian/patches/address-ecoutils-import-issue-fixes-294.patch
+++ /dev/null
@@ -1,118 +0,0 @@
-From: Mahmoud Hashemi <mahmoud@hatnote.com>
-Date: Sun, 10 Oct 2021 23:26:24 -0700
-Subject: address ecoutils import issue, fixes #294
-
-Origin: upstream, https://github.com/mahmoud/boltons/commit/270e974975984f662f998c8f6eb0ebebd964de82
-Bug-Debian: https://bugs.debian.org/1002347
----
- boltons/ecoutils.py | 87 +++++++++++++++++++++++++++--------------------------
- 1 file changed, 44 insertions(+), 43 deletions(-)
-
-diff --git a/boltons/ecoutils.py b/boltons/ecoutils.py
-index 0ccad70..91b9412 100644
---- a/boltons/ecoutils.py
-+++ b/boltons/ecoutils.py
-@@ -354,38 +354,53 @@ def get_profile(**kwargs):
- return ret
-
-
--_real_safe_repr = pprint._safe_repr
--
--
--def _fake_json_dumps(val, indent=2):
-- # never do this. this is a hack for Python 2.4. Python 2.5 added
-- # the json module for a reason.
-- def _fake_safe_repr(*a, **kw):
-- res, is_read, is_rec = _real_safe_repr(*a, **kw)
-- if res == 'None':
-- res = 'null'
-- if res == 'True':
-- res = 'true'
-- if res == 'False':
-- res = 'false'
-- if not (res.startswith("'") or res.startswith("u'")):
-- res = res
-- else:
-- if res.startswith('u'):
-- res = res[1:]
-+try:
-+ import json
-+
-+ def dumps(val, indent):
-+ if indent:
-+ return json.dumps(val, sort_keys=True, indent=indent)
-+ return json.dumps(val, sort_keys=True)
-+
-+except ImportError:
-+ _real_safe_repr = pprint._safe_repr
-+
-+ def _fake_json_dumps(val, indent=2):
-+ # never do this. this is a hack for Python 2.4. Python 2.5 added
-+ # the json module for a reason.
-+ def _fake_safe_repr(*a, **kw):
-+ res, is_read, is_rec = _real_safe_repr(*a, **kw)
-+ if res == 'None':
-+ res = 'null'
-+ if res == 'True':
-+ res = 'true'
-+ if res == 'False':
-+ res = 'false'
-+ if not (res.startswith("'") or res.startswith("u'")):
-+ res = res
-+ else:
-+ if res.startswith('u'):
-+ res = res[1:]
-
-- contents = res[1:-1]
-- contents = contents.replace('"', '').replace(r'\"', '')
-- res = '"' + contents + '"'
-- return res, is_read, is_rec
-+ contents = res[1:-1]
-+ contents = contents.replace('"', '').replace(r'\"', '')
-+ res = '"' + contents + '"'
-+ return res, is_read, is_rec
-
-- pprint._safe_repr = _fake_safe_repr
-- try:
-- ret = pprint.pformat(val, indent=indent)
-- finally:
-- pprint._safe_repr = _real_safe_repr
-+ pprint._safe_repr = _fake_safe_repr
-+ try:
-+ ret = pprint.pformat(val, indent=indent)
-+ finally:
-+ pprint._safe_repr = _real_safe_repr
-+
-+ return ret
-+
-+ def dumps(val, indent):
-+ ret = _fake_json_dumps(val, indent=indent)
-+ if not indent:
-+ ret = re.sub(r'\n\s*', ' ', ret)
-+ return ret
-
-- return ret
-
-
- def get_profile_json(indent=False):
-@@ -393,20 +408,6 @@ def get_profile_json(indent=False):
- indent = 2
- else:
- indent = 0
-- try:
-- import json
--
-- def dumps(val, indent):
-- if indent:
-- return json.dumps(val, sort_keys=True, indent=indent)
-- return json.dumps(val, sort_keys=True)
--
-- except ImportError:
-- def dumps(val, indent):
-- ret = _fake_json_dumps(val, indent=indent)
-- if not indent:
-- ret = re.sub(r'\n\s*', ' ', ret)
-- return ret
-
- data_dict = get_profile()
- return dumps(data_dict, indent)
diff --git a/debian/patches/python3.11.patch b/debian/patches/python3.11.patch
deleted file mode 100644
index 6046743..0000000
--- a/debian/patches/python3.11.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-From: Stefano Rivera <stefano@rivera.za.net>
-Date: Sun, 25 Dec 2022 10:40:05 -0400
-Subject: Add __getstate__ to through_methods (bpo-26579)
-
-Adds support for Python 3.11.
-
-Bug-Debian: https://bugs.debian.org/1025023
-Forwarded: https://github.com/mahmoud/boltons/pull/323
----
- tests/test_dictutils.py | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/tests/test_dictutils.py b/tests/test_dictutils.py
-index ca59a31..0044001 100644
---- a/tests/test_dictutils.py
-+++ b/tests/test_dictutils.py
-@@ -474,6 +474,7 @@ def test_frozendict_api():
- '__ge__',
- '__getattribute__',
- '__getitem__',
-+ '__getstate__',
- '__gt__',
- '__init__',
- '__iter__',
diff --git a/debian/patches/series b/debian/patches/series
index 7a900d5..e69de29 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,2 +0,0 @@
-address-ecoutils-import-issue-fixes-294.patch
-python3.11.patch
diff --git a/docs/conf.py b/docs/conf.py
index a93823b..e66f88a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -97,11 +97,11 @@ master_doc = 'index'
# General information about the project.
project = u'boltons'
-copyright = u'2020, Mahmoud Hashemi'
+copyright = u'2023, Mahmoud Hashemi'
author = u'Mahmoud Hashemi'
-version = '21.0'
-release = '21.0.0'
+version = '23.0'
+release = '23.0.0'
if os.name != 'nt':
today_fmt = '%B %d, %Y'
diff --git a/docs/index.rst b/docs/index.rst
index ca1c9a5..4ebcdcf 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -68,7 +68,7 @@ consider other integration options. See the :ref:`Integration
<arch_integration>` section of the architecture document for more
details.
-Boltons is tested against Python 2.6-2.7, 3.4-3.7, and PyPy.
+Boltons is tested against Python 3.7-3.11, as well as Python 2.7 and PyPy3.
.. _MacPorts: https://ports.macports.org/port/py-boltons/summary
diff --git a/docs/iterutils.rst b/docs/iterutils.rst
index 1cb5c75..23165a0 100644
--- a/docs/iterutils.rst
+++ b/docs/iterutils.rst
@@ -18,6 +18,7 @@ present in the standard library.
.. autofunction:: chunked
.. autofunction:: chunked_iter
+.. autofunction:: chunk_ranges
.. autofunction:: pairwise
.. autofunction:: pairwise_iter
.. autofunction:: windowed
diff --git a/requirements-test.txt b/requirements-test.txt
index f030ce8..ae05d73 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,4 +1,4 @@
-coverage==4.5.1
-pytest==3.0.7
-pytest-cov==2.5.1
+coverage==6.5.0
+pytest==7.2.0
+pytest-cov==4.0.0
tox<3.0
diff --git a/setup.py b/setup.py
index f4b8723..86971b2 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@ from setuptools import setup
__author__ = 'Mahmoud Hashemi'
-__version__ = '21.0.0'
+__version__ = '23.0.0'
__contact__ = 'mahmoud@hatnote.com'
__url__ = 'https://github.com/mahmoud/boltons'
__license__ = 'BSD'
@@ -42,15 +42,13 @@ setup(name='boltons',
# List of python versions and their support status:
# https://en.wikipedia.org/wiki/CPython#Version_history
'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
'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',
'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy', ]
)
diff --git a/tests/test_cacheutils.py b/tests/test_cacheutils.py
index cac47f9..91ebcf9 100644
--- a/tests/test_cacheutils.py
+++ b/tests/test_cacheutils.py
@@ -38,7 +38,7 @@ def test_lri():
for char in string.ascii_letters[least_recent_insert_index+1:idx]:
assert char in bc
- # test that reinserting an exising key changes eviction behavior
+ # test that reinserting an existing key changes eviction behavior
bc[string.ascii_letters[-cache_size+1]] = "new value"
least_recently_inserted_key = string.ascii_letters[-cache_size+2]
bc["unreferenced_key"] = "value"
diff --git a/tests/test_dictutils.py b/tests/test_dictutils.py
index ca59a31..ae046f6 100644
--- a/tests/test_dictutils.py
+++ b/tests/test_dictutils.py
@@ -473,7 +473,9 @@ def test_frozendict_api():
'__format__',
'__ge__',
'__getattribute__',
+ '__getstate__',
'__getitem__',
+ '__getstate__',
'__gt__',
'__init__',
'__iter__',
diff --git a/tests/test_formatutils.py b/tests/test_formatutils.py
index fee81a7..d7bfaa9 100644
--- a/tests/test_formatutils.py
+++ b/tests/test_formatutils.py
@@ -29,15 +29,8 @@ _TEST_TMPLS = ["example 1: {hello}",
"example 2: {hello:*10}",
"example 3: {hello:*{width}}",
"example 4: {hello!r:{fchar}{width}}, {width}, yes",
- "example 5: {0}, {1:d}, {2:f}, {1}"]
-
-try:
- from collections import OrderedDict
-except ImportError:
- pass # skip the non-2.6 compatible tests on 2.6
-else:
- _TEST_TMPLS.append("example 6: {}, {}, {}, {1}")
- del OrderedDict
+ "example 5: {0}, {1:d}, {2:f}, {1}",
+ "example 6: {}, {}, {}, {1}"]
def test_get_fstr_args():
@@ -45,8 +38,7 @@ def test_get_fstr_args():
for t in _TEST_TMPLS:
inferred_t = infer_positional_format_args(t)
res = get_format_args(inferred_t)
- results.append(res)
- return results
+ assert res
def test_split_fstr():
@@ -54,7 +46,7 @@ def test_split_fstr():
for t in _TEST_TMPLS:
res = split_format_str(t)
results.append(res)
- return results
+ assert res
def test_tokenize_format_str():
@@ -62,7 +54,7 @@ def test_tokenize_format_str():
for t in _TEST_TMPLS:
res = tokenize_format_str(t)
results.append(res)
- return results
+ assert res
def test_deferredvalue():
diff --git a/tests/test_ioutils.py b/tests/test_ioutils.py
index b14dd66..df672ce 100644
--- a/tests/test_ioutils.py
+++ b/tests/test_ioutils.py
@@ -75,6 +75,29 @@ class BaseTestMixin(object):
finally:
os.rmdir(custom_dir)
+ def test_compare_err(self):
+ """Read-heads are reset if a comparison raises an error."""
+ def _monkey_err(*args, **kwargs):
+ raise Exception('A sad error has occurred today')
+
+ a = self.spooled_flo.__class__()
+ a.write(self.test_str)
+ b = self.spooled_flo.__class__()
+ b.write(self.test_str)
+
+ a.seek(1)
+ b.seek(2)
+
+ b.__next__ = _monkey_err
+
+ try:
+ a == b
+ except Exception:
+ pass
+
+ self.assertEqual(a.tell(), 1)
+ self.assertEqual(b.tell(), 2)
+
def test_truncate_noargs_norollover(self):
"""Test truncating with no args with in-memory flo"""
self.spooled_flo.write(self.test_str)
@@ -190,6 +213,23 @@ class BaseTestMixin(object):
if not self.spooled_flo:
raise AssertionError("Instance is not truthy")
+ def test_instance_check(self):
+ """Instance checks against IOBase succeed."""
+ if not isinstance(self.spooled_flo, io.IOBase):
+ raise AssertionError('{} is not an instance of IOBase'.format(type(self.spooled_flo)))
+
+ def test_closed_file_method_valueerrors(self):
+ """ValueError raised on closed files for certain methods."""
+ self.spooled_flo.close()
+ methods = (
+ 'flush', 'isatty', 'pos', 'buf', 'truncate', '__next__', '__iter__',
+ '__enter__', 'read', 'readline', 'tell',
+ )
+ for method_name in methods:
+ with self.assertRaises(ValueError):
+ getattr(self.spooled_flo, method_name)()
+
+
class TestSpooledBytesIO(TestCase, BaseTestMixin, AssertionsMixin):
linesep = os.linesep.encode('ascii')
@@ -270,6 +310,13 @@ class TestSpooledBytesIO(TestCase, BaseTestMixin, AssertionsMixin):
self.spooled_flo.seek(0)
self.assertEqual([x for x in self.spooled_flo], [b"a\n", b"b"])
+ def test_writelines(self):
+ """An iterable of lines can be written"""
+ lines = [b"1", b"2", b"3"]
+ expected = b"123"
+ self.spooled_flo.writelines(lines)
+ self.assertEqual(self.spooled_flo.getvalue(), expected)
+
class TestSpooledStringIO(TestCase, BaseTestMixin, AssertionsMixin):
linesep = os.linesep
@@ -408,6 +455,13 @@ class TestSpooledStringIO(TestCase, BaseTestMixin, AssertionsMixin):
self.spooled_flo.seek(0)
self.assertEqual([x for x in self.spooled_flo], [u"a\n", u"b"])
+ def test_writelines(self):
+ """An iterable of lines can be written"""
+ lines = [u"1", u"2", u"3"]
+ expected = u"123"
+ self.spooled_flo.writelines(lines)
+ self.assertEqual(self.spooled_flo.getvalue(), expected)
+
class TestMultiFileReader(TestCase):
def test_read_seek_bytes(self):
diff --git a/tests/test_iterutils.py b/tests/test_iterutils.py
index 0896f38..2738d4b 100644
--- a/tests/test_iterutils.py
+++ b/tests/test_iterutils.py
@@ -511,6 +511,22 @@ def test_chunked_bytes():
assert chunked(b'123', 2) in (['12', '3'], [b'12', b'3'])
+def test_chunk_ranges():
+ from boltons.iterutils import chunk_ranges
+
+ assert list(chunk_ranges(input_offset=10, input_size=10, chunk_size=5)) == [(10, 15), (15, 20)]
+ assert list(chunk_ranges(input_offset=10, input_size=10, chunk_size=5, overlap_size=1)) == [(10, 15), (14, 19), (18, 20)]
+ assert list(chunk_ranges(input_offset=10, input_size=10, chunk_size=5, overlap_size=2)) == [(10, 15), (13, 18), (16, 20)]
+
+ assert list(chunk_ranges(input_offset=4, input_size=15, chunk_size=5, align=False)) == [(4, 9), (9, 14), (14, 19)]
+ assert list(chunk_ranges(input_offset=4, input_size=15, chunk_size=5, align=True)) == [(4, 5), (5, 10), (10, 15), (15, 19)]
+
+ assert list(chunk_ranges(input_offset=2, input_size=15, chunk_size=5, overlap_size=1, align=False)) == [(2, 7), (6, 11), (10, 15), (14, 17)]
+ assert list(chunk_ranges(input_offset=2, input_size=15, chunk_size=5, overlap_size=1, align=True)) == [(2, 5), (4, 9), (8, 13), (12, 17)]
+ assert list(chunk_ranges(input_offset=3, input_size=15, chunk_size=5, overlap_size=1, align=True)) == [(3, 5), (4, 9), (8, 13), (12, 17), (16, 18)]
+ assert list(chunk_ranges(input_offset=3, input_size=2, chunk_size=5, overlap_size=1, align=True)) == [(3, 5)]
+
+
def test_lstrip():
from boltons.iterutils import lstrip
diff --git a/tests/test_mathutils.py b/tests/test_mathutils.py
index 40ae40a..42915e9 100644
--- a/tests/test_mathutils.py
+++ b/tests/test_mathutils.py
@@ -95,15 +95,19 @@ def test_bits():
chk(Bits('11') >> 1, Bits('1'))
chk(Bits('1') << 1, Bits('10'))
assert Bits('0') != Bits('00')
+ # test roundtrip as_/from_hex
+ chk(Bits.from_hex(Bits('10101010').as_hex()),
+ Bits('10101010'))
+ # test roundtrip as_/from_bytes
chk(
- Bits.from_bytes(
- Bits.from_int(
- Bits.from_hex(
- Bits.from_bin(
- Bits.from_list(
- Bits('101').as_list()
- ).as_bin()
- ).as_hex()
- ).as_int()
- ).as_bytes()
- ), Bits('00000101'))
+ Bits.from_bytes(Bits('10101010').as_bytes()),
+ Bits('10101010'))
+ # pile of roundtripping
+ chk(Bits.from_int(
+ Bits.from_bin(
+ Bits.from_list(
+ Bits('101').as_list()
+ ).as_bin()
+ ).as_int()
+ ),
+ Bits('101'))
diff --git a/tests/test_socketutils.py b/tests/test_socketutils.py
index dd095eb..54cee6a 100644
--- a/tests/test_socketutils.py
+++ b/tests/test_socketutils.py
@@ -377,7 +377,7 @@ def test_socketutils_netstring():
def netstring_server_timeout_override(server_socket):
"""Netstring socket has an unreasonably low timeout,
- however it should be overriden by the `read_ns` argument."""
+ however it should be overridden by the `read_ns` argument."""
try:
while True:
@@ -398,9 +398,9 @@ def netstring_server_timeout_override(server_socket):
def test_socketutils_netstring_timeout():
- """Tests that server socket timeout is overriden by the argument to read call.
+ """Tests that server socket timeout is overridden by the argument to read call.
- Server has timeout of 10 ms, and we will sleep for 20 ms. If timeout is not overriden correctly,
+ Server has timeout of 10 ms, and we will sleep for 20 ms. If timeout is not overridden correctly,
a timeout exception will be raised."""
print("running timeout test")
@@ -426,4 +426,4 @@ def test_socketutils_netstring_timeout():
assert client.read_ns() == b'pong'
client.write_ns(b'shutdown')
- print("no timeout occured - all good.")
\ No newline at end of file
+ print("no timeout occurred - all good.")
\ No newline at end of file
diff --git a/tests/test_timeutils.py b/tests/test_timeutils.py
index 3e91ce6..9f0e19a 100644
--- a/tests/test_timeutils.py
+++ b/tests/test_timeutils.py
@@ -1,5 +1,8 @@
from datetime import timedelta, date
+
+import pytest
+
from boltons.timeutils import total_seconds, daterange
@@ -48,8 +51,35 @@ def test_daterange_years():
assert years_from_2025[0] == date(2025, 1, 1)
assert years_from_2025[-1] == date(2017, 1, 1)
+
+def test_daterange_years_step():
+ start_day = date(year=2012, month=12, day=25)
+ end_day = date(year=2016, month=1, day=1)
+ dates = list(daterange(start_day, end_day, step=(1, 0, 0), inclusive=False))
+ expected = [date(year=2012, month=12, day=25), date(year=2013, month=12, day=25), date(year=2014, month=12, day=25), date(year=2015, month=12, day=25)]
+
+ assert dates == expected
+
+ dates = list(daterange(start_day, end_day, step=(0, 13, 0), inclusive=False))
+ expected = [date(year=2012, month=12, day=25), date(year=2014, month=1, day=25), date(year=2015, month=2, day=25)]
+ assert dates == expected
+
+
def test_daterange_infinite():
today = date.today()
infinite_dates = daterange(today, None)
for i in range(10):
assert next(infinite_dates) == today + timedelta(days=i)
+
+
+def test_daterange_with_same_start_stop():
+ today = date.today()
+
+ date_range = daterange(today, today)
+ with pytest.raises(StopIteration):
+ next(date_range)
+
+ date_range_inclusive = daterange(today, today, inclusive=True)
+ assert next(date_range_inclusive) == today
+ with pytest.raises(StopIteration):
+ next(date_range_inclusive)
diff --git a/tests/test_urlutils.py b/tests/test_urlutils.py
index a8e1539..d551ae0 100644
--- a/tests/test_urlutils.py
+++ b/tests/test_urlutils.py
@@ -99,7 +99,7 @@ def test_idna():
def test_query_params(test_url):
url_obj = URL(test_url)
if not url_obj.query_params or url_obj.fragment:
- return True
+ return
qp_text = url_obj.query_params.to_text(full_quote=True)
assert test_url.endswith(qp_text)
@@ -313,6 +313,25 @@ def test_navigate():
assert navd.to_text() == _dest_text
+@pytest.mark.parametrize(
+ ('expected', 'base', 'paths'), [
+ ('https://host/b', 'https://host', ('a', '/b', )),
+ ('https://host/b', 'https://host', ('a', 'b', )),
+ ('https://host/a/b', 'https://host', ('a/', 'b', )),
+ ('https://host/b', 'https://host', ('/a', 'b', )),
+ ('https://host/a/b', 'https://host/a/', (None, 'b', )),
+ ('https://host/b', 'https://host/a', (None, 'b', )),
+])
+def test_chained_navigate(expected, base, paths):
+ """Chained :meth:`navigate` calls produces correct results."""
+ url = URL(base)
+
+ for path in paths:
+ url = url.navigate(path)
+
+ assert expected == url.to_text()
+
+
# TODO: RFC3986 6.2.3 (not just for query add, either)
# def test_add_query():
# url = URL('http://www.example.com')
diff --git a/tox.ini b/tox.ini
index 1392927..64e32f1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,13 @@
[tox]
-envlist = py27,py34,py37,py39,pypy
+envlist = py27,py37,py39,py310,py311,pypy3
[testenv]
changedir = .tox
deps = -rrequirements-test.txt
commands = py.test --doctest-modules {envsitepackagesdir}/boltons {toxinidir}/tests {posargs}
+
+[testenv:py27]
+deps =
+ coverage==5.5
+ pytest==4.6.11
+ pytest-cov==2.12
+
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-23.0.0.egg-info/PKG-INFO -rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-23.0.0.egg-info/dependency_links.txt -rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-23.0.0.egg-info/not-zip-safe -rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-23.0.0.egg-info/top_level.txt
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-21.0.0.egg-info/PKG-INFO -rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-21.0.0.egg-info/dependency_links.txt -rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-21.0.0.egg-info/not-zip-safe -rw-r--r-- root/root /usr/lib/python3/dist-packages/boltons-21.0.0.egg-info/top_level.txt
No differences were encountered in the control files