Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-humanfriendly for 
openSUSE:Factory checked in at 2021-08-18 08:56:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-humanfriendly (Old)
 and      /work/SRC/openSUSE:Factory/.python-humanfriendly.new.1899 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-humanfriendly"

Wed Aug 18 08:56:23 2021 rev:15 rq:912717 version:9.2

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-humanfriendly/python-humanfriendly.changes    
    2020-04-23 18:37:20.900880987 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-humanfriendly.new.1899/python-humanfriendly.changes
      2021-08-18 08:57:14.074881593 +0200
@@ -1,0 +2,35 @@
+Tue Aug 17 10:58:03 UTC 2021 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to 9.2
+  Maintenance release:
+  * Merged pull request `#46`_ which fixes several :pypi:`flake8` warnings.
+  * Merged pull request `#49`_ which marks Python 3.9 support final.
+  * Merged pull request `#51`_ which helps to stabilize the test suite.
+  * Merged pull request `#52`_ which updates the :mod:`humanfriendly.sphinx`
+    module to include Sphinx extension metadata that has become mandatory in a
+    recent Sphinx release. After merging the pull request I added additional
+    metadata including the version.
+- from version 9.1
+  * Added :func:`~humanfriendly.compat.on_macos()` function to detect Apple 
MacOS
+    (I need this in an upcoming :pypi:`coloredlogs` release and don't want to 
have
+    to think about how to detect MacOS again in the future ????).
+- from version 9.0
+  The major version number was bumped because the bug fix for
+  :func:`~humanfriendly.text.pluralize()` is backwards incompatible
+  and (even though this seems like very "cosmetic" functionality)
+  version numbers are cheap, so who cares ????.
+  **Bug fixes:**
+  * Changed :func:`~humanfriendly.format_number()` to properly support negative
+    numbers (as suggested in `issue #40`_).
+  * Changed :func:`~humanfriendly.text.pluralize()` to generate "1.5 seconds"
+    instead of "1.5 second" (as suggested in `issue #43`_).
+  **Enhancements:**
+  * Enhanced :func:`~humanfriendly.text.concatenate()` to support 
``conjunction``
+    and ``serial_comma`` keyword arguments (as suggested in `issue #30`_).
+  * Added :func:`~humanfriendly.text.pluralize_raw()` to select singular or
+    plural form without prefixing the count to the text that is returned.
+- from version 8.2
+  * Added a simple case insensitive dictionary implementation, for details 
refer to
+    the new :mod:`humanfriendly.case` module.
+
+-------------------------------------------------------------------

Old:
----
  humanfriendly-8.1.tar.gz

New:
----
  humanfriendly-9.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-humanfriendly.spec ++++++
--- /var/tmp/diff_new_pack.PfEhKm/_old  2021-08-18 08:57:14.542881042 +0200
+++ /var/tmp/diff_new_pack.PfEhKm/_new  2021-08-18 08:57:14.546881039 +0200
@@ -1,7 +1,7 @@
 #
-# spec file for package python
+# spec file
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -27,7 +27,7 @@
 %bcond_without python2
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-humanfriendly%{psuffix}
-Version:        8.1
+Version:        9.2
 Release:        0
 Summary:        Human friendly input/output for text interfaces using Python
 License:        MIT
@@ -38,7 +38,7 @@
 BuildRequires:  python-rpm-macros
 Requires:       python
 Requires(post): update-alternatives
-Requires(postun): update-alternatives
+Requires(postun):update-alternatives
 BuildArch:      noarch
 %if %{with test}
 BuildRequires:  %{python_module capturer >= 2.1}

++++++ humanfriendly-8.1.tar.gz -> humanfriendly-9.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/CHANGELOG.rst 
new/humanfriendly-9.2/CHANGELOG.rst
--- old/humanfriendly-8.1/CHANGELOG.rst 2020-03-06 22:01:01.000000000 +0100
+++ new/humanfriendly-9.2/CHANGELOG.rst 2021-06-11 12:52:37.000000000 +0200
@@ -11,6 +11,74 @@
 .. _Keep a Changelog: http://keepachangelog.com/
 .. _semantic versioning: http://semver.org/
 
