From 68a5b876d804eca34c0e5136e9c47d9cdc81bae4 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Thu, 9 Sep 2021 17:38:55 -0400 Subject: [PATCH 1/2] Vendor the Version package, use it instead of distutils --- seaborn/_core.py | 4 +- seaborn/external/version.py | 461 ++++++++++++++++++++++++++++ seaborn/tests/test_algorithms.py | 8 +- seaborn/tests/test_categorical.py | 10 +- seaborn/tests/test_distributions.py | 4 +- seaborn/tests/test_regression.py | 4 +- seaborn/tests/test_relational.py | 6 +- seaborn/tests/test_utils.py | 7 +- seaborn/utils.py | 4 +- 9 files changed, 484 insertions(+), 24 deletions(-) create mode 100644 seaborn/external/version.py diff --git a/seaborn/_core.py b/seaborn/_core.py index dd14986ff8..32a0f6c78e 100644 --- a/seaborn/_core.py +++ b/seaborn/_core.py @@ -6,7 +6,6 @@ from collections.abc import Iterable, Sequence, Mapping from numbers import Number from datetime import datetime -from distutils.version import LooseVersion import numpy as np import pandas as pd @@ -15,6 +14,7 @@ from ._decorators import ( share_init_params_with_map, ) +from .external.version import Version from .palettes import ( QUAL_PALETTES, color_palette, @@ -1252,7 +1252,7 @@ def _attach( if scale is True: set_scale("log") else: - if LooseVersion(mpl.__version__) >= "3.3": + if Version(mpl.__version__) >= Version("3.3"): set_scale("log", base=scale) else: set_scale("log", **{f"base{axis}": scale}) diff --git a/seaborn/external/version.py b/seaborn/external/version.py new file mode 100644 index 0000000000..7eb57d32ce --- /dev/null +++ b/seaborn/external/version.py @@ -0,0 +1,461 @@ +"""Extract reference documentation from the pypa/packaging source tree. + +In the process of copying, some unused methods / classes were removed. +These include: + +- parse() +- anything involving LegacyVersion + +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. + +Vendored from: +- https://github.com/pypa/packaging/ +- commit ba07d8287b4554754ac7178d177033ea3f75d489 (09/09/2021) +""" + + +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + + +import collections +import itertools +import re +from typing import Callable, Optional, SupportsInt, Tuple, Union + +__all__ = ["Version", "InvalidVersion", "VERSION_PATTERN"] + + +# Vendored from https://github.com/pypa/packaging/blob/main/packaging/_structures.py + +class InfinityType: + def __repr__(self) -> str: + return "Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return False + + def __le__(self, other: object) -> bool: + return False + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __ne__(self, other: object) -> bool: + return not isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return True + + def __ge__(self, other: object) -> bool: + return True + + def __neg__(self: object) -> "NegativeInfinityType": + return NegativeInfinity + + +Infinity = InfinityType() + + +class NegativeInfinityType: + def __repr__(self) -> str: + return "-Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return True + + def __le__(self, other: object) -> bool: + return True + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __ne__(self, other: object) -> bool: + return not isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return False + + def __ge__(self, other: object) -> bool: + return False + + def __neg__(self: object) -> InfinityType: + return Infinity + + +NegativeInfinity = NegativeInfinityType() + + +# Vendored from https://github.com/pypa/packaging/blob/main/packaging/version.py + +InfiniteTypes = Union[InfinityType, NegativeInfinityType] +PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] +SubLocalType = Union[InfiniteTypes, int, str] +LocalType = Union[ + NegativeInfinityType, + Tuple[ + Union[ + SubLocalType, + Tuple[SubLocalType, str], + Tuple[NegativeInfinityType, SubLocalType], + ], + ..., + ], +] +CmpKey = Tuple[ + int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType +] +LegacyCmpKey = Tuple[int, Tuple[str, ...]] +VersionComparisonMethod = Callable[ + [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool +] + +_Version = collections.namedtuple( + "_Version", ["epoch", "release", "dev", "pre", "post", "local"] +) + + + +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + +class _BaseVersion: + _key: Union[CmpKey, LegacyCmpKey] + + def __hash__(self) -> int: + return hash(self._key) + + # Please keep the duplicated `isinstance` check + # in the six comparisons hereunder + # unless you find a way to avoid adding overhead function calls. + def __lt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key < other._key + + def __le__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key <= other._key + + def __eq__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key == other._key + + def __ge__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key >= other._key + + def __gt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key > other._key + + def __ne__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key != other._key + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+
+class Version(_BaseVersion):
+
+    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+
+    def __init__(self, version: str) -> None:
+
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion(f"Invalid version: '{version}'")
+
+        # Store the parsed out pieces of the version
+        self._version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
+            post=_parse_letter_version(
+                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
+            ),
+            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self._version.epoch,
+            self._version.release,
+            self._version.pre,
+            self._version.post,
+            self._version.dev,
+            self._version.local,
+        )
+
+    def __repr__(self) -> str:
+        return f""
+
+    def __str__(self) -> str:
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        # Pre-release
+        if self.pre is not None:
+            parts.append("".join(str(x) for x in self.pre))
+
+        # Post-release
+        if self.post is not None:
+            parts.append(f".post{self.post}")
+
+        # Development release
+        if self.dev is not None:
+            parts.append(f".dev{self.dev}")
+
+        # Local version segment
+        if self.local is not None:
+            parts.append(f"+{self.local}")
+
+        return "".join(parts)
+
+    @property
+    def epoch(self) -> int:
+        _epoch: int = self._version.epoch
+        return _epoch
+
+    @property
+    def release(self) -> Tuple[int, ...]:
+        _release: Tuple[int, ...] = self._version.release
+        return _release
+
+    @property
+    def pre(self) -> Optional[Tuple[str, int]]:
+        _pre: Optional[Tuple[str, int]] = self._version.pre
+        return _pre
+
+    @property
+    def post(self) -> Optional[int]:
+        return self._version.post[1] if self._version.post else None
+
+    @property
+    def dev(self) -> Optional[int]:
+        return self._version.dev[1] if self._version.dev else None
+
+    @property
+    def local(self) -> Optional[str]:
+        if self._version.local:
+            return ".".join(str(x) for x in self._version.local)
+        else:
+            return None
+
+    @property
+    def public(self) -> str:
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self) -> str:
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        return "".join(parts)
+
+    @property
+    def is_prerelease(self) -> bool:
+        return self.dev is not None or self.pre is not None
+
+    @property
+    def is_postrelease(self) -> bool:
+        return self.post is not None
+
+    @property
+    def is_devrelease(self) -> bool:
+        return self.dev is not None
+
+    @property
+    def major(self) -> int:
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self) -> int:
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self) -> int:
+        return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+    letter: str, number: Union[str, bytes, SupportsInt]
+) -> Optional[Tuple[str, int]]:
+
+    if letter:
+        # We consider there to be an implicit 0 in a pre-release if there is
+        # not a numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["c", "pre", "preview"]:
+            letter = "rc"
+        elif letter in ["rev", "r"]:
+            letter = "post"
+
+        return letter, int(number)
+    if not letter and number:
+        # We assume if we are given a number, but we are not given a letter
+        # then this is using the implicit post release syntax (e.g. 1.0-1)
+        letter = "post"
+
+        return letter, int(number)
+
+    return None
+
+
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: str) -> Optional[LocalType]:
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_separators.split(local)
+        )
+    return None
+
+
+def _cmpkey(
+    epoch: int,
+    release: Tuple[int, ...],
+    pre: Optional[Tuple[str, int]],
+    post: Optional[Tuple[str, int]],
+    dev: Optional[Tuple[str, int]],
+    local: Optional[Tuple[SubLocalType]],
+) -> CmpKey:
+
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non zero, then take the rest
+    # re-reverse it back into the correct order and make it a tuple and use
+    # that for our sorting key.
+    _release = tuple(
+        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre segment, but we _only_ want to do this
+    # if there is not a pre or a post segment. If we have one of those then
+    # the normal sorting rules will handle this case correctly.
+    if pre is None and post is None and dev is not None:
+        _pre: PrePostDevType = NegativeInfinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        _pre = Infinity
+    else:
+        _pre = pre
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        _post: PrePostDevType = NegativeInfinity
+
+    else:
+        _post = post
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        _dev: PrePostDevType = Infinity
+
+    else:
+        _dev = dev
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        _local: LocalType = NegativeInfinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alpha numeric segments sort before numeric segments
+        # - Alpha numeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+        )
+
+    return epoch, _release, _pre, _post, _dev, _local
diff --git a/seaborn/tests/test_algorithms.py b/seaborn/tests/test_algorithms.py
index 1baef034e4..07b85ce8d0 100644
--- a/seaborn/tests/test_algorithms.py
+++ b/seaborn/tests/test_algorithms.py
@@ -3,9 +3,9 @@
 
 import pytest
 from numpy.testing import assert_array_equal
-from distutils.version import LooseVersion
 
 from .. import algorithms as algo
+from ..external.version import Version
 
 
 @pytest.fixture
@@ -151,7 +151,7 @@ def test_bootstrap_reproducibility(random):
         assert_array_equal(boots1, boots2)
 
 
-@pytest.mark.skipif(LooseVersion(np.__version__) < "1.17",
+@pytest.mark.skipif(Version(np.__version__) < Version("1.17"),
                     reason="Tests new numpy random functionality")
 def test_seed_new():
 
@@ -177,7 +177,7 @@ def test_seed_new():
         assert (rng1.uniform() == rng2.uniform()) == match
 
 
-@pytest.mark.skipif(LooseVersion(np.__version__) >= "1.17",
+@pytest.mark.skipif(Version(np.__version__) >= Version("1.17"),
                     reason="Tests old numpy random functionality")
 @pytest.mark.parametrize("seed1, seed2, match", [
     (None, None, False),
@@ -194,7 +194,7 @@ def test_seed_old(seed1, seed2, match):
     assert (rng1.uniform() == rng2.uniform()) == match
 
 
-@pytest.mark.skipif(LooseVersion(np.__version__) >= "1.17",
+@pytest.mark.skipif(Version(np.__version__) >= Version("1.17"),
                     reason="Tests old numpy random functionality")
 def test_bad_seed_old():
 
diff --git a/seaborn/tests/test_categorical.py b/seaborn/tests/test_categorical.py
index 57ae714224..d4e09b703f 100644
--- a/seaborn/tests/test_categorical.py
+++ b/seaborn/tests/test_categorical.py
@@ -10,7 +10,6 @@
 import pytest
 from pytest import approx
 import numpy.testing as npt
-from distutils.version import LooseVersion
 from numpy.testing import (
     assert_array_equal,
     assert_array_less,
@@ -20,6 +19,7 @@
 from .. import palettes
 
 from .._core import categorical_order
+from ..external.version import Version
 from ..categorical import (
     _CategoricalPlotterNew,
     Beeswarm,
@@ -1638,7 +1638,7 @@ def test_color(self, long_df):
         self.func(data=long_df, x="a", y="y", facecolor="C4", ax=ax)
         assert self.get_last_color(ax) == to_rgba("C4")
 
-        if LooseVersion(mpl.__version__) >= "3.1.0":
+        if Version(mpl.__version__) >= Version("3.1.0"):
             # https://github.com/matplotlib/matplotlib/pull/12851
 
             ax = plt.figure().subplots()
@@ -1653,7 +1653,7 @@ def test_supplied_color_array(self, long_df):
 
         keys = ["c", "facecolor", "facecolors"]
 
-        if LooseVersion(mpl.__version__) >= "3.1.0":
+        if Version(mpl.__version__) >= Version("3.1.0"):
             # https://github.com/matplotlib/matplotlib/pull/12851
             keys.append("fc")
 
@@ -2054,7 +2054,7 @@ def test_log_scale(self):
         # (Even though visual output is ok -- so it's not an actual bug).
         # I'm not exactly sure why, so this version check is approximate
         # and should be revisited on a version bump.
-        if LooseVersion(mpl.__version__) < "3.1":
+        if Version(mpl.__version__) < Version("3.1"):
             pytest.xfail()
 
         ax = plt.figure().subplots()
@@ -3286,7 +3286,7 @@ def test_legend_titlesize(self, size):
         plt.close("all")
 
     @pytest.mark.skipif(
-        LooseVersion(pd.__version__) < "1.2",
+        Version(pd.__version__) < Version("1.2"),
         reason="Test requires pandas>=1.2")
     def test_Float64_input(self):
         data = pd.DataFrame(
diff --git a/seaborn/tests/test_distributions.py b/seaborn/tests/test_distributions.py
index 0f34f9cd35..d241fd978c 100644
--- a/seaborn/tests/test_distributions.py
+++ b/seaborn/tests/test_distributions.py
@@ -1,5 +1,4 @@
 import itertools
-from distutils.version import LooseVersion
 
 import numpy as np
 import matplotlib as mpl
@@ -31,6 +30,7 @@
     kdeplot,
     rugplot,
 )
+from ..external.version import Version
 from ..axisgrid import FacetGrid
 from .._testing import (
     assert_plots_equal,
@@ -1402,7 +1402,7 @@ def test_discrete_requires_bars(self, long_df):
             histplot(long_df, x="s", discrete=True, element="poly")
 
     @pytest.mark.skipif(
-        LooseVersion(np.__version__) < "1.17",
+        Version(np.__version__) < Version("1.17"),
         reason="Histogram over datetime64 requires numpy >= 1.17",
     )
     def test_datetime_scale(self, long_df):
diff --git a/seaborn/tests/test_regression.py b/seaborn/tests/test_regression.py
index 63768a521c..d7f7b982ab 100644
--- a/seaborn/tests/test_regression.py
+++ b/seaborn/tests/test_regression.py
@@ -1,4 +1,3 @@
-from distutils.version import LooseVersion
 import numpy as np
 import matplotlib as mpl
 import matplotlib.pyplot as plt
@@ -18,6 +17,7 @@
     _no_statsmodels = True
 
 from .. import regression as lm
+from ..external.version import Version
 from ..palettes import color_palette
 
 rs = np.random.RandomState(0)
@@ -597,7 +597,7 @@ def test_lmplot_scatter_kws(self):
         npt.assert_array_equal(red, red_scatter.get_facecolors()[0, :3])
         npt.assert_array_equal(blue, blue_scatter.get_facecolors()[0, :3])
 
-    @pytest.mark.skipif(LooseVersion(mpl.__version__) < "3.4",
+    @pytest.mark.skipif(Version(mpl.__version__) < Version("3.4"),
                         reason="MPL bug #15967")
     @pytest.mark.parametrize("sharex", [True, False])
     def test_lmplot_facet_truncate(self, sharex):
diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py
index a3bd5991be..d76f0ee26b 100644
--- a/seaborn/tests/test_relational.py
+++ b/seaborn/tests/test_relational.py
@@ -1,4 +1,3 @@
-from distutils.version import LooseVersion
 from itertools import product
 import numpy as np
 import matplotlib as mpl
@@ -8,6 +7,7 @@
 import pytest
 from numpy.testing import assert_array_equal
 
+from ..external.version import Version
 from ..palettes import color_palette
 
 from ..relational import (
@@ -1283,7 +1283,7 @@ def test_color(self, long_df):
         self.func(data=long_df, x="x", y="y", facecolors="C6", ax=ax)
         assert self.get_last_color(ax) == to_rgba("C6")
 
-        if LooseVersion(mpl.__version__) >= "3.1.0":
+        if Version(mpl.__version__) >= Version("3.1.0"):
             # https://github.com/matplotlib/matplotlib/pull/12851
 
             ax = plt.figure().subplots()
@@ -1604,7 +1604,7 @@ def test_supplied_color_array(self, long_df):
 
         keys = ["c", "facecolor", "facecolors"]
 
-        if LooseVersion(mpl.__version__) >= "3.1.0":
+        if Version(mpl.__version__) >= Version("3.1.0"):
             # https://github.com/matplotlib/matplotlib/pull/12851
             keys.append("fc")
 
diff --git a/seaborn/tests/test_utils.py b/seaborn/tests/test_utils.py
index 587a2dcfa4..586d49b813 100644
--- a/seaborn/tests/test_utils.py
+++ b/seaborn/tests/test_utils.py
@@ -18,9 +18,8 @@
     assert_frame_equal,
 )
 
-from distutils.version import LooseVersion
-
 from .. import utils, rcmod
+from ..external.version import Version
 from ..utils import (
     get_dataset_names,
     get_color_cycle,
@@ -325,14 +324,14 @@ def test_locator_to_legend_entries():
     locator = mpl.ticker.LogLocator(numticks=5)
     limits = (5, 1425)
     levels, str_levels = utils.locator_to_legend_entries(locator, limits, int)
-    if LooseVersion(mpl.__version__) >= "3.1":
+    if Version(mpl.__version__) >= Version("3.1"):
         assert str_levels == ['10', '100', '1000']
 
     limits = (0.00003, 0.02)
     levels, str_levels = utils.locator_to_legend_entries(
         locator, limits, float
     )
-    if LooseVersion(mpl.__version__) >= "3.1":
+    if Version(mpl.__version__) >= Version("3.1"):
         assert str_levels == ['1e-04', '1e-03', '1e-02']
 
 
diff --git a/seaborn/utils.py b/seaborn/utils.py
index 9d5f86ae6d..a50831e579 100644
--- a/seaborn/utils.py
+++ b/seaborn/utils.py
@@ -5,7 +5,6 @@
 import warnings
 import colorsys
 from urllib.request import urlopen, urlretrieve
-from distutils.version import LooseVersion
 
 import numpy as np
 import pandas as pd
@@ -14,6 +13,7 @@
 import matplotlib.pyplot as plt
 from matplotlib.cbook import normalize_kwargs
 
+from .external.version import Version
 
 __all__ = ["desaturate", "saturate", "set_hls_values", "move_legend",
            "despine", "get_dataset_names", "get_data_home", "load_dataset"]
@@ -149,7 +149,7 @@ def _default_color(method, hue, color, kws):
             isinstance(ax.xaxis.converter, mpl.dates.DateConverter),
             isinstance(ax.yaxis.converter, mpl.dates.DateConverter),
         ])
-        if LooseVersion(mpl.__version__) < "3.3" and datetime_axis:
+        if Version(mpl.__version__) < Version("3.3") and datetime_axis:
             return "C0"
 
         kws = _normalize_kwargs(kws, mpl.collections.PolyCollection)

From 21c81439c777a4d1acf8044ab116aefc825128a2 Mon Sep 17 00:00:00 2001
From: Danny Sepler 
Date: Thu, 9 Sep 2021 22:46:47 -0400
Subject: [PATCH 2/2] add packaging license

---
 licences/PACKAGING_LICENSE | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)
 create mode 100644 licences/PACKAGING_LICENSE

diff --git a/licences/PACKAGING_LICENSE b/licences/PACKAGING_LICENSE
new file mode 100644
index 0000000000..42ce7b75c9
--- /dev/null
+++ b/licences/PACKAGING_LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) Donald Stufft and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.