Skip to content

Commit

Permalink
Extract normalization functions from editable_wheel and dist_info int…
Browse files Browse the repository at this point in the history
…o own module
  • Loading branch information
abravalheri committed Jan 23, 2023
1 parent 6050634 commit ec238c4
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 62 deletions.
99 changes: 99 additions & 0 deletions setuptools/_normalization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import os
import re
import sys
import warnings
from inspect import cleandoc
from pathlib import Path
from typing import Union

from setuptools.extern import packaging

from ._deprecation_warning import SetuptoolsDeprecationWarning

_Path = Union[str, Path]

# https://packaging.python.org/en/latest/specifications/core-metadata/#name
_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I)
_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9.]+", re.I)


def path(filename: _Path) -> str:
"""Normalize a file/dir name for comparison purposes."""
# See pkg_resources.normalize_path
file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
return os.path.normcase(os.path.realpath(os.path.normpath(file)))


def safe_identifier(name: str) -> str:
"""Make a string safe to be used as Python identifier.
>>> safe_identifier("12abc")
'_12abc'
>>> safe_identifier("__editable__.myns.pkg-78.9.3_local")
'__editable___myns_pkg_78_9_3_local'
"""
safe = re.sub(r'\W|^(?=\d)', '_', name)
assert safe.isidentifier()
return safe


def safe_name(component: str) -> str:
"""Escape a component used as a project name according to Core Metadata.
>>> safe_name("hello world")
'hello-world'
>>> safe_name("hello?world")
'hello-world'
"""
# See pkg_resources.safe_name
return _UNSAFE_NAME_CHARS.sub("-", component)


def safe_version(version: str) -> str:
"""Convert an arbitrary string into a valid version string.
>>> safe_version("1988 12 25")
'1988.12.25'
>>> safe_version("v0.2.1")
'0.2.1'
>>> safe_version("v0.2?beta")
'0.2b0'
>>> safe_version("v0.2 beta")
'0.2b0'
>>> safe_version("ubuntu lts")
Traceback (most recent call last):
...
setuptools.extern.packaging.version.InvalidVersion: Invalid version: 'ubuntu.lts'
"""
v = version.replace(' ', '.')
try:
return str(packaging.version.Version(v))
except packaging.version.InvalidVersion:
attempt = _UNSAFE_NAME_CHARS.sub("-", v)
return str(packaging.version.Version(attempt))