+`Release 9.2`_ (2021-06-11)
+---------------------------
+
+Maintenance release:
+
+- Merged pull request `#46`_ which fixes several :pypi:`flake8` warnings.
+
+- Merged pull request `#49`_ which marks Python 3.9 support final.
+
+- Merged pull request `#51`_ which helps to stabilize the test suite.
+
+- Merged pull request `#52`_ which updates the :mod:`humanfriendly.sphinx`
+  module to include Sphinx extension metadata that has become mandatory in a
+  recent Sphinx release. After merging the pull request I added additional
+  metadata including the version.
+
+.. _Release 9.2: 
https://github.com/xolox/python-humanfriendly/compare/9.1...9.2
+.. _#46: https://github.com/xolox/python-humanfriendly/pull/46
+.. _#49: https://github.com/xolox/python-humanfriendly/pull/49
+.. _#51: https://github.com/xolox/python-humanfriendly/pull/51
+.. _#52: https://github.com/xolox/python-humanfriendly/pull/52
+
+`Release 9.1`_ (2020-12-10)
+---------------------------
+
+Added :func:`~humanfriendly.compat.on_macos()` function to detect Apple MacOS
+(I need this in an upcoming :pypi:`coloredlogs` release and don't want to have
+to think about how to detect MacOS again in the future ????).
+
+.. _Release 9.1: 
https://github.com/xolox/python-humanfriendly/compare/9.0...9.1
+
+`Release 9.0`_ (2020-12-01)
+---------------------------
+
+The major version number was bumped because the bug fix for
+:func:`~humanfriendly.text.pluralize()` is backwards incompatible
+and (even though this seems like very "cosmetic" functionality)
+version numbers are cheap, so who cares ????.
+
+**Bug fixes:**
+
+- Changed :func:`~humanfriendly.format_number()` to properly support negative
+  numbers (as suggested in `issue #40`_).
+
+- Changed :func:`~humanfriendly.text.pluralize()` to generate "1.5 seconds"
+  instead of "1.5 second" (as suggested in `issue #43`_).
+
+**Enhancements:**
+
+- Enhanced :func:`~humanfriendly.text.concatenate()` to support ``conjunction``
+  and ``serial_comma`` keyword arguments (as suggested in `issue #30`_).
+
+- Added :func:`~humanfriendly.text.pluralize_raw()` to select singular or
+  plural form without prefixing the count to the text that is returned.
+
+.. _Release 9.0: 
https://github.com/xolox/python-humanfriendly/compare/8.2...9.0
+.. _issue #30: https://github.com/xolox/python-humanfriendly/issues/30
+.. _issue #40: https://github.com/xolox/python-humanfriendly/issues/40
+.. _issue #43: https://github.com/xolox/python-humanfriendly/issues/43
+
+`Release 8.2`_ (2020-04-19)
+---------------------------
+
+Added a simple case insensitive dictionary implementation, for details refer to
+the new :mod:`humanfriendly.case` module.
+
+.. _Release 8.2: 
https://github.com/xolox/python-humanfriendly/compare/8.1...8.2
+
 `Release 8.1`_ (2020-03-06)
 ---------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/LICENSE.txt 
new/humanfriendly-9.2/LICENSE.txt
--- old/humanfriendly-8.1/LICENSE.txt   2020-02-06 01:08:58.000000000 +0100
+++ new/humanfriendly-9.2/LICENSE.txt   2021-06-11 12:43:08.000000000 +0200
@@ -1,4 +1,4 @@
-Copyright (c) 2020 Peter Odding
+Copyright (c) 2021 Peter Odding
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/PKG-INFO 
new/humanfriendly-9.2/PKG-INFO
--- old/humanfriendly-8.1/PKG-INFO      2020-03-06 22:01:19.000000000 +0100
+++ new/humanfriendly-9.2/PKG-INFO      2021-06-11 12:53:09.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: humanfriendly
-Version: 8.1
+Version: 9.2
 Summary: Human friendly output for text interfaces using Python
 Home-page: https://humanfriendly.readthedocs.io
 Author: Peter Odding
@@ -152,7 +152,7 @@
         
         This software is licensed under the `MIT license`_.
         
-        ?? 2020 Peter Odding.
+        ?? 2021 Peter Odding.
         
         .. External references:
         .. _#4: https://github.com/xolox/python-humanfriendly/issues/4
