def debian_version(self): """ The version of the Debian package (a string). Reformats :attr:`python_version` using :func:`.normalize_package_version()`. """ return normalize_package_version(self.python_version)
def debian_version(self): """ The version of the Debian package (a string). Reformats the version of the Python package using :py:func:`.normalize_package_version()`. """ return normalize_package_version(self.requirement.version)
def debian_version(self): """ The version of the Debian package (a string). Reformats :attr:`python_version` using :func:`.normalize_package_version()`. """ return normalize_package_version( self.python_version, prerelease_workaround=self.converter.prerelease_workaround)
def test_version_reformatting(self): """Test reformatting of Python version strings.""" assert normalize_package_version('1.5_42') == '1.5-42' assert normalize_package_version('1.5-whatever') == '1.5-whatever-1' # PEP 440 pre-release versions (specific handling added in release 1.0). assert normalize_package_version('1.0a2') == '1.0~a2' assert normalize_package_version('1.0b2') == '1.0~b2' assert normalize_package_version('1.0c2') == '1.0~rc2' assert normalize_package_version('1.0rc2') == '1.0~rc2' # New versus old behavior (the option to control backwards compatibility was added in release 2.1). assert normalize_package_version( '1.0a2', prerelease_workaround=True) == '1.0~a2' assert normalize_package_version( '1.0a2', prerelease_workaround=False) == '1.0a2'
def test_version_reformatting(self): """Test reformatting of Python version strings.""" assert normalize_package_version('1.5_42') == '1.5-42' assert normalize_package_version('1.5-whatever') == '1.5-whatever-1' # PEP 440 pre-release versions. assert normalize_package_version('1.0a2') == '1.0~a2' assert normalize_package_version('1.0b2') == '1.0~b2' assert normalize_package_version('1.0c2') == '1.0~rc2' assert normalize_package_version('1.0rc2') == '1.0~rc2'
def debian_dependencies(self): """ Find Debian dependencies of Python package. Converts `Python version specifiers`_ to `Debian package relationships`_. :returns: A list with Debian package relationships (strings) in the format of the ``Depends:`` line of a Debian package ``control`` file. Based on :py:data:`python_requirements`. .. _Python version specifiers: http://www.python.org/dev/peps/pep-0440/#version-specifiers .. _Debian package relationships: https://www.debian.org/doc/debian-policy/ch-relationships.html """ dependencies = set() for requirement in self.python_requirements: debian_package_name = self.converter.transform_name(requirement.project_name, *requirement.extras) if requirement.specs: for constraint, version in requirement.specs: version = normalize_package_version(version) if version == 'dev': # Requirements like 'pytz > dev' (celery==3.1.16) don't # seem to really mean anything to pip (based on my # reading of the 1.4.x source code) but Debian will # definitely complain because version strings should # start with a digit. In this case we'll just fall # back to a dependency without a version specification # so we don't drop the dependency. dependencies.add(debian_package_name) elif constraint == '==': dependencies.add('%s (= %s)' % (debian_package_name, version)) elif constraint == '!=': values = (debian_package_name, version, debian_package_name, version) dependencies.add('%s (<< %s) | %s (>> %s)' % values) elif constraint == '<': dependencies.add('%s (<< %s)' % (debian_package_name, version)) elif constraint == '>': dependencies.add('%s (>> %s)' % (debian_package_name, version)) elif constraint in ('<=', '>='): dependencies.add('%s (%s %s)' % (debian_package_name, constraint, version)) else: msg = "Conversion specifier not supported! (%r used by Python package %s)" raise Exception(msg % (constraint, self.python_name)) else: dependencies.add(debian_package_name) dependencies = sorted(dependencies) logger.debug("Debian dependencies of %s: %r", self, dependencies) return dependencies
def transform_version(self, package_to_convert, python_requirement_name, python_requirement_version): """ Transform a Python requirement version to a Debian version number. :param package_to_convert: The :class:`.PackageToConvert` whose requirement is being transformed. :param python_requirement_name: The name of a Python package as found on PyPI (a string). :param python_requirement_version: The required version of the Python package (a string). :returns: The transformed version (a string). This method is a wrapper for :func:`.normalize_package_version()` that takes care of one additional quirk to ensure compatibility with :pypi:`pip`. Explaining this quirk requires a bit of context: - When package A requires package B (via ``install_requires``) and package A absolutely pins the required version of package B using one or more trailing zeros (e.g. ``B==1.0.0``) but the actual version number of package B (embedded in the metadata of package B) contains less trailing zeros (e.g. ``1.0``) then :pypi:`pip` will not complain but silently fetch version ``1.0`` of package B to satisfy the requirement. - However this doesn't change the absolutely pinned version in the ``install_requires`` metadata of package A. - When py2deb converts the resulting requirement set, the dependency of package A is converted as ``B (= 1.0.0)``. The resulting packages will not be installable because :man:`apt` considers ``1.0`` to be different from ``1.0.0``. This method analyzes the requirement set to identify occurrences of this quirk and strip trailing zeros in ``install_requires`` metadata that would otherwise result in converted packages that cannot be installed. """ matching_packages = [ pkg for pkg in self.packages_to_convert if package_names_match(pkg.python_name, python_requirement_name) ] if len(matching_packages) > 1: # My assumption while writing this code is that this should never # happen. This check is to make sure that if it does happen it will # be noticed because the last thing I want is for this `hack' to # result in packages that are silently wrongly converted. normalized_name = normalize_package_name(python_requirement_name) num_matches = len(matching_packages) raise Exception(compact(""" Expected requirement set to contain exactly one Python package whose name can be normalized to {name} but encountered {count} packages instead! (matching packages: {matches}) """, name=normalized_name, count=num_matches, matches=matching_packages)) elif matching_packages: # Check whether the version number included in the requirement set # matches the version number in a package's requirements. requirement_to_convert = matching_packages[0] if python_requirement_version != requirement_to_convert.python_version: logger.debug("Checking whether to strip trailing zeros from required version ..") # Check whether the version numbers share the same prefix. required_version = tokenize_version(python_requirement_version) included_version = tokenize_version(requirement_to_convert.python_version) common_length = min(len(required_version), len(included_version)) required_prefix = required_version[:common_length] included_prefix = included_version[:common_length] prefixes_match = (required_prefix == included_prefix) logger.debug("Prefix of required version: %s", required_prefix) logger.debug("Prefix of included version: %s", included_prefix) logger.debug("Prefixes match? %s", prefixes_match) # Check if 1) only the required version has a suffix and 2) this # suffix consists only of trailing zeros. required_suffix = required_version[common_length:] included_suffix = included_version[common_length:] logger.debug("Suffix of required version: %s", required_suffix) logger.debug("Suffix of included version: %s", included_suffix) if prefixes_match and required_suffix and not included_suffix: # Check whether the suffix of the required version contains # only zeros, i.e. pip considers the version numbers the same # although apt would not agree. if all(re.match('^0+$', t) for t in required_suffix if t.isdigit()): modified_version = ''.join(required_prefix) logger.warning("Stripping superfluous trailing zeros from required" " version of %s required by %s! (%s -> %s)", python_requirement_name, package_to_convert.python_name, python_requirement_version, modified_version) python_requirement_version = modified_version return normalize_package_version(python_requirement_version, prerelease_workaround=self.prerelease_workaround)
def test_version_reformatting(self): """ Test reformatting of Python version strings. """ assert normalize_package_version('1.5_42') == '1.5-42' assert normalize_package_version('1.5-whatever') == '1.5-whatever-1'
def transform_version(self, package_to_convert, python_requirement_name, python_requirement_version): """ Transform a Python requirement version to a Debian version number. :param package_to_convert: The :class:`.PackageToConvert` whose requirement is being transformed. :param python_requirement_name: The name of a Python package as found on PyPI (a string). :param python_requirement_version: The required version of the Python package (a string). :returns: The transformed version (a string). This method is a wrapper for :func:`.normalize_package_version()` that takes care of one additional quirk to ensure compatibility with pip. Explaining this quirk requires a bit of context: - When package A requires package B (via ``install_requires``) and package A absolutely pins the required version of package B using one or more trailing zeros (e.g. ``B==1.0.0``) but the actual version number of package B (embedded in the metadata of package B) contains less trailing zeros (e.g. ``1.0``) then pip will not complain but silently fetch version ``1.0`` of package B to satisfy the requirement. - However this doesn't change the absolutely pinned version in the ``install_requires`` metadata of package A. - When py2deb converts the resulting requirement set, the dependency of package A is converted as ``B (= 1.0.0)``. The resulting packages will not be installable because ``apt`` considers ``1.0`` to be different from ``1.0.0``. This method analyzes the requirement set to identify occurrences of this quirk and strip trailing zeros in ``install_requires`` metadata that would otherwise result in converted packages that cannot be installed. """ matching_packages = [p for p in self.packages_to_convert if package_names_match(p.python_name, python_requirement_name)] if len(matching_packages) != 1: # My assumption while writing this code is that this should never # happen. This check is to make sure that if it does happen it will # be noticed because the last thing I want is for this `hack' to # result in packages that are silently wrongly converted. normalized_name = normalize_package_name(python_requirement_name) num_matches = len(matching_packages) raise Exception(compact(""" Expected requirement set to contain exactly one Python package whose name can be normalized to {name} but encountered {count} packages instead! (matching packages: {matches}) """, name=normalized_name, count=num_matches, matches=matching_packages)) # Check whether the version number included in the requirement set # matches the version number in a package's requirements. requirement_to_convert = matching_packages[0] if python_requirement_version != requirement_to_convert.python_version: logger.debug("Checking whether to strip trailing zeros from required version ..") # Check whether the version numbers share the same prefix. required_version = tokenize_version(python_requirement_version) included_version = tokenize_version(requirement_to_convert.python_version) common_length = min(len(required_version), len(included_version)) required_prefix = required_version[:common_length] included_prefix = included_version[:common_length] prefixes_match = (required_prefix == included_prefix) logger.debug("Prefix of required version: %s", required_prefix) logger.debug("Prefix of included version: %s", included_prefix) logger.debug("Prefixes match? %s", prefixes_match) # Check if 1) only the required version has a suffix and 2) this # suffix consists only of trailing zeros. required_suffix = required_version[common_length:] included_suffix = included_version[common_length:] logger.debug("Suffix of required version: %s", required_suffix) logger.debug("Suffix of included version: %s", included_suffix) if prefixes_match and required_suffix and not included_suffix: # Check whether the suffix of the required version contains # only zeros, i.e. pip considers the version numbers the same # although apt would not agree. if all(re.match('^0+$', t) for t in required_suffix if t.isdigit()): modified_version = ''.join(required_prefix) logger.warning("Stripping superfluous trailing zeros from required" " version of %s required by %s! (%s -> %s)", python_requirement_name, package_to_convert.python_name, python_requirement_version, modified_version) python_requirement_version = modified_version return normalize_package_version(python_requirement_version)