From 1110808a9d39d4b808aef724c861a2e1a38d2a69 Mon Sep 17 00:00:00 2001
From: Devtools Arcadia <arcadia-devtools@yandex-team.ru>
Date: Mon, 7 Feb 2022 18:08:42 +0300
Subject: intermediate changes ref:cde9a383711a11544ce7e107a78147fb96cc4029

---
 contrib/python/PyHamcrest/src/hamcrest/__init__.py |   8 ++
 .../PyHamcrest/src/hamcrest/core/__init__.py       |   7 +
 .../PyHamcrest/src/hamcrest/core/assert_that.py    |  64 +++++++++
 .../src/hamcrest/core/base_description.py          |  92 ++++++++++++
 .../PyHamcrest/src/hamcrest/core/base_matcher.py   |  34 +++++
 .../python/PyHamcrest/src/hamcrest/core/compat.py  |  19 +++
 .../PyHamcrest/src/hamcrest/core/core/__init__.py  |  18 +++
 .../PyHamcrest/src/hamcrest/core/core/allof.py     |  44 ++++++
 .../PyHamcrest/src/hamcrest/core/core/anyof.py     |  37 +++++
 .../src/hamcrest/core/core/described_as.py         |  48 +++++++
 .../PyHamcrest/src/hamcrest/core/core/is_.py       |  76 ++++++++++
 .../src/hamcrest/core/core/isanything.py           |  31 +++++
 .../PyHamcrest/src/hamcrest/core/core/isequal.py   |  32 +++++
 .../src/hamcrest/core/core/isinstanceof.py         |  43 ++++++
 .../PyHamcrest/src/hamcrest/core/core/isnone.py    |  26 ++++
 .../PyHamcrest/src/hamcrest/core/core/isnot.py     |  57 ++++++++
 .../PyHamcrest/src/hamcrest/core/core/issame.py    |  39 ++++++
 .../PyHamcrest/src/hamcrest/core/core/raises.py    | 121 ++++++++++++++++
 .../PyHamcrest/src/hamcrest/core/description.py    |  58 ++++++++
 .../src/hamcrest/core/helpers/__init__.py          |   5 +
 .../src/hamcrest/core/helpers/hasmethod.py         |  12 ++
 .../src/hamcrest/core/helpers/wrap_matcher.py      |  36 +++++
 .../python/PyHamcrest/src/hamcrest/core/matcher.py |  52 +++++++
 .../PyHamcrest/src/hamcrest/core/selfdescribing.py |  18 +++
 .../src/hamcrest/core/selfdescribingvalue.py       |  27 ++++
 .../src/hamcrest/core/string_description.py        |  37 +++++
 .../PyHamcrest/src/hamcrest/library/__init__.py    |  44 ++++++
 .../src/hamcrest/library/collection/__init__.py    |  16 +++
 .../src/hamcrest/library/collection/is_empty.py    |  35 +++++
 .../library/collection/isdict_containing.py        |  54 ++++++++
 .../library/collection/isdict_containingentries.py | 133 ++++++++++++++++++
 .../library/collection/isdict_containingkey.py     |  48 +++++++
 .../library/collection/isdict_containingvalue.py   |  48 +++++++
 .../src/hamcrest/library/collection/isin.py        |  30 ++++
 .../library/collection/issequence_containing.py    |  89 ++++++++++++
 .../collection/issequence_containinginanyorder.py  |  97 +++++++++++++
 .../collection/issequence_containinginorder.py     |  88 ++++++++++++
 .../collection/issequence_onlycontaining.py        |  55 ++++++++
 .../src/hamcrest/library/integration/__init__.py   |   8 ++
 .../hamcrest/library/integration/match_equality.py |  42 ++++++
 .../src/hamcrest/library/number/__init__.py        |   9 ++
 .../src/hamcrest/library/number/iscloseto.py       |  74 ++++++++++
 .../hamcrest/library/number/ordering_comparison.py |  59 ++++++++
 .../src/hamcrest/library/object/__init__.py        |  10 ++
 .../src/hamcrest/library/object/haslength.py       |  50 +++++++
 .../src/hamcrest/library/object/hasproperty.py     | 154 +++++++++++++++++++++
 .../src/hamcrest/library/object/hasstring.py       |  40 ++++++
 .../src/hamcrest/library/text/__init__.py          |  14 ++
 .../hamcrest/library/text/isequal_ignoring_case.py |  43 ++++++
 .../library/text/isequal_ignoring_whitespace.py    |  57 ++++++++
 .../src/hamcrest/library/text/stringcontains.py    |  38 +++++
 .../hamcrest/library/text/stringcontainsinorder.py |  52 +++++++
 .../src/hamcrest/library/text/stringendswith.py    |  39 ++++++
 .../src/hamcrest/library/text/stringmatches.py     |  40 ++++++
 .../src/hamcrest/library/text/stringstartswith.py  |  39 ++++++
 .../src/hamcrest/library/text/substringmatcher.py  |  20 +++
 56 files changed, 2566 insertions(+)
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/assert_that.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/base_description.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/base_matcher.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/compat.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/allof.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/anyof.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/described_as.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/is_.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/isanything.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/isequal.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/isinstanceof.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/isnone.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/isnot.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/issame.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/core/raises.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/description.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/helpers/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/helpers/hasmethod.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/helpers/wrap_matcher.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/matcher.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/selfdescribing.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/selfdescribingvalue.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/core/string_description.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/is_empty.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containing.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingentries.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingkey.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingvalue.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/isin.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containing.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginanyorder.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginorder.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_onlycontaining.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/integration/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/integration/match_equality.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/number/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/number/iscloseto.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/number/ordering_comparison.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/object/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/object/haslength.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/object/hasproperty.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/object/hasstring.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/__init__.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_case.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_whitespace.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontains.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontainsinorder.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/stringendswith.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/stringmatches.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/stringstartswith.py
 create mode 100644 contrib/python/PyHamcrest/src/hamcrest/library/text/substringmatcher.py

(limited to 'contrib/python/PyHamcrest/src/hamcrest')