@@ -184,6 +184,7 @@
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Topic :: Communications
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/README.rst 
new/humanfriendly-9.2/README.rst
--- old/humanfriendly-8.1/README.rst    2020-02-16 20:22:55.000000000 +0100
+++ new/humanfriendly-9.2/README.rst    2021-06-11 12:43:08.000000000 +0200
@@ -144,7 +144,7 @@
 
 This software is licensed under the `MIT license`_.
 
-?? 2020 Peter Odding.
+?? 2021 Peter Odding.
 
 .. External references:
 .. _#4: https://github.com/xolox/python-humanfriendly/issues/4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/docs/api.rst 
new/humanfriendly-9.2/docs/api.rst
--- old/humanfriendly-8.1/docs/api.rst  2020-03-02 01:06:35.000000000 +0100
+++ new/humanfriendly-9.2/docs/api.rst  2020-04-19 02:22:19.000000000 +0200
@@ -34,6 +34,12 @@
 .. automodule:: humanfriendly
    :members:
 
+:mod:`humanfriendly.case`
+------------------------
+
+.. automodule:: humanfriendly.case
+   :members:
+
 :mod:`humanfriendly.cli`
 ------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/docs/conf.py 
new/humanfriendly-9.2/docs/conf.py
--- old/humanfriendly-8.1/docs/conf.py  2020-03-01 15:16:58.000000000 +0100
+++ new/humanfriendly-9.2/docs/conf.py  2021-06-11 12:43:08.000000000 +0200
@@ -32,7 +32,7 @@
 
 # General information about the project.
 project = 'humanfriendly'
-copyright = '2020, Peter Odding'
+copyright = '2021, Peter Odding'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/__init__.py 
new/humanfriendly-9.2/humanfriendly/__init__.py
--- old/humanfriendly-8.1/humanfriendly/__init__.py     2020-03-06 
22:01:01.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly/__init__.py     2021-06-11 
12:52:37.000000000 +0200
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <pe...@peterodding.com>
-# Last Change: March 6, 2020
+# Last Change: June 11, 2021
 # URL: https://humanfriendly.readthedocs.io
 
 """The main module of the `humanfriendly` package."""
@@ -51,7 +51,7 @@
 )
 
 # Semi-standard module versioning.
-__version__ = '8.1'
+__version__ = '9.2'
 
 # Named tuples to define units of size.
 SizeUnit = collections.namedtuple('SizeUnit', 'divider, symbol, name')