def best_effort_version(version: str) -> str:
"""Convert an arbitrary string into a version-like string.
>>> best_effort_version("v0.2 beta")
'0.2b0'
>>> import warnings
>>> warnings.simplefilter("ignore", category=SetuptoolsDeprecationWarning)
>>> best_effort_version("ubuntu lts")
'ubuntu.lts'
"""
try:
return safe_version(version)
except packaging.version.InvalidVersion:
msg = f"""Invalid version: {version!r}.
!!\n\n
###################
# Invalid version #
###################
{version!r} is not valid according to PEP 440.\n
Please make sure specify a valid version for your package.
Also note that future releases of setuptools may halt the build process
if an invalid version is given.
\n\n!!
"""
warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning)
v = version.replace(' ', '.')
return safe_name(v).strip("_")
40 changes: 6 additions & 34 deletions setuptools/command/dist_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@
"""

import os
import re
import shutil
import sys
import warnings
from contextlib import contextmanager
from inspect import cleandoc
from distutils import log
from distutils.core import Command
from pathlib import Path

from distutils.core import Command
from distutils import log
from setuptools.extern import packaging
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
from .. import _normalization
from .._deprecation_warning import SetuptoolsDeprecationWarning


class dist_info(Command):
Expand Down Expand Up @@ -72,8 +70,8 @@ def finalize_options(self):
egg_info.finalize_options()
self.egg_info = egg_info

name = _safe(dist.get_name())
version = _version(dist.get_version())
name = _normalization.safe_name(dist.get_name()).replace(".", "_")
version = _normalization.best_effort_version(dist.get_version())
self.name = f"{name}-{version}"
self.dist_info_dir = os.path.join(self.output_dir, f"{self.name}.dist-info")

Expand Down Expand Up @@ -105,32 +103,6 @@ def run(self):
bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir)


def _safe(component: str) -> str:
"""Escape a component used to form a wheel name according to PEP 491"""
return re.sub(r"[^\w\d.]+", "_", component)


def _version(version: str) -> str:
"""Convert an arbitrary string to a version string."""
v = version.replace(' ', '.')
try:
return str(packaging.version.Version(v)).replace("-", "_")
except packaging.version.InvalidVersion:
msg = f"""Invalid version: {version!r}.
!!\n\n
###################
# Invalid version #
###################
{version!r} is not valid according to PEP 440.\n
Please make sure specify a valid version for your package.
Also note that future releases of setuptools may halt the build process
if an invalid version is given.
\n\n!!
"""
warnings.warn(cleandoc(msg))
return _safe(v).strip("_")


def _rm(dir_name, **opts):
if os.path.isdir(dir_name):
shutil.rmtree(dir_name, **opts)
Expand Down
36 changes: 8 additions & 28 deletions setuptools/command/editable_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import logging
import os
import re
import shutil
import sys
import traceback
Expand All @@ -36,10 +35,10 @@
Union,
)

from setuptools import Command, SetuptoolsDeprecationWarning, errors, namespaces
from setuptools.command.build_py import build_py as build_py_cls
from setuptools.discovery import find_package_path
from setuptools.dist import Distribution
from .. import Command, SetuptoolsDeprecationWarning, errors, namespaces, _normalization
from ..discovery import find_package_path
from ..dist import Distribution
from .build_py import build_py as build_py_cls

if TYPE_CHECKING:
from wheel.wheelfile import WheelFile # noqa
Expand Down Expand Up @@ -490,7 +489,7 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]
))

name = f"__editable__.{self.name}.finder"
finder = _make_identifier(name)
finder = _normalization.safe_identifier(name)
content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
wheel.writestr(f"{finder}.py", content)

Expand Down Expand Up @@ -569,7 +568,7 @@ def _simple_layout(
return set(package_dir) in ({}, {""})
parent = os.path.commonpath([_parent_path(k, v) for k, v in layout.items()])
return all(
_normalize_path(Path(parent, *key.split('.'))) == _normalize_path(value)
_normalization.path(Path(parent, *key.split('.'))) == _normalization.path(value)
for key, value in layout.items()
)

Expand Down Expand Up @@ -698,40 +697,21 @@ def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:
>>> _is_nested("b.a", "path/b/a", "a", "path/a")
False
"""
norm_pkg_path = _normalize_path(pkg_path)
norm_pkg_path = _normalization.path(pkg_path)
rest = pkg.replace(parent, "", 1).strip(".").split(".")
return (
pkg.startswith(parent)
and norm_pkg_path == _normalize_path(Path(parent_path, *rest))
and norm_pkg_path == _normalization.path(Path(parent_path, *rest))
)


def _normalize_path(filename: _Path) -> str:
"""Normalize a file/dir name for comparison purposes"""
# See pkg_resources.normalize_path
file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
return os.path.normcase(os.path.realpath(os.path.normpath(file)))


def _empty_dir(dir_: _P) -> _P:
"""Create a directory ensured to be empty. Existing files may be removed."""
shutil.rmtree(dir_, ignore_errors=True)
os.makedirs(dir_)
return dir_


def _make_identifier(name: str) -> str:
"""Make a string safe to be used as Python identifier.
>>> _make_identifier("12abc")
'_12abc'
>>> _make_identifier("__editable__.myns.pkg-78.9.3_local")
'__editable___myns_pkg_78_9_3_local'
"""
safe = re.sub(r'\W|^(?=\d)', '_', name)
assert safe.isidentifier()
return safe


class _NamespaceInstaller(namespaces.Installer):
def __init__(self, distribution, installation_dir, editable_name, src_root):
self.distribution = distribution
Expand Down

0 comments on commit ec238c4

Please sign in to comment.