diff --git a/contrib/python/PyHamcrest/src/hamcrest/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/__init__.py
new file mode 100644
index 0000000000..3a751d44f6
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/__init__.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import
+from hamcrest.core import *
+from hamcrest.library import *
+
+__version__ = "1.9.0"
+__author__ = "Chris Rose"
+__copyright__ = "Copyright 2015 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/core/__init__.py
new file mode 100644
index 0000000000..779fa72345
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/__init__.py
@@ -0,0 +1,7 @@
+from __future__ import absolute_import
+from hamcrest.core.assert_that import assert_that
+from hamcrest.core.core import *
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/assert_that.py b/contrib/python/PyHamcrest/src/hamcrest/core/assert_that.py
new file mode 100644
index 0000000000..b38bc243ca
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/assert_that.py
@@ -0,0 +1,64 @@
+from __future__ import absolute_import
+from hamcrest.core.matcher import Matcher
+from hamcrest.core.string_description import StringDescription
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+# unittest integration; hide these frames from tracebacks
+__unittest = True
+# py.test integration; hide these frames from tracebacks
+__tracebackhide__ = True
+
+def assert_that(arg1, arg2=None, arg3=''):
+    """Asserts that actual value satisfies matcher. (Can also assert plain
+    boolean condition.)
+
+    :param actual: The object to evaluate as the actual value.
+    :param matcher: The matcher to satisfy as the expected condition.
+    :param reason: Optional explanation to include in failure description.
+
+    ``assert_that`` passes the actual value to the matcher for evaluation. If
+    the matcher is not satisfied, an exception is thrown describing the
+    mismatch.
+
+    ``assert_that`` is designed to integrate well with PyUnit and other unit
+    testing frameworks. The exception raised for an unmet assertion is an
+    :py:exc:`AssertionError`, which PyUnit reports as a test failure.
+
+    With a different set of parameters, ``assert_that`` can also verify a
+    boolean condition:
+
+    .. function:: assert_that(assertion[, reason])
+
+    :param assertion:  Boolean condition to verify.
+    :param reason:  Optional explanation to include in failure description.
+
+    This is equivalent to the :py:meth:`~unittest.TestCase.assertTrue` method
+    of :py:class:`unittest.TestCase`, but offers greater flexibility in test
+    writing by being a standalone function.
+
+    """
+    if isinstance(arg2, Matcher):
+        _assert_match(actual=arg1, matcher=arg2, reason=arg3)
+    else:
+        _assert_bool(assertion=arg1, reason=arg2)
+
+
+def _assert_match(actual, matcher, reason):
+    if not matcher.matches(actual):
+        description = StringDescription()
+        description.append_text(reason)             \
+                   .append_text('\nExpected: ')     \
+                   .append_description_of(matcher)  \
+                   .append_text('\n     but: ')
+        matcher.describe_mismatch(actual, description)
+        description.append_text('\n')
+        raise AssertionError(description)
+
+
+def _assert_bool(assertion, reason=None):
+    if not assertion:
+        if not reason:
+            reason = 'Assertion failed'
+        raise AssertionError(reason)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/base_description.py b/contrib/python/PyHamcrest/src/hamcrest/core/base_description.py
new file mode 100644
index 0000000000..8c7c51364d
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/base_description.py
@@ -0,0 +1,92 @@
+from __future__ import absolute_import
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+import warnings
+import six
+
+from hamcrest.core.description import Description
+from hamcrest.core.selfdescribingvalue import SelfDescribingValue
+from hamcrest.core.helpers.hasmethod import hasmethod
+
+class BaseDescription(Description):
+    """Base class for all :py:class:`~hamcrest.core.description.Description`
+    implementations.
+
+    """
+
+    def append_text(self, text):
+        self.append(text)
+        return self
+
+    def append_description_of(self, value):
+        if hasmethod(value, 'describe_to'):
+            value.describe_to(self)
+        elif six.PY3 and isinstance(value, six.text_type):
+            self.append(repr(value))
+        elif six.PY2 and isinstance(value, six.binary_type):
+            self.append_string_in_python_syntax(value)
+        elif isinstance(value, six.text_type):
+            self.append_string_in_python_syntax(value)
+        else:
+            description = str(value)
+            if description[:1] == '<' and description[-1:] == '>':
+                self.append(description)
+            else:
+                self.append('<')
+                self.append(description)
+                self.append('>')
+        return self
+
+    def append_value(self, value):
+        warnings.warn('Call append_description_of instead of append_value',
+                      DeprecationWarning)
+        if isinstance(value, str):
+            self.append_string_in_python_syntax(value)
+        else:
+            self.append('<')
+            self.append(str(value))
+            self.append('>')
+        return self
+
+    def append_value_list(self, start, separator, end, list):
+        warnings.warn('Call append_list instead of append_value_list',
+                      DeprecationWarning)
+        return self.append_list(start, separator, end,
+                                map(SelfDescribingValue, list))
+
+    def append_list(self, start, separator, end, list):
+        separate = False
+
+        self.append(start)
+        for item in list:
+            if separate:
+                self.append(separator)
+            self.append_description_of(item)
+            separate = True
+        self.append(end)
+        return self
+
+    def append(self, string):
+        """Append the string to the description."""
+        raise NotImplementedError('append')
+
+    def append_string_in_python_syntax(self, string):
+        self.append("'")
+        for ch in string:
+            self.append(character_in_python_syntax(ch))
+        self.append("'")
+
+
+def character_in_python_syntax(ch):
+    if ch == "'":
+        return "\'"
+    elif ch == '\n':
+        return '\\n'
+    elif ch == '\r':
+        return '\\r'
+    elif ch == '\t':
+        return '\\t'
+    else:
+        return ch
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/base_matcher.py b/contrib/python/PyHamcrest/src/hamcrest/core/base_matcher.py
new file mode 100644
index 0000000000..951e2a7b95
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/base_matcher.py
@@ -0,0 +1,34 @@
+from __future__ import absolute_import
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.matcher import Matcher
+from hamcrest.core.string_description import tostring
+
+
+class BaseMatcher(Matcher):
+    """Base class for all :py:class:`~hamcrest.core.matcher.Matcher`
+    implementations.
+
+    Most implementations can just implement :py:obj:`_matches`, leaving the
+    handling of any mismatch description to the ``matches`` method. But if it
+    makes more sense to generate the mismatch description during the matching,
+    override :py:meth:`~hamcrest.core.matcher.Matcher.matches` instead.
+
+    """
+
+    def __str__(self):
+        return tostring(self)
+
+    def _matches(self, item):
+        raise NotImplementedError('_matches')
+
+    def matches(self, item, mismatch_description=None):
+        match_result = self._matches(item)
+        if not match_result and mismatch_description:
+            self.describe_mismatch(item, mismatch_description)
+        return match_result
+
+    def describe_mismatch(self, item, mismatch_description):
+        mismatch_description.append_text('was ').append_description_of(item)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/compat.py b/contrib/python/PyHamcrest/src/hamcrest/core/compat.py
new file mode 100644
index 0000000000..2c6d1fa74c
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/compat.py
@@ -0,0 +1,19 @@
+__author__ = "Per Fagrell"
+__copyright__ = "Copyright 2013 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+__all__ = ['is_callable']
+
+import sys
+
+# callable was not part of py3k until 3.2, so we create this
+# generic is_callable to use callable if possible, otherwise
+# we use generic homebrew.
+if sys.version_info[0] == 3 and sys.version_info[1] < 2:
+    def is_callable(function):
+        """Return whether the object is callable (i.e., some kind of function)."""
+        if function is None:
+            return False
+        return any("__call__" in klass.__dict__ for klass in type(function).__mro__)
+else:
+    is_callable = callable
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/__init__.py
new file mode 100644
index 0000000000..38e93e249a
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/__init__.py
@@ -0,0 +1,18 @@
+from __future__ import absolute_import
+"""Fundamental matchers of objects and values, and composite matchers."""
+
+from hamcrest.core.core.allof import all_of
+from hamcrest.core.core.anyof import any_of
+from hamcrest.core.core.described_as import described_as
+from hamcrest.core.core.is_ import is_
+from hamcrest.core.core.isanything import anything
+from hamcrest.core.core.isequal import equal_to
+from hamcrest.core.core.isinstanceof import instance_of
+from hamcrest.core.core.isnone import none, not_none
+from hamcrest.core.core.isnot import is_not, not_
+from hamcrest.core.core.issame import same_instance
+from hamcrest.core.core.raises import calling, raises
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/allof.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/allof.py
new file mode 100644
index 0000000000..35c5d0bfeb
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/allof.py
@@ -0,0 +1,44 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class AllOf(BaseMatcher):
+
+    def __init__(self, *matchers):
+        self.matchers = matchers
+
+    def matches(self, item, mismatch_description=None):
+        for matcher in self.matchers:
+            if not matcher.matches(item):
+                if mismatch_description:
+                    mismatch_description.append_description_of(matcher) \
+                                        .append_text(' ')
+                    matcher.describe_mismatch(item, mismatch_description)
+                return False
+        return True
+
+    def describe_mismatch(self, item, mismatch_description):
+        self.matches(item, mismatch_description)
+
+    def describe_to(self, description):
+        description.append_list('(', ' and ', ')', self.matchers)
+
+
+def all_of(*items):
+    """Matches if all of the given matchers evaluate to ``True``.
+
+    :param matcher1,...:  A comma-separated list of matchers.
+
+    The matchers are evaluated from left to right using short-circuit
+    evaluation, so evaluation stops as soon as a matcher returns ``False``.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+    return AllOf(*[wrap_matcher(item) for item in items])
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/anyof.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/anyof.py
new file mode 100644
index 0000000000..7a2bfc6627
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/anyof.py
@@ -0,0 +1,37 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class AnyOf(BaseMatcher):
+
+    def __init__(self, *matchers):
+        self.matchers = matchers
+
+    def _matches(self, item):
+        for matcher in self.matchers:
+            if matcher.matches(item):
+                return True
+        return False
+
+    def describe_to(self, description):
+        description.append_list('(', ' or ', ')', self.matchers)
+
+
+def any_of(*items):
+    """Matches if any of the given matchers evaluate to ``True``.
+
+    :param matcher1,...:  A comma-separated list of matchers.
+
+    The matchers are evaluated from left to right using short-circuit
+    evaluation, so evaluation stops as soon as a matcher returns ``True``.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+    return AnyOf(*[wrap_matcher(item) for item in items])
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/described_as.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/described_as.py
new file mode 100644
index 0000000000..93b4d6ac45
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/described_as.py
@@ -0,0 +1,48 @@
+from hamcrest.core.base_matcher import BaseMatcher
+import re
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+ARG_PATTERN = re.compile('%([0-9]+)')
+
+
+class DescribedAs(BaseMatcher):
+
+    def __init__(self, description_template, matcher, *values):
+        self.template = description_template
+        self.matcher = matcher
+        self.values = values
+
+    def matches(self, item, mismatch_description=None):
+        return self.matcher.matches(item, mismatch_description)
+
+    def describe_mismatch(self, item, mismatch_description):
+        self.matcher.describe_mismatch(item, mismatch_description)
+
+    def describe_to(self, description):
+        text_start = 0
+        for match in re.finditer(ARG_PATTERN, self.template):
+            description.append_text(self.template[text_start:match.start()])
+            arg_index = int(match.group()[1:])
+            description.append_description_of(self.values[arg_index])
+            text_start = match.end()
+
+        if text_start < len(self.template):
+            description.append_text(self.template[text_start:])
+
+
+def described_as(description, matcher, *values):
+    """Adds custom failure description to a given matcher.
+
+    :param description: Overrides the matcher's description.
+    :param matcher: The matcher to satisfy.
+    :param value1,...: Optional comma-separated list of substitution values.
+
+    The description may contain substitution placeholders %0, %1, etc. These
+    will be replaced by any values that follow the matcher.
+
+    """
+    return DescribedAs(description, matcher, *values)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/is_.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/is_.py
new file mode 100644
index 0000000000..ba11a762ae
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/is_.py
@@ -0,0 +1,76 @@
+from __future__ import absolute_import
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher, is_matchable_type
+from .isinstanceof import instance_of
+
+
+class Is(BaseMatcher):
+
+    def __init__(self, matcher):
+        self.matcher = matcher
+
+    def matches(self, item, mismatch_description=None):
+        return self.matcher.matches(item, mismatch_description)
+
+    def describe_mismatch(self, item, mismatch_description):
+        return self.matcher.describe_mismatch(item, mismatch_description)
+
+    def describe_to(self, description):
+        description.append_description_of(self.matcher)
+
+
+def wrap_value_or_type(x):
+    if is_matchable_type(x):
+        return instance_of(x)
+    else:
+        return wrap_matcher(x)
+
+
+def is_(x):
+    """Decorates another matcher, or provides shortcuts to the frequently used
+    ``is(equal_to(x))`` and ``is(instance_of(x))``.
+
+    :param x: The matcher to satisfy,  or a type for
+        :py:func:`~hamcrest.core.core.isinstanceof.instance_of` matching, or an
+        expected value for :py:func:`~hamcrest.core.core.isequal.equal_to`
+        matching.
+
+    This matcher compares the evaluated object to the given matcher.
+
+    .. note::
+
+        PyHamcrest's ``is_`` matcher is unrelated to Python's ``is`` operator.
+        The matcher for object identity is
+        :py:func:`~hamcrest.core.core.issame.same_instance`.
+
+    If the ``x`` argument is a matcher, its behavior is retained, but the test
+    may be more expressive. For example::
+
+        assert_that(value, less_than(5))
+        assert_that(value, is_(less_than(5)))
+
+    If the ``x`` argument is a type, it is wrapped in an
+    :py:func:`~hamcrest.core.core.isinstanceof.instance_of` matcher. This makes
+    the following statements equivalent::
+
+        assert_that(cheese, instance_of(Cheddar))
+        assert_that(cheese, is_(instance_of(Cheddar)))
+        assert_that(cheese, is_(Cheddar))
+
+    Otherwise, if the ``x`` argument is not a matcher, it is wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher. This makes the
+    following statements equivalent::
+
+        assert_that(cheese, equal_to(smelly))
+        assert_that(cheese, is_(equal_to(smelly)))
+        assert_that(cheese, is_(smelly))
+
+    Choose the style that makes your expression most readable. This will vary
+    depending on context.
+
+    """
+    return Is(wrap_value_or_type(x))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/isanything.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/isanything.py
new file mode 100644
index 0000000000..f916811bc9
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/isanything.py
@@ -0,0 +1,31 @@
+from hamcrest.core.base_matcher import BaseMatcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsAnything(BaseMatcher):
+
+    def __init__(self, description):
+        self.description = description
+        if not description:
+            self.description = 'ANYTHING'
+
+    def _matches(self, item):
+        return True
+
+    def describe_to(self, description):
+        description.append_text(self.description)
+
+
+def anything(description=None):
+    """Matches anything.
+
+    :param description: Optional string used to describe this matcher.
+
+    This matcher always evaluates to ``True``. Specify this in composite
+    matchers when the value of a particular element is unimportant.
+
+    """
+    return IsAnything(description)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/isequal.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/isequal.py
new file mode 100644
index 0000000000..119fd58a48
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/isequal.py
@@ -0,0 +1,32 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.matcher import Matcher
+
+
+class IsEqual(BaseMatcher):
+
+    def __init__(self, equals):
+        self.object = equals
+
+    def _matches(self, item):
+        return item == self.object
+
+    def describe_to(self, description):
+        nested_matcher = isinstance(self.object, Matcher)
+        if nested_matcher:
+            description.append_text('<')
+        description.append_description_of(self.object)
+        if nested_matcher:
+            description.append_text('>')
+
+
+def equal_to(obj):
+    """Matches if object is equal to a given object.
+
+    :param obj: The object to compare against as the expected value.
+
+    This matcher compares the evaluated object to ``obj`` for equality."""
+    return IsEqual(obj)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/isinstanceof.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/isinstanceof.py
new file mode 100644
index 0000000000..f8eb4a2fd1
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/isinstanceof.py
@@ -0,0 +1,43 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.wrap_matcher import is_matchable_type
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+import types
+
+class IsInstanceOf(BaseMatcher):
+
+    def __init__(self, expected_type):
+        if not is_matchable_type(expected_type):
+            raise TypeError('IsInstanceOf requires type or a tuple of classes and types')
+        self.expected_type = expected_type
+
+    def _matches(self, item):
+        return isinstance(item, self.expected_type)
+
+    def describe_to(self, description):
+        try:
+            type_description = self.expected_type.__name__
+        except AttributeError:
+            type_description = "one of %s" % ",".join(str(e) for e in self.expected_type)
+        description.append_text('an instance of ')              \
+                    .append_text(type_description)
+
+
+def instance_of(atype):
+    """Matches if object is an instance of, or inherits from, a given type.
+
+    :param atype: The type to compare against as the expected type or a tuple
+        of types.
+
+    This matcher checks whether the evaluated object is an instance of
+    ``atype`` or an instance of any class that inherits from ``atype``.
+
+    Example::
+
+        instance_of(str)
+
+    """
+    return IsInstanceOf(atype)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/isnone.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/isnone.py
new file mode 100644
index 0000000000..511fd5ae46
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/isnone.py
@@ -0,0 +1,26 @@
+from __future__ import absolute_import
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+from .isnot import is_not
+
+
+class IsNone(BaseMatcher):
+
+    def _matches(self, item):
+        return item is None
+
+    def describe_to(self, description):
+        description.append_text('None')
+
+
+def none():
+    """Matches if object is ``None``."""
+    return IsNone()
+
+
+def not_none():
+    """Matches if object is not ``None``."""
+    return is_not(none())
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/isnot.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/isnot.py
new file mode 100644
index 0000000000..7567e6f325
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/isnot.py
@@ -0,0 +1,57 @@
+from __future__ import absolute_import
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher, Matcher
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher, is_matchable_type
+from .isequal import equal_to
+from .isinstanceof import instance_of
+
+
+class IsNot(BaseMatcher):
+
+    def __init__(self, matcher):
+        self.matcher = matcher
+
+    def _matches(self, item):
+        return not self.matcher.matches(item)
+
+    def describe_to(self, description):
+        description.append_text('not ').append_description_of(self.matcher)
+
+
+def wrap_value_or_type(x):
+    if is_matchable_type(x):
+        return instance_of(x)
+    else:
+        return wrap_matcher(x)
+
+
+def is_not(match):
+    """Inverts the given matcher to its logical negation.
+
+    :param match: The matcher to negate.
+
+    This matcher compares the evaluated object to the negation of the given
+    matcher. If the ``match`` argument is not a matcher, it is implicitly
+    wrapped in an :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to
+    check for equality, and thus matches for inequality.
+
+    Examples::
+
+        assert_that(cheese, is_not(equal_to(smelly)))
+        assert_that(cheese, is_not(smelly))
+
+    """
+    return IsNot(wrap_value_or_type(match))
+
+def not_(match):
+    """Alias of :py:func:`is_not` for better readability of negations.
+
+    Examples::
+
+        assert_that(alist, not_(has_item(item)))
+
+    """
+    return is_not(match)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/issame.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/issame.py
new file mode 100644
index 0000000000..b1f85427d7
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/issame.py
@@ -0,0 +1,39 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+
+
+class IsSame(BaseMatcher):
+
+    def __init__(self, object):
+        self.object = object
+
+    def _matches(self, item):
+        return item is self.object
+
+    def describe_to(self, description):
+        description.append_text('same instance as ')            \
+                   .append_text(hex(id(self.object)))           \
+                   .append_text(' ')                            \
+                   .append_description_of(self.object)
+
+    def describe_mismatch(self, item, mismatch_description):
+        mismatch_description.append_text('was ')
+        if item is not None:
+            mismatch_description.append_text(hex(id(item)))         \
+                                .append_text(' ')
+        mismatch_description.append_description_of(item)
+
+
+def same_instance(obj):
+    """Matches if evaluated object is the same instance as a given object.
+
+    :param obj: The object to compare against as the expected value.
+
+    This matcher invokes the ``is`` identity operator to determine if the
+    evaluated object is the the same object as ``obj``.
+
+    """
+    return IsSame(obj)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/core/raises.py b/contrib/python/PyHamcrest/src/hamcrest/core/core/raises.py
new file mode 100644
index 0000000000..878e2af87a
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/core/raises.py
@@ -0,0 +1,121 @@
+from weakref import ref
+import re
+import sys
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+from hamcrest.core.compat import is_callable
+
+__author__ = "Per Fagrell"
+__copyright__ = "Copyright 2013 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class Raises(BaseMatcher):
+    def __init__(self, expected, pattern=None, matcher=None):
+        self.pattern = pattern
+        self.expected = expected
+        self.actual = None
+        self.function = None
+        self.matcher = matcher
+        self.actual_return_value = None
+
+    def _matches(self, function):
+        if not is_callable(function):
+            return False
+
+        self.function = ref(function)
+        return self._call_function(function)
+
+    def _call_function(self, function):
+        self.actual = None
+        try:
+            self.actual_return_value = function()
+        except Exception:
+            self.actual = sys.exc_info()[1]
+
+            if isinstance(self.actual, self.expected):
+                if self.pattern is not None:
+                    return (
+                        re.search(self.pattern, str(self.actual)) is not None
+                        and (self.matcher is None or self.matcher.matches(self.actual))
+                    )
+                return self.matcher is None or self.matcher.matches(self.actual)
+        return False
+
+    def describe_to(self, description):
+        description.append_text('Expected a callable raising %s' % self.expected)
+        if self.matcher is not None:
+            description.append_text("\n and ")
+            description.append_description_of(self.matcher)
+
+    def describe_mismatch(self, item, description):
+        if not is_callable(item):
+            description.append_text('%s is not callable' % item)
+            return
+
+        function = None if self.function is None else self.function()
+        if function is None or function is not item:
+            self.function = ref(item)
+            if not self._call_function(item):
+                return
+
+        if self.actual is None:
+            description.append_text('No exception raised and actual return value = ')
+            description.append_value(self.actual_return_value)
+        elif isinstance(self.actual, self.expected):
+            if self.pattern is not None:
+                description.append_text('Correct assertion type raised, but the expected pattern ("%s") not found.' % self.pattern)
+                description.append_text('\n          message was: "%s"' % str(self.actual))
+            if self.matcher is not None:
+                description.append_text("\nAdditional exception matcher: ")
+                self.matcher.describe_mismatch(self.actual, description)
+        else:
+            description.append_text('%s was raised instead' % type(self.actual))
+
+
+def raises(exception, pattern=None, matcher=None):
+    """Matches if the called function raised the expected exception.
+
+    :param exception:  The class of the expected exception
+    :param pattern:    Optional regular expression to match exception message.
+
+    Expects the actual to be wrapped by using :py:func:`~hamcrest.core.core.raises.calling`,
+    or a callable taking no arguments.
+    Optional argument pattern should be a string containing a regular expression.  If provided,
+    the string representation of the actual exception - e.g. `str(actual)` - must match pattern.
+
+    Examples::
+
+        assert_that(calling(int).with_args('q'), raises(TypeError))
+        assert_that(calling(parse, broken_input), raises(ValueError))
+    """
+    return Raises(exception, pattern, matcher)
+
+
+class DeferredCallable(object):
+    def __init__(self, func):
+        self.func = func
+        self.args = tuple()
+        self.kwargs = {}
+
+    def __call__(self):
+        return self.func(*self.args, **self.kwargs)
+
+    def with_args(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        return self
+
+
+def calling(func):
+    """Wrapper for function call that delays the actual execution so that
+    :py:func:`~hamcrest.core.core.raises.raises` matcher can catch any thrown exception.
+
+    :param func: The function or method to be called
+
+    The arguments can be provided with a call to the `with_args` function on the returned
+    object::
+
+           calling(my_method).with_args(arguments, and_='keywords')
+    """
+    return DeferredCallable(func)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/description.py b/contrib/python/PyHamcrest/src/hamcrest/core/description.py
new file mode 100644
index 0000000000..6201b7f70f
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/description.py
@@ -0,0 +1,58 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class Description(object):
+    """A description of a :py:class:`~hamcrest.core.matcher.Matcher`.
+
+    A :py:class:`~hamcrest.core.matcher.Matcher` will describe itself to a
+    description which can later be used for reporting.
+
+    """
+
+    def append_text(self, text):
+        """Appends some plain text to the description.
+
+        :returns: ``self``, for chaining
+
+        """
+        raise NotImplementedError('append_text')
+
+    def append_description_of(self, value):
+        """Appends description of given value to this description.
+
+        If the value implements
+        :py:meth:`~hamcrest.core.selfdescribing.SelfDescribing.describe_to`,
+        then it will be used.
+
+        :returns: ``self``, for chaining
+
+        """
+        raise NotImplementedError('append_description_of')
+
+    def append_value(self, value):
+        """Appends an arbitary value to the description.
+
+        **Deprecated:** Call
+        :py:meth:`~hamcrest.core.description.Description.append_description_of`
+        instead.
+
+        :returns: ``self``, for chaining
+
+        """
+        raise NotImplementedError('append_value')
+
+    def append_list(self, start, separator, end, list):
+        """Appends a list of objects to the description.
+
+        :param start: String that will begin the list description.
+        :param separator: String that will separate each object in the
+            description.
+        :param end: String that will end the list description.
+        :param list: List of objects to be described.
+
+        :returns: ``self``, for chaining
+
+        """
+        raise NotImplementedError('append_list')
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/helpers/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/core/helpers/__init__.py
new file mode 100644
index 0000000000..61cb82d43b
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/helpers/__init__.py
@@ -0,0 +1,5 @@
+"""Utilities for writing Matchers."""
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/helpers/hasmethod.py b/contrib/python/PyHamcrest/src/hamcrest/core/helpers/hasmethod.py
new file mode 100644
index 0000000000..a1f3bfb154
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/helpers/hasmethod.py
@@ -0,0 +1,12 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+def hasmethod(obj, methodname):
+    """Does ``obj`` have a method named ``methodname``?"""
+
+    if not hasattr(obj, methodname):
+        return False
+    method = getattr(obj, methodname)
+    return callable(method)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/helpers/wrap_matcher.py b/contrib/python/PyHamcrest/src/hamcrest/core/helpers/wrap_matcher.py
new file mode 100644
index 0000000000..a5b506fb39
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/helpers/wrap_matcher.py
@@ -0,0 +1,36 @@
+import six
+
+from hamcrest.core.base_matcher import Matcher
+from hamcrest.core.core.isequal import equal_to
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+import types
+
+def wrap_matcher(x):
+    """Wraps argument in a matcher, if necessary.
+
+    :returns: the argument as-is if it is already a matcher, otherwise wrapped
+        in an :py:func:`~hamcrest.core.core.isequal.equal_to` matcher.
+
+    """
+    if isinstance(x, Matcher):
+        return x
+    else:
+        return equal_to(x)
+
+def is_matchable_type(expected_type):
+    if isinstance(expected_type, type):
+        return True
+
+    if isinstance(expected_type, six.class_types):
+        return True
+
+    if isinstance(expected_type, tuple) and \
+       expected_type and \
+       all(map(is_matchable_type, expected_type)):
+        return True
+
+    return False
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/matcher.py b/contrib/python/PyHamcrest/src/hamcrest/core/matcher.py
new file mode 100644
index 0000000000..81ee27c6d9
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/matcher.py
@@ -0,0 +1,52 @@
+from __future__ import absolute_import
+from .selfdescribing import SelfDescribing
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class Matcher(SelfDescribing):
+    """A matcher over acceptable values.
+
+    A matcher is able to describe itself to give feedback when it fails.
+
+    Matcher implementations should *not* directly implement this protocol.
+    Instead, *extend* the :py:class:`~hamcrest.core.base_matcher.BaseMatcher`
+    class, which will ensure that the
+    :py:class:`~hamcrest.core.matcher.Matcher` API can grow to support new
+    features and remain compatible with all
+    :py:class:`~hamcrest.core.matcher.Matcher` implementations.
+
+    """
+
+    def matches(self, item, mismatch_description=None):
+        """Evaluates the matcher for argument item.
+
+        If a mismatch is detected and argument ``mismatch_description`` is
+        provided, it will generate a description of why the matcher has not
+        accepted the item.
+
+        :param item: The object against which the matcher is evaluated.
+        :returns: ``True`` if ``item`` matches, otherwise ``False``.
+
+        """
+        raise NotImplementedError('matches')
+
+    def describe_mismatch(self, item, mismatch_description):
+        """Generates a description of why the matcher has not accepted the
+        item.
+
+        The description will be part of a larger description of why a matching
+        failed, so it should be concise.
+
+        This method assumes that ``matches(item)`` is ``False``, but will not
+        check this.
+
+        :param item: The item that the
+            :py:class:`~hamcrest.core.matcher.Matcher` has rejected.
+        :param mismatch_description: The description to be built or appended
+            to.
+
+        """
+        raise NotImplementedError('describe_mismatch')
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/selfdescribing.py b/contrib/python/PyHamcrest/src/hamcrest/core/selfdescribing.py
new file mode 100644
index 0000000000..e77b0e0fdb
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/selfdescribing.py
@@ -0,0 +1,18 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class SelfDescribing(object):
+    """The ability of an object to describe itself."""
+
+    def describe_to(self, description):
+        """Generates a description of the object.
+
+        The description may be part of a description of a larger object of
+        which this is just a component, so it should be worded appropriately.
+
+        :param description: The description to be built or appended to.
+
+        """
+        raise NotImplementedError('describe_to')
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/selfdescribingvalue.py b/contrib/python/PyHamcrest/src/hamcrest/core/selfdescribingvalue.py
new file mode 100644
index 0000000000..dfa4e3a20e
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/selfdescribingvalue.py
@@ -0,0 +1,27 @@
+from hamcrest.core.selfdescribing import SelfDescribing
+
+import warnings
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class SelfDescribingValue(SelfDescribing):
+    """Wrap any value in a ``SelfDescribingValue`` to satisfy the
+    :py:class:`~hamcrest.core.selfdescribing.SelfDescribing` interface.
+
+    **Deprecated:** No need for this class now that
+    :py:meth:`~hamcrest.core.description.Description.append_description_of`
+    handles any type of value.
+
+    """
+
+    def __init__(self, value):
+        warnings.warn('SelfDescribingValue no longer needed',
+                      DeprecationWarning)
+        self.value = value
+
+    def describe_to(self, description):
+        """Generates a description of the value."""
+        description.append_value(self.value)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/core/string_description.py b/contrib/python/PyHamcrest/src/hamcrest/core/string_description.py
new file mode 100644
index 0000000000..7626bf91e8
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/core/string_description.py
@@ -0,0 +1,37 @@
+from __future__ import absolute_import
+
+import codecs
+import six
+
+from .base_description import BaseDescription
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+def tostring(selfdescribing):
+    """Returns the description of a
+    :py:class:`~hamcrest.core.selfdescribing.SelfDescribing` object as a
+    string.
+
+    :param selfdescribing: The object to be described.
+    :returns: The description of the object.
+    """
+    return str(StringDescription().append_description_of(selfdescribing))
+
+
+class StringDescription(BaseDescription):
+    """A :py:class:`~hamcrest.core.description.Description` that is stored as a
+    string.
+    """
+
+    def __init__(self):
+        self.__out_list = []
+
+    def __str__(self):
+        """Returns the description."""
+        return ''.join(self.__out_list)
+
+    def append(self, string):
+        self.__out_list.append(six.text_type(string))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/library/__init__.py
new file mode 100644
index 0000000000..a5a7963521
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/__init__.py
@@ -0,0 +1,44 @@
+from __future__ import absolute_import
+"""Library of Matcher implementations."""
+
+from hamcrest.core import *
+from hamcrest.library.collection import *
+from hamcrest.library.integration import *
+from hamcrest.library.number import *
+from hamcrest.library.object import *
+from hamcrest.library.text import *
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+__all__ = [
+    'has_entry',
+    'has_entries',
+    'has_key',
+    'has_value',
+    'is_in',
+    'empty',
+    'has_item',
+    'has_items',
+    'contains_inanyorder',
+    'contains',
+    'only_contains',
+    'match_equality',
+    'matches_regexp',
+    'close_to',
+    'greater_than',
+    'greater_than_or_equal_to',
+    'less_than',
+    'less_than_or_equal_to',
+    'has_length',
+    'has_property',
+    'has_properties',
+    'has_string',
+    'equal_to_ignoring_case',
+    'equal_to_ignoring_whitespace',
+    'contains_string',
+    'ends_with',
+    'starts_with',
+    'string_contains_in_order',
+]
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/__init__.py
new file mode 100644
index 0000000000..2f89987788
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/__init__.py
@@ -0,0 +1,16 @@
+"""Matchers of collections."""
+from __future__ import absolute_import
+from .isdict_containing import has_entry
+from .isdict_containingentries import has_entries
+from .isdict_containingkey import has_key
+from .isdict_containingvalue import has_value
+from .isin import is_in
+from .issequence_containing import has_item, has_items
+from .issequence_containinginanyorder import contains_inanyorder
+from .issequence_containinginorder import contains
+from .issequence_onlycontaining import only_contains
+from .is_empty import empty
+
+__author__ = "Chris Rose"
+__copyright__ = "Copyright 2013 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/is_empty.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/is_empty.py
new file mode 100644
index 0000000000..bc99b63373
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/is_empty.py
@@ -0,0 +1,35 @@
+from hamcrest.core.base_matcher import BaseMatcher
+
+__author__ = "Chris Rose"
+__copyright__ = "Copyright 2012 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsEmpty(BaseMatcher):
+
+    def matches(self, item, mismatch_description=None):
+        try:
+            if len(item) == 0:
+                return True
+
+            if mismatch_description:
+                mismatch_description \
+                    .append_text('has %d item(s)' % len(item))
+
+        except TypeError:
+            if mismatch_description:
+                mismatch_description \
+                    .append_text('does not support length')
+
+            return False
+
+    def describe_to(self, description):
+        description.append_text('an empty collection')
+
+
+def empty():
+    """
+    This matcher matches any collection-like object that responds to the
+    __len__ method, and has a length of 0.
+    """
+    return IsEmpty()
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containing.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containing.py
new file mode 100644
index 0000000000..95281973fb
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containing.py
@@ -0,0 +1,54 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsDictContaining(BaseMatcher):
+
+    def __init__(self, key_matcher, value_matcher):
+        self.key_matcher = key_matcher
+        self.value_matcher = value_matcher
+
+    def _matches(self, dictionary):
+        if hasmethod(dictionary, 'items'):
+            for key, value in dictionary.items():
+                if self.key_matcher.matches(key) and self.value_matcher.matches(value):
+                    return True
+        return False
+
+    def describe_to(self, description):
+        description.append_text('a dictionary containing [')        \
+                    .append_description_of(self.key_matcher)        \
+                    .append_text(': ')                              \
+                    .append_description_of(self.value_matcher)      \
+                    .append_text(']')
+
+
+def has_entry(key_match, value_match):
+    """Matches if dictionary contains key-value entry satisfying a given pair
+    of matchers.
+
+    :param key_match: The matcher to satisfy for the key, or an expected value
+        for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+    :param value_match: The matcher to satisfy for the value, or an expected
+        value for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    This matcher iterates the evaluated dictionary, searching for any key-value
+    entry that satisfies ``key_match`` and ``value_match``. If a matching entry
+    is found, ``has_entry`` is satisfied.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    Examples::
+
+        has_entry(equal_to('foo'), equal_to(1))
+        has_entry('foo', 1)
+
+    """
+    return IsDictContaining(wrap_matcher(key_match), wrap_matcher(value_match))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingentries.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingentries.py
new file mode 100644
index 0000000000..eb83ade52d
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingentries.py
@@ -0,0 +1,133 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsDictContainingEntries(BaseMatcher):
+
+    def __init__(self, value_matchers):
+        self.value_matchers = sorted(value_matchers.items())
+
+    def _not_a_dictionary(self, dictionary, mismatch_description):
+        if mismatch_description:
+            mismatch_description.append_description_of(dictionary) \
+                                .append_text(' is not a mapping object')
+        return False
+
+    def matches(self, dictionary, mismatch_description=None):
+        for key, value_matcher in self.value_matchers:
+
+            try:
+                if not key in dictionary:
+                    if mismatch_description:
+                        mismatch_description.append_text('no ')             \
+                                            .append_description_of(key)     \
+                                            .append_text(' key in ')        \
+                                            .append_description_of(dictionary)
+                    return False
+            except TypeError:
+                return self._not_a_dictionary(dictionary, mismatch_description)
+
+            try:
+                actual_value = dictionary[key]
+            except TypeError:
+                return self._not_a_dictionary(dictionary, mismatch_description)
+
+            if not value_matcher.matches(actual_value):
+                if mismatch_description:
+                    mismatch_description.append_text('value for ')  \
+                                        .append_description_of(key) \
+                                        .append_text(' ')
+                    value_matcher.describe_mismatch(actual_value, mismatch_description)
+                return False
+
+        return True
+
+    def describe_mismatch(self, item, mismatch_description):
+        self.matches(item, mismatch_description)
+
+    def describe_keyvalue(self, index, value, description):
+        """Describes key-value pair at given index."""
+        description.append_description_of(index)                        \
+                   .append_text(': ')                                   \
+                   .append_description_of(value)
+
+    def describe_to(self, description):
+        description.append_text('a dictionary containing {')
+        first = True
+        for key, value in self.value_matchers:
+            if not first:
+                description.append_text(', ')
+            self.describe_keyvalue(key, value, description)
+            first = False
+        description.append_text('}')
+
+
+def has_entries(*keys_valuematchers, **kv_args):
+    """Matches if dictionary contains entries satisfying a dictionary of keys
+    and corresponding value matchers.
+
+    :param matcher_dict: A dictionary mapping keys to associated value matchers,
+        or to expected values for
+        :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    Note that the keys must be actual keys, not matchers. Any value argument
+    that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    Examples::
+
+        has_entries({'foo':equal_to(1), 'bar':equal_to(2)})
+        has_entries({'foo':1, 'bar':2})
+
+    ``has_entries`` also accepts a list of keyword arguments:
+
+    .. function:: has_entries(keyword1=value_matcher1[, keyword2=value_matcher2[, ...]])
+
+    :param keyword1: A keyword to look up.
+    :param valueMatcher1: The matcher to satisfy for the value, or an expected
+        value for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    Examples::
+
+        has_entries(foo=equal_to(1), bar=equal_to(2))
+        has_entries(foo=1, bar=2)
+
+    Finally, ``has_entries`` also accepts a list of alternating keys and their
+    value matchers:
+
+    .. function:: has_entries(key1, value_matcher1[, ...])
+
+    :param key1: A key (not a matcher) to look up.
+    :param valueMatcher1: The matcher to satisfy for the value, or an expected
+        value for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    Examples::
+
+        has_entries('foo', equal_to(1), 'bar', equal_to(2))
+        has_entries('foo', 1, 'bar', 2)
+
+    """
+    if len(keys_valuematchers) == 1:
+        try:
+            base_dict = keys_valuematchers[0].copy()
+            for key in base_dict:
+                base_dict[key] = wrap_matcher(base_dict[key])
+        except AttributeError:
+            raise ValueError('single-argument calls to has_entries must pass a dict as the argument')
+    else:
+        if len(keys_valuematchers) % 2:
+            raise ValueError('has_entries requires key-value pairs')
+        base_dict = {}
+        for index in range(int(len(keys_valuematchers) / 2)):
+            base_dict[keys_valuematchers[2 * index]] = wrap_matcher(keys_valuematchers[2 * index + 1])
+
+    for key, value in kv_args.items():
+        base_dict[key] = wrap_matcher(value)
+
+    return IsDictContainingEntries(base_dict)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingkey.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingkey.py
new file mode 100644
index 0000000000..ccb51e6396
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingkey.py
@@ -0,0 +1,48 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsDictContainingKey(BaseMatcher):
+
+    def __init__(self, key_matcher):
+        self.key_matcher = key_matcher
+
+    def _matches(self, dictionary):
+        if hasmethod(dictionary, 'keys'):
+            for key in dictionary.keys():
+                if self.key_matcher.matches(key):
+                    return True
+        return False
+
+    def describe_to(self, description):
+        description.append_text('a dictionary containing key ')     \
+                    .append_description_of(self.key_matcher)
+
+
+def has_key(key_match):
+    """Matches if dictionary contains an entry whose key satisfies a given
+    matcher.
+
+    :param key_match: The matcher to satisfy for the key, or an expected value
+        for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    This matcher iterates the evaluated dictionary, searching for any key-value
+    entry whose key satisfies the given matcher. If a matching entry is found,
+    ``has_key`` is satisfied.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    Examples::
+
+        has_key(equal_to('foo'))
+        has_key('foo')
+
+    """
+    return IsDictContainingKey(wrap_matcher(key_match))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingvalue.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingvalue.py
new file mode 100644
index 0000000000..e5288841b2
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isdict_containingvalue.py
@@ -0,0 +1,48 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsDictContainingValue(BaseMatcher):
+
+    def __init__(self, value_matcher):
+        self.value_matcher = value_matcher
+
+    def _matches(self, dictionary):
+        if hasmethod(dictionary, 'values'):
+            for value in dictionary.values():
+                if self.value_matcher.matches(value):
+                    return True
+        return False
+
+    def describe_to(self, description):
+        description.append_text('a dictionary containing value ')   \
+                    .append_description_of(self.value_matcher)
+
+
+def has_value(value):
+    """Matches if dictionary contains an entry whose value satisfies a given
+    matcher.
+
+    :param value_match: The matcher to satisfy for the value, or an expected
+        value for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    This matcher iterates the evaluated dictionary, searching for any key-value
+    entry whose value satisfies the given matcher. If a matching entry is
+    found, ``has_value`` is satisfied.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    Examples::
+
+        has_value(equal_to('bar'))
+        has_value('bar')
+
+    """
+    return IsDictContainingValue(wrap_matcher(value))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/isin.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isin.py
new file mode 100644
index 0000000000..87962a2474
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/isin.py
@@ -0,0 +1,30 @@
+from hamcrest.core.base_matcher import BaseMatcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsIn(BaseMatcher):
+
+    def __init__(self, sequence):
+        self.sequence = sequence
+
+    def _matches(self, item):
+        return item in self.sequence
+
+    def describe_to(self, description):
+        description.append_text('one of ')      \
+                   .append_list('(', ', ', ')', self.sequence)
+
+
+def is_in(sequence):
+    """Matches if evaluated object is present in a given sequence.
+
+    :param sequence: The sequence to search.
+
+    This matcher invokes the ``in`` membership operator to determine if the
+    evaluated object is a member of the sequence.
+
+    """
+    return IsIn(sequence)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containing.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containing.py
new file mode 100644
index 0000000000..21939b3f3f
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containing.py
@@ -0,0 +1,89 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.core.allof import all_of
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+
+class IsSequenceContaining(BaseMatcher):
+
+    def __init__(self, element_matcher):
+        self.element_matcher = element_matcher
+
+    def _matches(self, sequence):
+        try:
+            for item in sequence:
+                if self.element_matcher.matches(item):
+                    return True
+        except TypeError: # not a sequence
+            return False
+
+    def describe_to(self, description):
+        description.append_text('a sequence containing ')           \
+                    .append_description_of(self.element_matcher)
+
+
+# It'd be great to make use of all_of, but we can't be sure we won't
+# be seeing a one-time sequence here (like a generator); see issue #20
+# Instead, we wrap it inside a class that will convert the sequence into
+# a concrete list and then hand it off to the all_of matcher.
+class IsSequenceContainingEvery(BaseMatcher):
+
+    def __init__(self, *element_matchers):
+        delegates = [has_item(e) for e in element_matchers]
+        self.matcher = all_of(*delegates)
+
+    def _matches(self, sequence):
+        try:
+            return self.matcher.matches(list(sequence))
+        except TypeError:
+            return False
+
+    def describe_mismatch(self, item, mismatch_description):
+        self.matcher.describe_mismatch(item, mismatch_description)
+
+    def describe_to(self, description):
+        self.matcher.describe_to(description)
+
+
+
+def has_item(match):
+    """Matches if any element of sequence satisfies a given matcher.
+
+    :param match: The matcher to satisfy, or an expected value for
+        :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    This matcher iterates the evaluated sequence, searching for any element
+    that satisfies a given matcher. If a matching element is found,
+    ``has_item`` is satisfied.
+
+    If the ``match`` argument is not a matcher, it is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+    return IsSequenceContaining(wrap_matcher(match))
+
+
+def has_items(*items):
+    """Matches if all of the given matchers are satisfied by any elements of
+    the sequence.
+
+    :param match1,...: A comma-separated list of matchers.
+
+    This matcher iterates the given matchers, searching for any elements in the
+    evaluated sequence that satisfy them. If each matcher is satisfied, then
+    ``has_items`` is satisfied.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+    matchers = []
+    for item in items:
+        matchers.append(wrap_matcher(item))
+    return IsSequenceContainingEvery(*matchers)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginanyorder.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginanyorder.py
new file mode 100644
index 0000000000..78e2b006fc
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginanyorder.py
@@ -0,0 +1,97 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class MatchInAnyOrder(object):
+    def __init__(self, matchers, mismatch_description):
+        self.matchers = matchers[:]
+        self.mismatch_description = mismatch_description
+
+    def matches(self, item):
+        return self.isnotsurplus(item) and self.ismatched(item)
+
+    def isfinished(self, sequence):
+        if not self.matchers:
+            return True
+        if self.mismatch_description:
+            self.mismatch_description.append_text('no item matches: ')      \
+                                .append_list('', ', ', '', self.matchers)   \
+                                .append_text(' in ')                        \
+                                .append_list('[', ', ', ']', sequence)
+        return False
+
+    def isnotsurplus(self, item):
+        if not self.matchers:
+            if self.mismatch_description:
+                self.mismatch_description.append_text('not matched: ')  \
+                                         .append_description_of(item)
+            return False
+        return True
+
+    def ismatched(self, item):
+        for index, matcher in enumerate(self.matchers):
+            if matcher.matches(item):
+                del self.matchers[index]
+                return True
+
+        if self.mismatch_description:
+            self.mismatch_description.append_text('not matched: ')  \
+                                     .append_description_of(item)
+        return False
+
+
+class IsSequenceContainingInAnyOrder(BaseMatcher):
+
+    def __init__(self, matchers):
+        self.matchers = matchers
+
+    def matches(self, sequence, mismatch_description=None):
+        try:
+            sequence = list(sequence)
+            matchsequence = MatchInAnyOrder(self.matchers, mismatch_description)
+            for item in sequence:
+                if not matchsequence.matches(item):
+                    return False
+            return matchsequence.isfinished(sequence)
+        except TypeError:
+            if mismatch_description:
+                super(IsSequenceContainingInAnyOrder, self)             \
+                    .describe_mismatch(sequence, mismatch_description)
+            return False
+
+    def describe_mismatch(self, item, mismatch_description):
+        self.matches(item, mismatch_description)
+
+    def describe_to(self, description):
+        description.append_text('a sequence over ')             \
+                   .append_list('[', ', ', ']', self.matchers)  \
+                   .append_text(' in any order')
+
+
+def contains_inanyorder(*items):
+    """Matches if sequences's elements, in any order, satisfy a given list of
+    matchers.
+
+    :param match1,...: A comma-separated list of matchers.
+
+    This matcher iterates the evaluated sequence, seeing if each element
+    satisfies any of the given matchers. The matchers are tried from left to
+    right, and when a satisfied matcher is found, it is no longer a candidate
+    for the remaining elements. If a one-to-one correspondence is established
+    between elements and matchers, ``contains_inanyorder`` is satisfied.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+
+    matchers = []
+    for item in items:
+        matchers.append(wrap_matcher(item))
+    return IsSequenceContainingInAnyOrder(matchers)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginorder.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginorder.py
new file mode 100644
index 0000000000..3fd91a6c92
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_containinginorder.py
@@ -0,0 +1,88 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+
+class MatchingInOrder(object):
+    def __init__(self, matchers, mismatch_description):
+        self.matchers = matchers
+        self.mismatch_description = mismatch_description
+        self.next_match_index = 0
+
+    def matches(self, item):
+        return self.isnotsurplus(item) and self.ismatched(item)
+
+    def isfinished(self):
+        if self.next_match_index < len(self.matchers):
+            if self.mismatch_description:
+                self.mismatch_description.append_text('No item matched: ') \
+                                 .append_description_of(self.matchers[self.next_match_index])
+            return False
+        return True
+
+    def ismatched(self, item):
+        matcher = self.matchers[self.next_match_index]
+        if not matcher.matches(item):
+            if self.mismatch_description:
+                self.mismatch_description.append_text('item ' + str(self.next_match_index) + ': ')
+                matcher.describe_mismatch(item, self.mismatch_description)
+            return False
+        self.next_match_index += 1
+        return True
+
+    def isnotsurplus(self, item):
+        if len(self.matchers) <= self.next_match_index:
+            if self.mismatch_description:
+                self.mismatch_description.append_text('Not matched: ')  \
+                                         .append_description_of(item)
+            return False
+        return True
+
+
+class IsSequenceContainingInOrder(BaseMatcher):
+
+    def __init__(self, matchers):
+        self.matchers = matchers
+
+    def matches(self, sequence, mismatch_description=None):
+        try:
+            matchsequence = MatchingInOrder(self.matchers, mismatch_description)
+            for item in sequence:
+                if not matchsequence.matches(item):
+                    return False
+            return matchsequence.isfinished()
+        except TypeError:
+            if mismatch_description:
+                super(IsSequenceContainingInOrder, self)                \
+                    .describe_mismatch(sequence, mismatch_description)
+            return False
+
+    def describe_mismatch(self, item, mismatch_description):
+        self.matches(item, mismatch_description)
+
+    def describe_to(self, description):
+        description.append_text('a sequence containing ')   \
+                   .append_list('[', ', ', ']', self.matchers)
+
+
+def contains(*items):
+    """Matches if sequence's elements satisfy a given list of matchers, in order.
+
+    :param match1,...: A comma-separated list of matchers.
+
+    This matcher iterates the evaluated sequence and a given list of matchers,
+    seeing if each element satisfies its corresponding matcher.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+    matchers = []
+    for item in items:
+        matchers.append(wrap_matcher(item))
+    return IsSequenceContainingInOrder(matchers)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_onlycontaining.py b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_onlycontaining.py
new file mode 100644
index 0000000000..bd52c10419
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/collection/issequence_onlycontaining.py
@@ -0,0 +1,55 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.core.anyof import any_of
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsSequenceOnlyContaining(BaseMatcher):
+
+    def __init__(self, matcher):
+        self.matcher = matcher
+
+    def _matches(self, sequence):
+        try:
+            sequence = list(sequence)
+            if len(sequence) == 0:
+                return False
+            for item in sequence:
+                if not self.matcher.matches(item):
+                    return False
+            return True
+        except TypeError:
+            return False
+
+    def describe_to(self, description):
+        description.append_text('a sequence containing items matching ')    \
+                    .append_description_of(self.matcher)
+
+
+def only_contains(*items):
+    """Matches if each element of sequence satisfies any of the given matchers.
+
+    :param match1,...: A comma-separated list of matchers.
+
+    This matcher iterates the evaluated sequence, confirming whether each
+    element satisfies any of the given matchers.
+
+    Example::
+
+        only_contains(less_than(4))
+
+    will match ``[3,1,2]``.
+
+    Any argument that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    """
+    matchers = []
+    for item in items:
+        matchers.append(wrap_matcher(item))
+    return IsSequenceOnlyContaining(any_of(*matchers))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/integration/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/library/integration/__init__.py
new file mode 100644
index 0000000000..cc1e132163
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/integration/__init__.py
@@ -0,0 +1,8 @@
+from __future__ import absolute_import
+"""Utilities for integrating Hamcrest with other libraries."""
+
+from .match_equality import match_equality
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/integration/match_equality.py b/contrib/python/PyHamcrest/src/hamcrest/library/integration/match_equality.py
new file mode 100644
index 0000000000..52da054760
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/integration/match_equality.py
@@ -0,0 +1,42 @@
+from hamcrest.core.string_description import tostring
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Chris Rose"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+__unittest = True
+
+
+class EqualityWrapper(object):
+
+    def __init__(self, matcher):
+        self.matcher = matcher
+
+    def __eq__(self, object):
+        return self.matcher.matches(object)
+
+    def __str__(self):
+        return repr(self)
+
+    def __repr__(self):
+        return tostring(self.matcher)
+
+
+def match_equality(matcher):
+    """Wraps a matcher to define equality in terms of satisfying the matcher.
+
+    ``match_equality`` allows Hamcrest matchers to be used in libraries that
+    are not Hamcrest-aware. They might use the equality operator::
+
+        assert match_equality(matcher) == object
+
+    Or they might provide a method that uses equality for its test::
+
+        library.method_that_tests_eq(match_equality(matcher))
+
+    One concrete example is integrating with the ``assert_called_with`` methods
+    in Michael Foord's `mock <http://www.voidspace.org.uk/python/mock/>`_
+    library.
+
+    """
+    return EqualityWrapper(wrap_matcher(matcher))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/number/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/library/number/__init__.py
new file mode 100644
index 0000000000..5087faf846
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/number/__init__.py
@@ -0,0 +1,9 @@
+from __future__ import absolute_import
+"""Matchers that perform numeric comparisons."""
+
+from .iscloseto import close_to
+from .ordering_comparison import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/number/iscloseto.py b/contrib/python/PyHamcrest/src/hamcrest/library/number/iscloseto.py
new file mode 100644
index 0000000000..e401615e35
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/number/iscloseto.py
@@ -0,0 +1,74 @@
+import six
+from hamcrest.core.base_matcher import BaseMatcher
+from math import fabs
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+def isnumeric(value):
+    """Confirm that 'value' can be treated numerically; duck-test accordingly
+    """
+    if isinstance(value, (float, complex) + six.integer_types):
+        return True
+
+    try:
+        _ = (fabs(value) + 0 - 0) * 1
+        return True
+    except ArithmeticError:
+        return True
+    except:
+        return False
+    return False
+
+
+class IsCloseTo(BaseMatcher):
+
+    def __init__(self, value, delta):
+        if not isnumeric(value):
+            raise TypeError('IsCloseTo value must be numeric')
+        if not isnumeric(delta):
+            raise TypeError('IsCloseTo delta must be numeric')
+
+        self.value = value
+        self.delta = delta
+
+    def _matches(self, item):
+        if not isnumeric(item):
+            return False
+        return fabs(item - self.value) <= self.delta
+
+    def describe_mismatch(self, item, mismatch_description):
+        if not isnumeric(item):
+            super(IsCloseTo, self).describe_mismatch(item, mismatch_description)
+        else:
+            actual_delta = fabs(item - self.value)
+            mismatch_description.append_description_of(item)            \
+                                .append_text(' differed by ')           \
+                                .append_description_of(actual_delta)
+
+    def describe_to(self, description):
+        description.append_text('a numeric value within ')  \
+                   .append_description_of(self.delta)       \
+                   .append_text(' of ')                     \
+                   .append_description_of(self.value)
+
+
+def close_to(value, delta):
+    """Matches if object is a number close to a given value, within a given
+    delta.
+
+    :param value: The value to compare against as the expected value.
+    :param delta: The maximum delta between the values for which the numbers
+        are considered close.
+
+    This matcher compares the evaluated object against ``value`` to see if the
+    difference is within a positive ``delta``.
+
+    Example::
+
+        close_to(3.0, 0.25)
+
+    """
+    return IsCloseTo(value, delta)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/number/ordering_comparison.py b/contrib/python/PyHamcrest/src/hamcrest/library/number/ordering_comparison.py
new file mode 100644
index 0000000000..c3c75f425d
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/number/ordering_comparison.py
@@ -0,0 +1,59 @@
+from hamcrest.core.base_matcher import BaseMatcher
+import operator
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class OrderingComparison(BaseMatcher):
+
+    def __init__(self, value, comparison_function, comparison_description):
+        self.value = value
+        self.comparison_function = comparison_function
+        self.comparison_description = comparison_description
+
+    def _matches(self, item):
+        return self.comparison_function(item, self.value)
+
+    def describe_to(self, description):
+        description.append_text('a value ')                     \
+                   .append_text(self.comparison_description)    \
+                   .append_text(' ')                            \
+                   .append_description_of(self.value)
+
+
+def greater_than(value):
+    """Matches if object is greater than a given value.
+
+    :param value: The value to compare against.
+
+    """
+    return OrderingComparison(value, operator.gt, 'greater than')
+
+
+def greater_than_or_equal_to(value):
+    """Matches if object is greater than or equal to a given value.
+
+    :param value: The value to compare against.
+
+    """
+    return OrderingComparison(value, operator.ge, 'greater than or equal to')
+
+
+def less_than(value):
+    """Matches if object is less than a given value.
+
+    :param value: The value to compare against.
+
+    """
+    return OrderingComparison(value, operator.lt, 'less than')
+
+
+def less_than_or_equal_to(value):
+    """Matches if object is less than or equal to a given value.
+
+    :param value: The value to compare against.
+
+    """
+    return OrderingComparison(value, operator.le, 'less than or equal to')
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/object/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/library/object/__init__.py
new file mode 100644
index 0000000000..5ca4556661
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/object/__init__.py
@@ -0,0 +1,10 @@
+from __future__ import absolute_import
+"""Matchers that inspect objects and classes."""
+
+from .haslength import has_length
+from .hasproperty import has_property, has_properties
+from .hasstring import has_string
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/object/haslength.py b/contrib/python/PyHamcrest/src/hamcrest/library/object/haslength.py
new file mode 100644
index 0000000000..3ef0ab5b81
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/object/haslength.py
@@ -0,0 +1,50 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class HasLength(BaseMatcher):
+
+    def __init__(self, len_matcher):
+        self.len_matcher = len_matcher
+
+    def _matches(self, item):
+        if not hasmethod(item, '__len__'):
+            return False
+        return self.len_matcher.matches(len(item))
+
+    def describe_mismatch(self, item, mismatch_description):
+        super(HasLength, self).describe_mismatch(item, mismatch_description)
+        if hasmethod(item, '__len__'):
+            mismatch_description.append_text(' with length of ')    \
+                                .append_description_of(len(item))
+
+    def describe_to(self, description):
+        description.append_text('an object with length of ')    \
+                    .append_description_of(self.len_matcher)
+
+
+def has_length(match):
+    """Matches if ``len(item)`` satisfies a given matcher.
+
+    :param match: The matcher to satisfy, or an expected value for
+        :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    This matcher invokes the :py:func:`len` function on the evaluated object to
+    get its length, passing the result to a given matcher for evaluation.
+
+    If the ``match`` argument is not a matcher, it is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    :equality.
+
+    Examples::
+
+        has_length(greater_than(6))
+        has_length(5)
+
+    """
+    return HasLength(wrap_matcher(match))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/object/hasproperty.py b/contrib/python/PyHamcrest/src/hamcrest/library/object/hasproperty.py
new file mode 100644
index 0000000000..d2536d69f4
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/object/hasproperty.py
@@ -0,0 +1,154 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core import anything
+from hamcrest.core.core.allof import all_of
+from hamcrest.core.string_description import StringDescription
+from hamcrest.core.helpers.hasmethod import hasmethod
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher as wrap_shortcut
+
+__author__ = "Chris Rose"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class IsObjectWithProperty(BaseMatcher):
+
+    def __init__(self, property_name, value_matcher):
+        self.property_name = property_name
+        self.value_matcher = value_matcher
+
+    def _matches(self, o):
+        if o is None:
+            return False
+
+        if not hasattr(o, self.property_name):
+            return False
+
+        value = getattr(o, self.property_name)
+        return self.value_matcher.matches(value)
+
+    def describe_to(self, description):
+        description.append_text("an object with a property '") \
+                                        .append_text(self.property_name) \
+                                        .append_text("' matching ") \
+                                        .append_description_of(self.value_matcher)
+
+    def describe_mismatch(self, item, mismatch_description):
+        if item is None:
+            mismatch_description.append_text('was None')
+            return
+
+        if not hasattr(item, self.property_name):
+            mismatch_description.append_value(item) \
+                                                    .append_text(' did not have the ') \
+                                                    .append_value(self.property_name) \
+                                                    .append_text(' property')
+            return
+
+        mismatch_description.append_text('property ').append_value(self.property_name).append_text(' ')
+        value = getattr(item, self.property_name)
+        self.value_matcher.describe_mismatch(value, mismatch_description)
+
+    def __str__(self):
+        d = StringDescription()
+        self.describe_to(d)
+        return str(d)
+
+
+def has_property(name, match=None):
+    """Matches if object has a property with a given name whose value satisfies
+    a given matcher.
+
+    :param name: The name of the property.
+    :param match: Optional matcher to satisfy.
+
+    This matcher determines if the evaluated object has a property with a given
+    name. If no such property is found, ``has_property`` is not satisfied.
+
+    If the property is found, its value is passed to a given matcher for
+    evaluation. If the ``match`` argument is not a matcher, it is implicitly
+    wrapped in an :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to
+    check for equality.
+
+    If the ``match`` argument is not provided, the
+    :py:func:`~hamcrest.core.core.isanything.anything` matcher is used so that
+    ``has_property`` is satisfied if a matching property is found.
+
+    Examples::
+
+        has_property('name', starts_with('J'))
+        has_property('name', 'Jon')
+        has_property('name')
+
+    """
+
+    if match is None:
+        match = anything()
+
+    return IsObjectWithProperty(name, wrap_shortcut(match))
+
+
+def has_properties(*keys_valuematchers, **kv_args):
+    """Matches if an object has properties satisfying all of a dictionary
+    of string property names and corresponding value matchers.
+
+    :param matcher_dict: A dictionary mapping keys to associated value matchers,
+        or to expected values for
+        :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    Note that the keys must be actual keys, not matchers. Any value argument
+    that is not a matcher is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    Examples::
+
+        has_properties({'foo':equal_to(1), 'bar':equal_to(2)})
+        has_properties({'foo':1, 'bar':2})
+
+    ``has_properties`` also accepts a list of keyword arguments:
+
+    .. function:: has_properties(keyword1=value_matcher1[, keyword2=value_matcher2[, ...]])
+
+    :param keyword1: A keyword to look up.
+    :param valueMatcher1: The matcher to satisfy for the value, or an expected
+        value for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    Examples::
+
+        has_properties(foo=equal_to(1), bar=equal_to(2))
+        has_properties(foo=1, bar=2)
+
+    Finally, ``has_properties`` also accepts a list of alternating keys and their
+    value matchers:
+
+    .. function:: has_properties(key1, value_matcher1[, ...])
+
+    :param key1: A key (not a matcher) to look up.
+    :param valueMatcher1: The matcher to satisfy for the value, or an expected
+        value for :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    Examples::
+
+        has_properties('foo', equal_to(1), 'bar', equal_to(2))
+        has_properties('foo', 1, 'bar', 2)
+
+    """
+    if len(keys_valuematchers) == 1:
+        try:
+            base_dict = keys_valuematchers[0].copy()
+            for key in base_dict:
+                base_dict[key] = wrap_shortcut(base_dict[key])
+        except AttributeError:
+            raise ValueError('single-argument calls to has_properties must pass a dict as the argument')
+    else:
+        if len(keys_valuematchers) % 2:
+            raise ValueError('has_properties requires key-value pairs')
+        base_dict = {}
+        for index in range(int(len(keys_valuematchers) / 2)):
+            base_dict[keys_valuematchers[2 * index]] = wrap_shortcut(keys_valuematchers[2 * index + 1])
+
+    for key, value in kv_args.items():
+        base_dict[key] = wrap_shortcut(value)
+
+    return all_of(*[has_property(property_name, property_value_matcher) for \
+                   property_name, property_value_matcher in base_dict.items()])
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/object/hasstring.py b/contrib/python/PyHamcrest/src/hamcrest/library/object/hasstring.py
new file mode 100644
index 0000000000..8b50547e21
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/object/hasstring.py
@@ -0,0 +1,40 @@
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.wrap_matcher import wrap_matcher
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class HasString(BaseMatcher):
+
+    def __init__(self, str_matcher):
+        self.str_matcher = str_matcher
+
+    def _matches(self, item):
+        return self.str_matcher.matches(str(item))
+
+    def describe_to(self, description):
+        description.append_text('an object with str ')          \
+                    .append_description_of(self.str_matcher)
+
+
+def has_string(match):
+    """Matches if ``str(item)`` satisfies a given matcher.
+
+    :param match: The matcher to satisfy, or an expected value for
+        :py:func:`~hamcrest.core.core.isequal.equal_to` matching.
+
+    This matcher invokes the :py:func:`str` function on the evaluated object to
+    get its length, passing the result to a given matcher for evaluation. If
+    the ``match`` argument is not a matcher, it is implicitly wrapped in an
+    :py:func:`~hamcrest.core.core.isequal.equal_to` matcher to check for
+    equality.
+
+    Examples::
+
+        has_string(starts_with('foo'))
+        has_string('bar')
+
+    """
+    return HasString(wrap_matcher(match))
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/__init__.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/__init__.py
new file mode 100644
index 0000000000..39d0e8b382
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/__init__.py
@@ -0,0 +1,14 @@
+from __future__ import absolute_import
+"""Matchers that perform text comparisons."""
+
+from .isequal_ignoring_case import equal_to_ignoring_case
+from .isequal_ignoring_whitespace import equal_to_ignoring_whitespace
+from .stringcontains import contains_string
+from .stringendswith import ends_with
+from .stringstartswith import starts_with
+from .stringmatches import matches_regexp
+from .stringcontainsinorder import string_contains_in_order
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_case.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_case.py
new file mode 100644
index 0000000000..d1ee2d17fc
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_case.py
@@ -0,0 +1,43 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+
+import six
+
+class IsEqualIgnoringCase(BaseMatcher):
+
+    def __init__(self, string):
+        if not isinstance(string, six.string_types):
+            raise TypeError('IsEqualIgnoringCase requires string')
+        self.original_string = string
+        self.lowered_string = string.lower()
+
+    def _matches(self, item):
+        if not isinstance(item, six.string_types):
+            return False
+        return self.lowered_string == item.lower()
+
+    def describe_to(self, description):
+        description.append_description_of(self.original_string)    \
+                   .append_text(' ignoring case')
+
+
+def equal_to_ignoring_case(string):
+    """Matches if object is a string equal to a given string, ignoring case
+    differences.
+
+    :param string: The string to compare against as the expected value.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it compares it with ``string``, ignoring differences of case.
+
+    Example::
+
+        equal_to_ignoring_case("hello world")
+
+    will match "heLLo WorlD".
+
+    """
+    return IsEqualIgnoringCase(string)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_whitespace.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_whitespace.py
new file mode 100644
index 0000000000..86fa997601
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/isequal_ignoring_whitespace.py
@@ -0,0 +1,57 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+
+import six
+
+def stripspace(string):
+    result = ''
+    last_was_space = True
+    for character in string:
+        if character.isspace():
+            if not last_was_space:
+                result += ' '
+            last_was_space = True
+        else:
+            result += character
+            last_was_space = False
+    return result.strip()
+
+
+class IsEqualIgnoringWhiteSpace(BaseMatcher):
+
+    def __init__(self, string):
+        if not isinstance(string, six.string_types):
+            raise TypeError('IsEqualIgnoringWhiteSpace requires string')
+        self.original_string = string
+        self.stripped_string = stripspace(string)
+
+    def _matches(self, item):
+        if not isinstance(item, six.string_types):
+            return False
+        return self.stripped_string == stripspace(item)
+
+    def describe_to(self, description):
+        description.append_description_of(self.original_string)    \
+                   .append_text(' ignoring whitespace')
+
+
+def equal_to_ignoring_whitespace(string):
+    """Matches if object is a string equal to a given string, ignoring
+    differences in whitespace.
+
+    :param string: The string to compare against as the expected value.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it compares it with ``string``, ignoring differences in runs of whitespace.
+
+    Example::
+
+        equal_to_ignoring_whitespace("hello world")
+
+    will match ``"hello   world"``.
+
+    """
+    return IsEqualIgnoringWhiteSpace(string)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontains.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontains.py
new file mode 100644
index 0000000000..58ffd283c6
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontains.py
@@ -0,0 +1,38 @@
+from hamcrest.library.text.substringmatcher import SubstringMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class StringContains(SubstringMatcher):
+
+    def __init__(self, substring):
+        super(StringContains, self).__init__(substring)
+
+    def _matches(self, item):
+        if not hasmethod(item, 'find'):
+            return False
+        return item.find(self.substring) >= 0
+
+    def relationship(self):
+        return 'containing'
+
+
+def contains_string(substring):
+    """Matches if object is a string containing a given string.
+
+    :param string: The string to search for.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it checks whether it contains ``string``.
+
+    Example::
+
+        contains_string("def")
+
+    will match "abcdefg".
+
+    """
+    return StringContains(substring)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontainsinorder.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontainsinorder.py
new file mode 100644
index 0000000000..516b1043ab
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringcontainsinorder.py
@@ -0,0 +1,52 @@
+__author__ = "Romilly Cocking"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+
+import six
+
+class StringContainsInOrder(BaseMatcher):
+
+    def __init__(self, *substrings):
+        for substring in substrings:
+            if not isinstance(substring, six.string_types):
+                raise TypeError(self.__class__.__name__
+                                + ' requires string arguments')
+        self.substrings = substrings
+
+    def _matches(self, item):
+        if not hasmethod(item, 'find'):
+            return False
+        from_index = 0
+        for substring in self.substrings:
+            from_index = item.find(substring, from_index)
+            if from_index == -1:
+                return False
+        return True
+
+    def describe_to(self, description):
+        description.append_list('a string containing ', ', ', ' in order',
+                                self.substrings)
+
+
+def string_contains_in_order(*substrings):
+    """Matches if object is a string containing a given list of substrings in
+    relative order.
+
+    :param string1,...:  A comma-separated list of strings.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it checks whether it contains a given list of strings, in relative order to
+    each other. The searches are performed starting from the beginning of the
+    evaluated string.
+
+    Example::
+
+        string_contains_in_order("bc", "fg", "jkl")
+
+    will match "abcdefghijklm".
+
+    """
+    return StringContainsInOrder(*substrings)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/stringendswith.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringendswith.py
new file mode 100644
index 0000000000..43f9c3d302
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringendswith.py
@@ -0,0 +1,39 @@
+from hamcrest.library.text.substringmatcher import SubstringMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+
+class StringEndsWith(SubstringMatcher):
+
+    def __init__(self, substring):
+        super(StringEndsWith, self).__init__(substring)
+
+    def _matches(self, item):
+        if not hasmethod(item, 'endswith'):
+            return False
+        return item.endswith(self.substring)
+
+    def relationship(self):
+        return 'ending with'
+
+
+def ends_with(string):
+    """Matches if object is a string ending with a given string.
+
+    :param string: The string to search for.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it checks if ``string`` matches the ending characters of the evaluated
+    object.
+
+    Example::
+
+        ends_with("bar")
+
+    will match "foobar".
+
+    """
+    return StringEndsWith(string)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/stringmatches.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringmatches.py
new file mode 100644
index 0000000000..2a16e29729
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringmatches.py
@@ -0,0 +1,40 @@
+__author__ = "Chris Rose"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+import re
+
+import six
+
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+
+class StringMatchesPattern(BaseMatcher):
+
+    def __init__(self, pattern):
+        self.pattern = pattern
+
+    def describe_to(self, description):
+        description.append_text("a string matching '") \
+                                   .append_text(self.pattern.pattern) \
+                                   .append_text("'")
+
+    def _matches(self, item):
+        return self.pattern.search(item) is not None
+
+
+def matches_regexp(pattern):
+    """Matches if object is a string containing a match for a given regular
+    expression.
+
+    :param pattern: The regular expression to search for.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it checks if the regular expression ``pattern`` matches anywhere within the
+    evaluated object.
+
+    """
+    if isinstance(pattern, six.string_types):
+        pattern = re.compile(pattern)
+
+    return StringMatchesPattern(pattern)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/stringstartswith.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringstartswith.py
new file mode 100644
index 0000000000..a0af49c9c2
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/stringstartswith.py
@@ -0,0 +1,39 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.library.text.substringmatcher import SubstringMatcher
+from hamcrest.core.helpers.hasmethod import hasmethod
+
+
+class StringStartsWith(SubstringMatcher):
+
+    def __init__(self, substring):
+        super(StringStartsWith, self).__init__(substring)
+
+    def _matches(self, item):
+        if not hasmethod(item, 'startswith'):
+            return False
+        return item.startswith(self.substring)
+
+    def relationship(self):
+        return 'starting with'
+
+
+def starts_with(substring):
+    """Matches if object is a string starting with a given string.
+
+    :param string: The string to search for.
+
+    This matcher first checks whether the evaluated object is a string. If so,
+    it checks if ``string`` matches the beginning characters of the evaluated
+    object.
+
+    Example::
+
+        starts_with("foo")
+
+    will match "foobar".
+
+    """
+    return StringStartsWith(substring)
diff --git a/contrib/python/PyHamcrest/src/hamcrest/library/text/substringmatcher.py b/contrib/python/PyHamcrest/src/hamcrest/library/text/substringmatcher.py
new file mode 100644
index 0000000000..63ea359a51
--- /dev/null
+++ b/contrib/python/PyHamcrest/src/hamcrest/library/text/substringmatcher.py
@@ -0,0 +1,20 @@
+__author__ = "Jon Reid"
+__copyright__ = "Copyright 2011 hamcrest.org"
+__license__ = "BSD, see License.txt"
+
+from hamcrest.core.base_matcher import BaseMatcher
+
+import six
+
+class SubstringMatcher(BaseMatcher):
+
+    def __init__(self, substring):
+        if not isinstance(substring, six.string_types):
+            raise TypeError(self.__class__.__name__ + ' requires string')
+        self.substring = substring
+
+    def describe_to(self, description):
+        description.append_text('a string ')                \
+                   .append_text(self.relationship())        \
+                   .append_text(' ')                        \
+                   .append_description_of(self.substring)
-- 
cgit v1.2.3