@@ -353,7 +353,8 @@
     6,000,000,000
     """
     integer_part, _, decimal_part = str(float(number)).partition('.')
-    reversed_digits = ''.join(reversed(integer_part))
+    negative_sign = integer_part.startswith('-')
+    reversed_digits = ''.join(reversed(integer_part.lstrip('-')))
     parts = []
     while reversed_digits:
         parts.append(reversed_digits[:3])
@@ -362,6 +363,8 @@
     decimals_to_add = decimal_part[:num_decimals].rstrip('0')
     if decimals_to_add:
         formatted_number += '.' + decimals_to_add
+    if negative_sign:
+        formatted_number = '-' + formatted_number
     return formatted_number
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/case.py 
new/humanfriendly-9.2/humanfriendly/case.py
--- old/humanfriendly-8.1/humanfriendly/case.py 1970-01-01 01:00:00.000000000 
+0100
+++ new/humanfriendly-9.2/humanfriendly/case.py 2020-04-19 02:22:19.000000000 
+0200
@@ -0,0 +1,157 @@
+# Human friendly input/output in Python.
+#
+# Author: Peter Odding <pe...@peterodding.com>
+# Last Change: April 19, 2020
+# URL: https://humanfriendly.readthedocs.io
+
+"""
+Simple case insensitive dictionaries.
+
+The :class:`CaseInsensitiveDict` class is a dictionary whose string keys
+are case insensitive. It works by automatically coercing string keys to
+:class:`CaseInsensitiveKey` objects. Keys that are not strings are
+supported as well, just without case insensitivity.
+
+At its core this module works by normalizing strings to lowercase before
+comparing or hashing them. It doesn't support proper case folding nor
+does it support Unicode normalization, hence the word "simple".
+"""
+
+# Standard library modules.
+import collections
+
+try:
+    # Python >= 3.3.
+    from collections.abc import Iterable, Mapping
+except ImportError:
+    # Python 2.7.
+    from collections import Iterable, Mapping
+
+# Modules included in our package.
+from humanfriendly.compat import basestring, unicode
+
+# Public identifiers that require documentation.
+__all__ = ("CaseInsensitiveDict", "CaseInsensitiveKey")
+
+
+class CaseInsensitiveDict(collections.OrderedDict):
+
+    """
+    Simple case insensitive dictionary implementation (that remembers 
insertion order).
+
+    This class works by overriding methods that deal with dictionary keys to
+    coerce string keys to :class:`CaseInsensitiveKey` objects before calling
+    down to the regular dictionary handling methods. While intended to be
+    complete this class has not been extensively tested yet.
+    """
+
+    def __init__(self, other=None, **kw):
+        """Initialize a :class:`CaseInsensitiveDict` object."""
+        # Initialize our superclass.
+        super(CaseInsensitiveDict, self).__init__()
+        # Handle the initializer arguments.
+        self.update(other, **kw)
+
+    def coerce_key(self, key):
+        """
+        Coerce string keys to :class:`CaseInsensitiveKey` objects.
+
+        :param key: The value to coerce (any type).
+        :returns: If `key` is a string then a :class:`CaseInsensitiveKey`
+                  object is returned, otherwise the value of `key` is
+                  returned unmodified.
+        """
+        if isinstance(key, basestring):
+            key = CaseInsensitiveKey(key)
+        return key
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        """Create a case insensitive dictionary with keys from `iterable` and 
values set to `value`."""
+        return cls((k, value) for k in iterable)
+
+    def get(self, key, default=None):
+        """Get the value of an existing item."""
+        return super(CaseInsensitiveDict, self).get(self.coerce_key(key), 
default)
+
+    def pop(self, key, default=None):
+        """Remove an item from a case insensitive dictionary."""
+        return super(CaseInsensitiveDict, self).pop(self.coerce_key(key), 
default)
+
+    def setdefault(self, key, default=None):
+        """Get the value of an existing item or add a new item."""
+        return super(CaseInsensitiveDict, 
self).setdefault(self.coerce_key(key), default)
+
+    def update(self, other=None, **kw):
+        """Update a case insensitive dictionary with new items."""
+        if isinstance(other, Mapping):
+            # Copy the items from the given mapping.
+            for key, value in other.items():
+                self[key] = value
+        elif isinstance(other, Iterable):
+            # Copy the items from the given iterable.
+            for key, value in other:
+                self[key] = value
+        elif other is not None:
+            # Complain about unsupported values.
+            msg = "'%s' object is not iterable"
+            type_name = type(value).__name__
+            raise TypeError(msg % type_name)
+        # Copy the keyword arguments (if any).
+        for key, value in kw.items():
+            self[key] = value
+
+    def __contains__(self, key):
+        """Check if a case insensitive dictionary contains the given key."""
+        return super(CaseInsensitiveDict, 
self).__contains__(self.coerce_key(key))
+
+    def __delitem__(self, key):
+        """Delete an item in a case insensitive dictionary."""
+        return super(CaseInsensitiveDict, 
self).__delitem__(self.coerce_key(key))
+
+    def __getitem__(self, key):
+        """Get the value of an item in a case insensitive dictionary."""
+        return super(CaseInsensitiveDict, 
self).__getitem__(self.coerce_key(key))
+
+    def __setitem__(self, key, value):
+        """Set the value of an item in a case insensitive dictionary."""
+        return super(CaseInsensitiveDict, 
self).__setitem__(self.coerce_key(key), value)
+
+
+class CaseInsensitiveKey(unicode):
+
+    """
+    Simple case insensitive dictionary key implementation.
+
+    The :class:`CaseInsensitiveKey` class provides an intentionally simple
+    implementation of case insensitive strings to be used as dictionary keys.
+
+    If you need features like Unicode normalization or proper case folding
+    please consider using a more advanced implementation like the :pypi:`istr`
+    package instead.
+    """
+
+    def __new__(cls, value):
+        """Create a :class:`CaseInsensitiveKey` object."""
+        # Delegate string object creation to our superclass.
+        obj = unicode.__new__(cls, value)
+        # Store the lowercased string and its hash value.
+        normalized = obj.lower()
+        obj._normalized = normalized
+        obj._hash_value = hash(normalized)
+        return obj
+
+    def __hash__(self):
+        """Get the hash value of the lowercased string."""
+        return self._hash_value
+
+    def __eq__(self, other):
+        """Compare two strings as lowercase."""
+        if isinstance(other, CaseInsensitiveKey):
+            # Fast path (and the most common case): Comparison with same type.
+            return self._normalized == other._normalized
+        elif isinstance(other, unicode):
+            # Slow path: Comparison with strings that need lowercasing.
+            return self._normalized == other.lower()
+        else:
+            return NotImplemented
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/compat.py 
new/humanfriendly-9.2/humanfriendly/compat.py
--- old/humanfriendly-8.1/humanfriendly/compat.py       2020-03-01 
19:23:29.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly/compat.py       2020-12-10 
01:12:05.000000000 +0100
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <pe...@peterodding.com>
-# Last Change: March 1, 2020
+# Last Change: December 10, 2020
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -52,6 +52,7 @@
     'is_unicode',
     'monotonic',
     'name2codepoint',
+    'on_macos',
     'on_windows',
     'unichr',
     'unicode',
@@ -132,6 +133,15 @@
     return isinstance(value, unicode)
 
 
+def on_macos():
+    """
+    Check if we're running on Apple MacOS.
+
+    :returns: :data:`True` if running MacOS, :data:`False` otherwise.
+    """
+    return sys.platform.startswith('darwin')
+
+
 def on_windows():
     """
     Check if we're running on the Microsoft Windows OS.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/sphinx.py 
