======================================================================== * license-file-metadata.patch ======================================================================== From 490a2b28fa2325f9929261aa2ee398fbb4c715dd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 3 Apr 2021 16:12:19 -0400 Subject: [PATCH 1/2] license_files - Add support for glob patterns + add default patterns https://github.com/pypa/setuptools/pull/2620 --- changelog.d/2620.breaking.rst | 4 ++ changelog.d/2620.change.rst | 1 + changelog.d/2620.deprecation.rst | 2 + changelog.d/2620.doc.rst | 1 + docs/references/keywords.rst | 11 +++++ docs/userguide/declarative_config.rst | 2 +- setuptools/command/sdist.py | 55 +++++++++++++--------- setuptools/tests/test_egg_info.py | 68 +++++++++++++++++++++++++-- setuptools/tests/test_manifest.py | 1 + 9 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 changelog.d/2620.breaking.rst create mode 100644 changelog.d/2620.change.rst create mode 100644 changelog.d/2620.deprecation.rst create mode 100644 changelog.d/2620.doc.rst diff --git a/changelog.d/2620.breaking.rst b/changelog.d/2620.breaking.rst new file mode 100644 index 00000000..431e7105 --- /dev/null +++ b/changelog.d/2620.breaking.rst @@ -0,0 +1,4 @@ +If neither ``license_file`` nor ``license_files`` is specified, the ``sdist`` +option will now auto-include files that match the following patterns: +``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS*``. +This matches the behavior of ``bdist_wheel``. -- by :user:`cdce8p` diff --git a/changelog.d/2620.change.rst b/changelog.d/2620.change.rst new file mode 100644 index 00000000..5470592d --- /dev/null +++ b/changelog.d/2620.change.rst @@ -0,0 +1 @@ +The ``license_file`` and ``license_files`` options now support glob patterns. -- by :user:`cdce8p` diff --git a/changelog.d/2620.deprecation.rst b/changelog.d/2620.deprecation.rst new file mode 100644 index 00000000..1af5f246 --- /dev/null +++ b/changelog.d/2620.deprecation.rst @@ -0,0 +1,2 @@ +The ``license_file`` option is now marked as deprecated. +Use ``license_files`` instead. -- by :user:`cdce8p` diff --git a/changelog.d/2620.doc.rst b/changelog.d/2620.doc.rst new file mode 100644 index 00000000..7564adac --- /dev/null +++ b/changelog.d/2620.doc.rst @@ -0,0 +1 @@ +Added documentation for the ``license_files`` option. -- by :user:`cdce8p` diff --git a/docs/references/keywords.rst b/docs/references/keywords.rst index 03ce9fa2..619b2d14 100644 --- a/docs/references/keywords.rst +++ b/docs/references/keywords.rst @@ -76,6 +76,17 @@ Keywords ``license`` A string specifying the license of the package. +``license_file`` + + .. warning:: + ``license_file`` is deprecated. Use ``license_files`` instead. + +``license_files`` + + A list of glob patterns for license related files that should be included. + If neither ``license_file`` nor ``license_files`` is specified, this option + defaults to ``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, and ``AUTHORS*``. + ``keywords`` A list of strings or a comma-separated string providing descriptive meta-data. See: `PEP 0314`_. diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst index bc66869b..1d2d66e2 100644 --- a/docs/userguide/declarative_config.rst +++ b/docs/userguide/declarative_config.rst @@ -184,7 +184,7 @@ maintainer_email maintainer-email str classifiers classifier file:, list-comma license str license_file str -license_files list-comma +license_files list-comma 42.0.0 description summary file:, str long_description long-description file:, str long_description_content_type str 38.6.0 diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 887b7efa..a6ea814a 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -4,6 +4,7 @@ import os import sys import io import contextlib +from glob import iglob from setuptools.extern import ordered_set @@ -194,29 +195,41 @@ class sdist(sdist_add_defaults, orig.sdist): """Checks if license_file' or 'license_files' is configured and adds any valid paths to 'self.filelist'. """ - - files = ordered_set.OrderedSet() - opts = self.distribution.get_option_dict('metadata') - # ignore the source of the value - _, license_file = opts.get('license_file', (None, None)) - - if license_file is None: - log.debug("'license_file' option was not specified") - else: - files.add(license_file) - + files = ordered_set.OrderedSet() try: - files.update(self.distribution.metadata.license_files) + license_files = self.distribution.metadata.license_files except TypeError: log.warn("warning: 'license_files' option is malformed") - - for f in files: - if not os.path.exists(f): - log.warn( - "warning: Failed to find the configured license file '%s'", - f) - files.remove(f) - - self.filelist.extend(files) + license_files = ordered_set.OrderedSet() + patterns = license_files if isinstance(license_files, ordered_set.OrderedSet) \ + else ordered_set.OrderedSet(license_files) + + if 'license_file' in opts: + log.warn( + "warning: the 'license_file' option is deprecated, " + "use 'license_files' instead") + patterns.append(opts['license_file'][1]) + + if 'license_file' not in opts and 'license_files' not in opts: + # Default patterns match the ones wheel uses + # See https://wheel.readthedocs.io/en/stable/user_guide.html + # -> 'Including license files in the generated wheel file' + patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + + for pattern in patterns: + for path in iglob(pattern): + if path.endswith('~'): + log.debug( + "ignoring license file '%s' as it looks like a backup", + path) + continue + + if path not in files and os.path.isfile(path): + log.info( + "adding license file '%s' (matched pattern '%s')", + path, pattern) + files.add(path) + + self.filelist.extend(sorted(files)) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 1047468b..c93ed020 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -533,7 +533,7 @@ class TestEggInfo: 'setup.cfg': DALS(""" """), 'LICENSE': "Test license" - }, False), # no license_file attribute + }, True), # no license_file attribute, LICENSE auto-included ({ 'setup.cfg': DALS(""" [metadata] @@ -541,7 +541,15 @@ class TestEggInfo: """), 'MANIFEST.in': "exclude LICENSE", 'LICENSE': "Test license" - }, False) # license file is manually excluded + }, False), # license file is manually excluded + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICEN[CS]E* + """), + 'LICENSE': "Test license", + }, True, + id="glob_pattern"), ]) def test_setup_cfg_license_file( self, tmpdir_cwd, env, files, license_in_sources): @@ -621,7 +629,7 @@ class TestEggInfo: 'setup.cfg': DALS(""" """), 'LICENSE': "Test license" - }, [], ['LICENSE']), # no license_files attribute + }, ['LICENSE'], []), # no license_files attribute, LICENSE auto-included ({ 'setup.cfg': DALS(""" [metadata] @@ -640,7 +648,36 @@ class TestEggInfo: 'MANIFEST.in': "exclude LICENSE-XYZ", 'LICENSE-ABC': "ABC license", 'LICENSE-XYZ': "XYZ license" - }, ['LICENSE-ABC'], ['LICENSE-XYZ']) # subset is manually excluded + }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # subset is manually excluded + pytest.param({ + 'setup.cfg': "", + 'LICENSE-ABC': "ABC license", + 'COPYING-ABC': "ABC copying", + 'NOTICE-ABC': "ABC notice", + 'AUTHORS-ABC': "ABC authors", + 'LICENCE-XYZ': "XYZ license", + 'LICENSE': "License", + 'INVALID-LICENSE': "Invalid license", + }, [ + 'LICENSE-ABC', + 'COPYING-ABC', + 'NOTICE-ABC', + 'AUTHORS-ABC', + 'LICENCE-XYZ', + 'LICENSE', + ], ['INVALID-LICENSE'], + # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + id="default_glob_patterns"), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE* + """), + 'LICENSE-ABC': "ABC license", + 'NOTICE-XYZ': "XYZ notice", + }, ['LICENSE-ABC'], ['NOTICE-XYZ'], + id="no_default_glob_patterns"), ]) def test_setup_cfg_license_files( self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): @@ -745,7 +782,28 @@ class TestEggInfo: 'LICENSE-PQR': "PQR license", 'LICENSE-XYZ': "XYZ license" # manually excluded - }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']) + }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE* + """), + 'LICENSE-ABC': "ABC license", + 'NOTICE-XYZ': "XYZ notice", + }, ['LICENSE-ABC'], ['NOTICE-XYZ'], + id="no_default_glob_patterns"), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE* + license_files = + NOTICE* + """), + 'LICENSE-ABC': "ABC license", + 'NOTICE-ABC': "ABC notice", + 'AUTHORS-ABC': "ABC authors", + }, ['LICENSE-ABC', 'NOTICE-ABC'], ['AUTHORS-ABC'], + id="combined_glob_patterrns"), ]) def test_setup_cfg_license_file_license_files( self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 82bdb9c6..589cefb2 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -55,6 +55,7 @@ def touch(filename): default_files = frozenset(map(make_local_path, [ 'README.rst', 'MANIFEST.in', + 'LICENSE', 'setup.py', 'app.egg-info/PKG-INFO', 'app.egg-info/SOURCES.txt', From e1aa3949d2b0d610f6d83bc3c85d96c5c4cabd3a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 22 May 2021 20:00:24 -0400 Subject: [PATCH 2/2] Add License-File field to package metadata https://github.com/pypa/setuptools/pull/2645 --- changelog.d/2645.breaking.rst | 3 ++ changelog.d/2645.change.rst | 4 +++ setuptools/command/egg_info.py | 9 +++++- setuptools/command/sdist.py | 46 -------------------------- setuptools/config.py | 5 +++ setuptools/dist.py | 54 ++++++++++++++++++++++++++++++- setuptools/tests/test_egg_info.py | 54 ++++++++++++++++++++++++++++--- setuptools/tests/test_manifest.py | 1 - 8 files changed, 122 insertions(+), 54 deletions(-) create mode 100644 changelog.d/2645.breaking.rst create mode 100644 changelog.d/2645.change.rst diff --git a/changelog.d/2645.breaking.rst b/changelog.d/2645.breaking.rst new file mode 100644 index 00000000..b96b492a --- /dev/null +++ b/changelog.d/2645.breaking.rst @@ -0,0 +1,3 @@ +License files excluded via the ``MANIFEST.in`` but matched by either +the ``license_file`` (deprecated) or ``license_files`` options, +will be nevertheless included in the source distribution. - by :user:`cdce8p` diff --git a/changelog.d/2645.change.rst b/changelog.d/2645.change.rst new file mode 100644 index 00000000..b22385c1 --- /dev/null +++ b/changelog.d/2645.change.rst @@ -0,0 +1,4 @@ +Added ``License-File`` (multiple) to the output package metadata. +The field will contain the path of a license file, matched by the +``license_file`` (deprecated) and ``license_files`` options, +relative to ``.dist-info``. - by :user:`cdce8p` diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 1f120b67..18b81340 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -541,6 +541,7 @@ class manifest_maker(sdist): self.add_defaults() if os.path.exists(self.template): self.read_template() + self.add_license_files() self.prune_file_list() self.filelist.sort() self.filelist.remove_duplicates() @@ -575,7 +576,6 @@ class manifest_maker(sdist): def add_defaults(self): sdist.add_defaults(self) - self.check_license() self.filelist.append(self.template) self.filelist.append(self.manifest) rcfiles = list(walk_revctrl()) @@ -592,6 +592,13 @@ class manifest_maker(sdist): ei_cmd = self.get_finalized_command('egg_info') self.filelist.graft(ei_cmd.egg_info) + def add_license_files(self): + license_files = self.distribution.metadata.license_files or [] + for lf in license_files: + log.info("adding license file '%s'", lf) + pass + self.filelist.extend(license_files) + def prune_file_list(self): build = self.get_finalized_command('build') base_dir = self.distribution.get_fullname() diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index a6ea814a..4a014283 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -4,9 +4,6 @@ import os import sys import io import contextlib -from glob import iglob - -from setuptools.extern import ordered_set from .py36compat import sdist_add_defaults @@ -190,46 +187,3 @@ class sdist(sdist_add_defaults, orig.sdist): continue self.filelist.append(line) manifest.close() - - def check_license(self): - """Checks if license_file' or 'license_files' is configured and adds any - valid paths to 'self.filelist'. - """ - opts = self.distribution.get_option_dict('metadata') - - files = ordered_set.OrderedSet() - try: - license_files = self.distribution.metadata.license_files - except TypeError: - log.warn("warning: 'license_files' option is malformed") - license_files = ordered_set.OrderedSet() - patterns = license_files if isinstance(license_files, ordered_set.OrderedSet) \ - else ordered_set.OrderedSet(license_files) - - if 'license_file' in opts: - log.warn( - "warning: the 'license_file' option is deprecated, " - "use 'license_files' instead") - patterns.append(opts['license_file'][1]) - - if 'license_file' not in opts and 'license_files' not in opts: - # Default patterns match the ones wheel uses - # See https://wheel.readthedocs.io/en/stable/user_guide.html - # -> 'Including license files in the generated wheel file' - patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') - - for pattern in patterns: - for path in iglob(pattern): - if path.endswith('~'): - log.debug( - "ignoring license file '%s' as it looks like a backup", - path) - continue - - if path not in files and os.path.isfile(path): - log.info( - "adding license file '%s' (matched pattern '%s')", - path, pattern) - files.add(path) - - self.filelist.extend(sorted(files)) diff --git a/setuptools/config.py b/setuptools/config.py index af3a3bcb..ece325e2 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -520,6 +520,11 @@ class ConfigMetadataHandler(ConfigHandler): 'obsoletes': parse_list, 'classifiers': self._get_parser_compound(parse_file, parse_list), 'license': exclude_files_parser('license'), + 'license_file': self._deprecated_config_handler( + exclude_files_parser('license_file'), + "The license_file parameter is deprecated, " + "use license_files instead.", + DeprecationWarning), 'license_files': parse_list, 'description': parse_file, 'long_description': parse_file, diff --git a/setuptools/dist.py b/setuptools/dist.py index 050388de..bc663e63 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -14,6 +14,7 @@ import distutils.dist from distutils.util import strtobool from distutils.debug import DEBUG from distutils.fancy_getopt import translate_longopt +from glob import iglob import itertools from collections import defaultdict @@ -117,6 +118,8 @@ def read_pkg_file(self, file): self.provides = None self.obsoletes = None + self.license_files = _read_list('license-file') + def single_line(val): # quick and dirty validation for description pypa/setuptools#1390 @@ -199,6 +202,7 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME for extra in self.provides_extras: write_field('Provides-Extra', extra) + self._write_list(file, 'License-File', self.license_files or []) sequence = tuple, list @@ -398,7 +402,8 @@ class Distribution(_Distribution): 'long_description_content_type': None, 'project_urls': dict, 'provides_extras': ordered_set.OrderedSet, - 'license_files': ordered_set.OrderedSet, + 'license_file': lambda: None, + 'license_files': lambda: None, } _patched_dist = None @@ -557,6 +562,34 @@ class Distribution(_Distribution): req.marker = None return req + def _finalize_license_files(self): + """Compute names of all license files which should be included.""" + license_files: Optional[List[str]] = self.metadata.license_files + patterns: List[str] = license_files if license_files else [] + + license_file: Optional[str] = self.metadata.license_file + if license_file and license_file not in patterns: + patterns.append(license_file) + + if license_files is None and license_file is None: + # Default patterns match the ones wheel uses + # See https://wheel.readthedocs.io/en/stable/user_guide.html + # -> 'Including license files in the generated wheel file' + patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + + self.metadata.license_files = list( + unique_everseen(self._expand_patterns(patterns))) + + @staticmethod + def _expand_patterns(patterns): + return ( + path + for pattern in patterns + for path in iglob(pattern) + if not path.endswith('~') + and os.path.isfile(path) + ) + # FIXME: 'Distribution._parse_config_files' is too complex (14) def _parse_config_files(self, filenames=None): # noqa: C901 """ @@ -680,6 +713,7 @@ class Distribution(_Distribution): parse_configuration(self, self.command_options, ignore_option_errors=ignore_option_errors) self._finalize_requires() + self._finalize_license_files() def fetch_build_eggs(self, requires): """Resolve pre-setup requirements""" @@ -1020,3 +1054,21 @@ class Distribution(_Distribution): class DistDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in dist in setuptools. Not ignored by default, unlike DeprecationWarning.""" + + +def unique_everseen(iterable, key=None): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + for element in itertools.filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index c93ed020..e8b49732 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -541,7 +541,7 @@ class TestEggInfo: """), 'MANIFEST.in': "exclude LICENSE", 'LICENSE': "Test license" - }, False), # license file is manually excluded + }, True), # manifest is overwritten by license_file pytest.param({ 'setup.cfg': DALS(""" [metadata] @@ -637,7 +637,7 @@ class TestEggInfo: """), 'MANIFEST.in': "exclude LICENSE", 'LICENSE': "Test license" - }, [], ['LICENSE']), # license file is manually excluded + }, ['LICENSE'], []), # manifest is overwritten by license_files ({ 'setup.cfg': DALS(""" [metadata] @@ -648,7 +648,8 @@ class TestEggInfo: 'MANIFEST.in': "exclude LICENSE-XYZ", 'LICENSE-ABC': "ABC license", 'LICENSE-XYZ': "XYZ license" - }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # subset is manually excluded + # manifest is overwritten by license_files + }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), pytest.param({ 'setup.cfg': "", 'LICENSE-ABC': "ABC license", @@ -678,6 +679,17 @@ class TestEggInfo: 'NOTICE-XYZ': "XYZ notice", }, ['LICENSE-ABC'], ['NOTICE-XYZ'], id="no_default_glob_patterns"), + pytest.param({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + LICENSE* + """), + 'LICENSE-ABC': "ABC license", + }, ['LICENSE-ABC'], [], + id="files_only_added_once", + ), ]) def test_setup_cfg_license_files( self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): @@ -781,8 +793,8 @@ class TestEggInfo: 'LICENSE-ABC': "ABC license", 'LICENSE-PQR': "PQR license", 'LICENSE-XYZ': "XYZ license" - # manually excluded - }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']), + # manifest is overwritten + }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), pytest.param({ 'setup.cfg': DALS(""" [metadata] @@ -825,6 +837,38 @@ class TestEggInfo: for lf in excl_licenses: assert sources_lines.count(lf) == 0 + def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): + """All matched license files should have a corresponding License-File.""" + self._create_project() + build_files({ + "setup.cfg": DALS(""" + [metadata] + license_files = + NOTICE* + LICENSE* + """), + "LICENSE-ABC": "ABC license", + "LICENSE-XYZ": "XYZ license", + "NOTICE": "included", + "IGNORE": "not include", + }) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + pkg_info_lines = pkginfo_file.read().split('\n') + license_file_lines = [ + line for line in pkg_info_lines if line.startswith('License-File:')] + + # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched + # Also assert that order from license_files is keeped + assert "License-File: NOTICE" == license_file_lines[0] + assert "License-File: LICENSE-ABC" in license_file_lines[1:] + assert "License-File: LICENSE-XYZ" in license_file_lines[1:] + def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to # the `setup` function results in writing a `Description-Content-Type` diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 589cefb2..82bdb9c6 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -55,7 +55,6 @@ def touch(filename): default_files = frozenset(map(make_local_path, [ 'README.rst', 'MANIFEST.in', - 'LICENSE', 'setup.py', 'app.egg-info/PKG-INFO', 'app.egg-info/SOURCES.txt', ======================================================================== * LICENSE ======================================================================== Copyright Jason R. Coombs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.