new/humanfriendly-9.2/humanfriendly/sphinx.py
--- old/humanfriendly-8.1/humanfriendly/sphinx.py       2020-03-01 
20:01:16.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly/sphinx.py       2021-06-11 
12:43:08.000000000 +0200
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <pe...@peterodding.com>
-# Last Change: March 1, 2020
+# Last Change: June 11, 2021
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -245,12 +245,16 @@
     like that idea you may be better of calling the individual functions from
     your own ``setup()`` function.
     """
+    from humanfriendly import __version__
+
     enable_deprecation_notes(app)
     enable_man_role(app)
     enable_pypi_role(app)
     enable_special_methods(app)
     enable_usage_formatting(app)
 
+    return dict(parallel_read_safe=True, parallel_write_safe=True, 
version=__version__)
+
 
 def special_methods_callback(app, what, name, obj, skip, options):
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/tests.py 
new/humanfriendly-9.2/humanfriendly/tests.py
--- old/humanfriendly-8.1/humanfriendly/tests.py        2020-03-06 
22:01:01.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly/tests.py        2021-06-11 
12:43:08.000000000 +0200
@@ -4,7 +4,7 @@
 # Tests for the `humanfriendly' package.
 #
 # Author: Peter Odding <peter.odd...@paylogic.eu>
-# Last Change: March 6, 2020
+# Last Change: June 11, 2021
 # URL: https://humanfriendly.readthedocs.io
 
 """Test suite for the `humanfriendly` package."""
@@ -44,6 +44,7 @@
     prompts,
     round_number,
 )
+from humanfriendly.case import CaseInsensitiveDict, CaseInsensitiveKey
 from humanfriendly.cli import main
 from humanfriendly.compat import StringIO
 from humanfriendly.decorators import cached
@@ -129,6 +130,60 @@
 
     """Container for the `humanfriendly` test suite."""
 
+    def test_case_insensitive_dict(self):
+        """Test the CaseInsensitiveDict class."""
+        # Test support for the dict(iterable) signature.
+        assert len(CaseInsensitiveDict([('key', True), ('KEY', False)])) == 1
+        # Test support for the dict(iterable, **kw) signature.
+        assert len(CaseInsensitiveDict([('one', True), ('ONE', False)], 
one=False, two=True)) == 2
+        # Test support for the dict(mapping) signature.
+        assert len(CaseInsensitiveDict(dict(key=True, KEY=False))) == 1
+        # Test support for the dict(mapping, **kw) signature.
+        assert len(CaseInsensitiveDict(dict(one=True, ONE=False), one=False, 
two=True)) == 2
+        # Test support for the dict(**kw) signature.
+        assert len(CaseInsensitiveDict(one=True, ONE=False, two=True)) == 2
+        # Test support for dict.fromkeys().
+        obj = CaseInsensitiveDict.fromkeys(["One", "one", "ONE", "Two", "two", 
"TWO"])
+        assert len(obj) == 2
+        # Test support for dict.get().
+        obj = CaseInsensitiveDict(existing_key=42)
+        assert obj.get('Existing_Key') == 42
+        # Test support for dict.pop().
+        obj = CaseInsensitiveDict(existing_key=42)
+        assert obj.pop('Existing_Key') == 42
+        assert len(obj) == 0
+        # Test support for dict.setdefault().
+        obj = CaseInsensitiveDict(existing_key=42)
+        assert obj.setdefault('Existing_Key') == 42
+        obj.setdefault('other_key', 11)
+        assert obj['Other_Key'] == 11
+        # Test support for dict.__contains__().
+        obj = CaseInsensitiveDict(existing_key=42)
+        assert 'Existing_Key' in obj
+        # Test support for dict.__delitem__().
+        obj = CaseInsensitiveDict(existing_key=42)
+        del obj['Existing_Key']
+        assert len(obj) == 0
+        # Test support for dict.__getitem__().
+        obj = CaseInsensitiveDict(existing_key=42)
+        assert obj['Existing_Key'] == 42
+        # Test support for dict.__setitem__().
+        obj = CaseInsensitiveDict(existing_key=42)
+        obj['Existing_Key'] = 11
+        assert obj['existing_key'] == 11
+
+    def test_case_insensitive_key(self):
+        """Test the CaseInsensitiveKey class."""
+        # Test the __eq__() special method.
+        polite = CaseInsensitiveKey("Please don't shout")
+        rude = CaseInsensitiveKey("PLEASE DON'T SHOUT")
+        assert polite == rude
+        # Test the __hash__() special method.
+        mapping = {}
+        mapping[polite] = 1
+        mapping[rude] = 2
+        assert len(mapping) == 1
+
     def test_capture_output(self):
         """Test the CaptureOutput class."""
         with CaptureOutput() as capturer:
@@ -386,7 +441,7 @@
         # Make sure milliseconds are never shown separately when 
detailed=False.
         # https://github.com/xolox/python-humanfriendly/issues/10
         assert '1 minute, 1 second and 100 milliseconds' == 
format_timespan(61.10, detailed=True)
-        assert '1 minute and 1.1 second' == format_timespan(61.10, 
detailed=False)
+        assert '1 minute and 1.1 seconds' == format_timespan(61.10, 
detailed=False)
         # Test for loss of precision as reported in issue 11:
         # https://github.com/xolox/python-humanfriendly/issues/11
         assert '1 minute and 0.3 seconds' == format_timespan(60.300)
@@ -520,6 +575,8 @@
         self.assertEqual('1,000', format_number(1000.12, 0))
         self.assertEqual('1,000,000', format_number(1000000))
         self.assertEqual('1,000,000.42', format_number(1000000.42))
+        # Regression test for 
https://github.com/xolox/python-humanfriendly/issues/40.
+        self.assertEqual('-285.67', format_number(-285.67))
 
     def test_round_number(self):
         """Test :func:`humanfriendly.round_number()`."""
@@ -673,6 +730,10 @@
         assert concatenate(['one']) == 'one'
         assert concatenate(['one', 'two']) == 'one and two'
         assert concatenate(['one', 'two', 'three']) == 'one, two and three'
+        # Test the 'conjunction' option.
+        assert concatenate(['one', 'two', 'three'], conjunction='or') == 'one, 
two or three'
+        # Test the 'serial_comma' option.
+        assert concatenate(['one', 'two', 'three'], serial_comma=True) == 
'one, two, and three'
 
     def test_split(self):
         """Test :func:`humanfriendly.text.split()`."""
@@ -732,8 +793,8 @@
                         .replace(ANSI_HIDE_CURSOR, ''))
         lines = [line for line in output.split(ANSI_ERASE_LINE) if line]
         self.assertTrue(len(lines) > 0)
-        self.assertTrue(all('test spinner' in l for l in lines))
-        self.assertTrue(all('%' in l for l in lines))
+        self.assertTrue(all('test spinner' in line for line in lines))
+        self.assertTrue(all('%' in line for line in lines))
         self.assertEqual(sorted(set(lines)), sorted(lines))
 
     def test_automatic_spinner(self):
@@ -897,7 +958,7 @@
         # https://github.com/xolox/python-humanfriendly/issues/28
         returncode, output = run_cli(main, '--demo')
         assert returncode == 0
-        lines = [ansi_strip(l) for l in output.splitlines()]
+        lines = [ansi_strip(line) for line in output.splitlines()]
         assert "Text styles:" in lines
         assert "Foreground colors:" in lines
         assert "Background colors:" in lines
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/text.py 
new/humanfriendly-9.2/humanfriendly/text.py
--- old/humanfriendly-8.1/humanfriendly/text.py 2020-03-01 19:23:29.000000000 
+0100
+++ new/humanfriendly-9.2/humanfriendly/text.py 2020-12-01 01:05:44.000000000 
+0100
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <pe...@peterodding.com>
-# Last Change: March 1, 2020
+# Last Change: December 1, 2020
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -20,7 +20,6 @@
 """
 
 # Standard library modules.
-import math
 import numbers
 import random
 import re
@@ -38,6 +37,7 @@
     'is_empty_line',
     'join_lines',
     'pluralize',
+    'pluralize_raw',
     'random_string',
     'split',
     'split_paragraphs',
@@ -94,20 +94,40 @@
     return ''.join(lines)
 
 
-def concatenate(items):
+def concatenate(items, conjunction='and', serial_comma=False):
     """
     Concatenate a list of items in a human friendly way.
 
-    :param items: A sequence of strings.
-    :returns: A single string.
+    :param items:
+
+        A sequence of strings.
+
+    :param conjunction:
+
+        The word to use before the last item (a string, defaults to "and").
+
+    :param serial_comma:
+
+        :data:`True` to use a `serial comma`_, :data:`False` otherwise
+        (defaults to :data:`False`).
+
+    :returns:
+
+        A single string.
 
     >>> from humanfriendly.text import concatenate
     >>> concatenate(["eggs", "milk", "bread"])
     'eggs, milk and bread'
+
+    .. _serial comma: https://en.wikipedia.org/wiki/Serial_comma
     """
     items = list(items)
     if len(items) > 1:
-        return ', '.join(items[:-1]) + ' and ' + items[-1]
+        final_item = items.pop()
+        formatted = ', '.join(items)
+        if serial_comma:
+            formatted += ','
+        return ' '.join([formatted, conjunction, final_item])
     elif items:
         return items[0]
     else:
@@ -276,19 +296,36 @@
     """
     Combine a count with the singular or plural form of a word.
 
-    If the plural form of the word is not provided it is obtained by
-    concatenating the singular form of the word with the letter "s". Of course
-    this will not always be correct, which is why you have the option to
-    specify both forms.
+    :param count: The count (a number).
+    :param singular: The singular form of the word (a string).
+    :param plural: The plural form of the word (a string or :data:`None`).
+    :returns: The count and singular or plural word concatenated (a string).
+
+    See :func:`pluralize_raw()` for the logic underneath :func:`pluralize()`.
+    """
+    return '%s %s' % (count, pluralize_raw(count, singular, plural))
+
+
+def pluralize_raw(count, singular, plural=None):
+    """
+    Select the singular or plural form of a word based on a count.
 
     :param count: The count (a number).
     :param singular: The singular form of the word (a string).
     :param plural: The plural form of the word (a string or :data:`None`).
-    :returns: The count and singular/plural word concatenated (a string).
+    :returns: The singular or plural form of the word (a string).
+
+    When the given count is exactly 1.0 the singular form of the word is
+    selected, in all other cases the plural form of the word is selected.
+
+    If the plural form of the word is not provided it is obtained by
+    concatenating the singular form of the word with the letter "s". Of course
+    this will not always be correct, which is why you have the option to
+    specify both forms.
     """
     if not plural:
         plural = singular + 's'
-    return '%s %s' % (count, singular if math.floor(float(count)) == 1 else 
plural)
+    return singular if float(count) == 1.0 else plural
 
 
 def random_string(length=(25, 100), characters=string.ascii_letters):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/usage.py 
new/humanfriendly-9.2/humanfriendly/usage.py
--- old/humanfriendly-8.1/humanfriendly/usage.py        2020-02-16 
01:03:47.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly/usage.py        2021-06-11 
12:43:08.000000000 +0200
@@ -1,7 +1,7 @@
 # Human friendly input/output in Python.
 #
 # Author: Peter Odding <pe...@peterodding.com>
-# Last Change: June 24, 2017
+# Last Change: June 11, 2021
 # URL: https://humanfriendly.readthedocs.io
 
 """
@@ -258,7 +258,7 @@
                 ('\n\n'.join(render_paragraph(p, meta_variables) for p in 
split_paragraphs(description))).rstrip(),
             ])
         csv_lines = csv_buffer.getvalue().splitlines()
-        output.append('\n'.join('   %s' % l for l in csv_lines))
+        output.append('\n'.join('   %s' % line for line in csv_lines))
     logger.debug("Rendered output: %s", output)
     return '\n\n'.join(trim_empty_lines(o) for o in output)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly.egg-info/PKG-INFO 
new/humanfriendly-9.2/humanfriendly.egg-info/PKG-INFO
--- old/humanfriendly-8.1/humanfriendly.egg-info/PKG-INFO       2020-03-06 
22:01:19.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly.egg-info/PKG-INFO       2021-06-11 
12:53:09.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: humanfriendly
-Version: 8.1
+Version: 9.2
 Summary: Human friendly output for text interfaces using Python
 Home-page: https://humanfriendly.readthedocs.io
 Author: Peter Odding
@@ -152,7 +152,7 @@
         
         This software is licensed under the `MIT license`_.
         
-        ?? 2020 Peter Odding.
+        ?? 2021 Peter Odding.
         
         .. External references:
         .. _#4: https://github.com/xolox/python-humanfriendly/issues/4
@@ -184,6 +184,7 @@
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: Implementation :: CPython
 Classifier: Programming Language :: Python :: Implementation :: PyPy
 Classifier: Topic :: Communications
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly.egg-info/SOURCES.txt 
new/humanfriendly-9.2/humanfriendly.egg-info/SOURCES.txt
--- old/humanfriendly-8.1/humanfriendly.egg-info/SOURCES.txt    2020-03-06 
22:01:19.000000000 +0100
+++ new/humanfriendly-9.2/humanfriendly.egg-info/SOURCES.txt    2021-06-11 
12:53:09.000000000 +0200
@@ -19,6 +19,7 @@
 docs/images/spinner-with-progress.gif
 docs/images/spinner-with-timer.gif
 humanfriendly/__init__.py
+humanfriendly/case.py
 humanfriendly/cli.py
 humanfriendly/compat.py
 humanfriendly/decorators.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/requirements-tests.txt 
new/humanfriendly-9.2/requirements-tests.txt
--- old/humanfriendly-8.1/requirements-tests.txt        2020-03-01 
19:23:29.000000000 +0100
+++ new/humanfriendly-9.2/requirements-tests.txt        2021-06-11 
12:26:04.000000000 +0200
@@ -1,6 +1,6 @@
 # Test suite requirements.
 capturer >= 2.1
-coloredlogs >= 2.0
+coloredlogs >= 15.0.1
 docutils >= 0.15
 mock >= 3.0.5
 pytest >= 3.0.7
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/requirements-travis.txt 
new/humanfriendly-9.2/requirements-travis.txt
--- old/humanfriendly-8.1/requirements-travis.txt       2018-10-20 
07:21:01.000000000 +0200
+++ new/humanfriendly-9.2/requirements-travis.txt       2021-06-11 
12:26:04.000000000 +0200
@@ -1,3 +1,5 @@
 --requirement=requirements-checks.txt
 --requirement=requirements-tests.txt
 coveralls
+# Dependency of coveralls:
+cryptography < 3; python_version == '2.7' and platform_python_implementation 
== "PyPy"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/humanfriendly-8.1/setup.py 
new/humanfriendly-9.2/setup.py
--- old/humanfriendly-8.1/setup.py      2020-02-16 02:15:19.000000000 +0100
+++ new/humanfriendly-9.2/setup.py      2021-06-11 12:43:08.000000000 +0200
@@ -125,6 +125,7 @@
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
+        'Programming Language :: Python :: 3.9',
         'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Topic :: Communications',

Reply via email to