Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple pybabel frontend from distutils/setuptools; remove dependency #1041

Merged
merged 3 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ repos:
exclude: (tests/messages/data/)
- id: name-tests-test
args: [ '--django' ]
exclude: (tests/messages/data/)
exclude: (tests/messages/data/|.*(consts|utils).py)
- id: requirements-txt-fixer
- id: trailing-whitespace
148 changes: 49 additions & 99 deletions babel/messages/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,17 @@

log = logging.getLogger('babel')

try:
# See: https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html
from setuptools import Command as _Command
distutils_log = log # "distutils.log → (no replacement yet)"

try:
from setuptools.errors import BaseError, OptionError, SetupError
except ImportError: # Error aliases only added in setuptools 59 (2021-11).
OptionError = SetupError = BaseError = Exception
class BaseError(Exception):
pass

except ImportError:
from distutils import log as distutils_log
from distutils.cmd import Command as _Command
from distutils.errors import DistutilsError as BaseError
from distutils.errors import DistutilsOptionError as OptionError
from distutils.errors import DistutilsSetupError as SetupError

class OptionError(BaseError):
pass


class SetupError(BaseError):
pass


def listify_value(arg, split=None):
Expand Down Expand Up @@ -100,7 +95,7 @@ def listify_value(arg, split=None):
return out


class Command(_Command):
class CommandMixin:
# This class is a small shim between Distutils commands and
# optparse option parsing in the frontend command line.

Expand Down Expand Up @@ -128,7 +123,7 @@ class Command(_Command):
option_choices = {}

#: Log object. To allow replacement in the script command line runner.
log = distutils_log
log = log

def __init__(self, dist=None):
# A less strict version of distutils' `__init__`.
Expand All @@ -140,24 +135,21 @@ def __init__(self, dist=None):
self.help = 0
self.finalized = 0

def initialize_options(self):
pass

class compile_catalog(Command):
"""Catalog compilation command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.frontend import compile_catalog
def ensure_finalized(self):
if not self.finalized:
self.finalize_options()
self.finalized = 1

setup(
...
cmdclass = {'compile_catalog': compile_catalog}
def finalize_options(self):
raise RuntimeError(
f"abstract method -- subclass {self.__class__} must override",
)

.. versionadded:: 0.9
"""

class CompileCatalog(CommandMixin):
description = 'compile message catalogs to binary MO files'
user_options = [
('domain=', 'D',
Expand Down Expand Up @@ -280,31 +272,19 @@ def _make_directory_filter(ignore_patterns):
"""
Build a directory_filter function based on a list of ignore patterns.
"""

def cli_directory_filter(dirname):
basename = os.path.basename(dirname)
return not any(
fnmatch.fnmatch(basename, ignore_pattern)
for ignore_pattern
in ignore_patterns
)
return cli_directory_filter


class extract_messages(Command):
"""Message extraction command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::
return cli_directory_filter

from babel.messages.frontend import extract_messages

setup(
...
cmdclass = {'extract_messages': extract_messages}
)
"""

class ExtractMessages(CommandMixin):
description = 'extract localizable strings from the project code'
user_options = [
('charset=', None,
Expand Down Expand Up @@ -497,6 +477,7 @@ def callback(filename: str, method: str, options: dict):
opt_values = ", ".join(f'{k}="{v}"' for k, v in options.items())
optstr = f" ({opt_values})"
self.log.info('extracting messages from %s%s', filepath, optstr)

return callback

def run(self):
Expand Down Expand Up @@ -572,38 +553,7 @@ def _get_mappings(self):
return mappings


def check_message_extractors(dist, name, value):
"""Validate the ``message_extractors`` keyword argument to ``setup()``.

:param dist: the distutils/setuptools ``Distribution`` object
:param name: the name of the keyword argument (should always be
"message_extractors")
:param value: the value of the keyword argument
:raise `DistutilsSetupError`: if the value is not valid
"""
assert name == 'message_extractors'
if not isinstance(value, dict):
raise SetupError(
'the value of the "message_extractors" '
'parameter must be a dictionary'
)


class init_catalog(Command):
"""New catalog initialization command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.frontend import init_catalog

setup(
...
cmdclass = {'init_catalog': init_catalog}
)
"""

class InitCatalog(CommandMixin):
description = 'create a new catalog based on a POT file'
user_options = [
('domain=', 'D',
Expand Down Expand Up @@ -678,23 +628,7 @@ def run(self):
write_po(outfile, catalog, width=self.width)


class update_catalog(Command):
"""Catalog merging command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.frontend import update_catalog

setup(
...
cmdclass = {'update_catalog': update_catalog}
)

.. versionadded:: 0.9
"""

class UpdateCatalog(CommandMixin):
description = 'update message catalogs from a POT file'
user_options = [
('domain=', 'D',
Expand Down Expand Up @@ -911,10 +845,10 @@ class CommandLineInterface:
}

command_classes = {
'compile': compile_catalog,
'extract': extract_messages,
'init': init_catalog,
'update': update_catalog,
'compile': CompileCatalog,
'extract': ExtractMessages,
'init': InitCatalog,
'update': UpdateCatalog,
}

log = None # Replaced on instance level
Expand Down Expand Up @@ -996,7 +930,7 @@ def _configure_command(self, cmdname, argv):
cmdinst = cmdclass()
if self.log:
cmdinst.log = self.log # Use our logger, not distutils'.
assert isinstance(cmdinst, Command)
assert isinstance(cmdinst, CommandMixin)
cmdinst.initialize_options()

parser = optparse.OptionParser(
Expand Down Expand Up @@ -1113,7 +1047,8 @@ def parse_mapping(fileobj, filename=None):

return method_map, options_map

def _parse_spec(s: str) -> tuple[int | None, tuple[int|tuple[int, str], ...]]:

def _parse_spec(s: str) -> tuple[int | None, tuple[int | tuple[int, str], ...]]:
inds = []
number = None
for x in s.split(','):
Expand All @@ -1125,6 +1060,7 @@ def _parse_spec(s: str) -> tuple[int | None, tuple[int|tuple[int, str], ...]]:
inds.append(int(x))
return number, tuple(inds)


def parse_keywords(strings: Iterable[str] = ()):
"""Parse keywords specifications from the given list of strings.

Expand Down Expand Up @@ -1173,5 +1109,19 @@ def parse_keywords(strings: Iterable[str] = ()):
return keywords


try:
# Re-exports for backwards compatibility;
# `setuptools_frontend` is the canonical import location.
from babel.messages.setuptools_frontend import (
check_message_extractors, # noqa: F401
compile_catalog, # noqa: F401
extract_messages, # noqa: F401
init_catalog, # noqa: F401
update_catalog, # noqa: F401
)
except ImportError:
# We expect this to mean that neither setuptools or distutils are installed.
pass
akx marked this conversation as resolved.
Show resolved Hide resolved

if __name__ == '__main__':
main()
108 changes: 108 additions & 0 deletions babel/messages/setuptools_frontend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from __future__ import annotations

from babel.messages import frontend

try:
# See: https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html
from setuptools import Command

try:
from setuptools.errors import BaseError, OptionError, SetupError
except ImportError: # Error aliases only added in setuptools 59 (2021-11).
OptionError = SetupError = BaseError = Exception

except ImportError:
from distutils.cmd import Command
from distutils.errors import DistutilsSetupError as SetupError


def check_message_extractors(dist, name, value):
"""Validate the ``message_extractors`` keyword argument to ``setup()``.

:param dist: the distutils/setuptools ``Distribution`` object
:param name: the name of the keyword argument (should always be
"message_extractors")
:param value: the value of the keyword argument
:raise `DistutilsSetupError`: if the value is not valid
"""
assert name == "message_extractors"
if not isinstance(value, dict):
raise SetupError(
'the value of the "message_extractors" parameter must be a dictionary'
)


class compile_catalog(frontend.CompileCatalog, Command):
"""Catalog compilation command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.setuptools_frontend import compile_catalog

setup(
...
cmdclass = {'compile_catalog': compile_catalog}
)

.. versionadded:: 0.9
"""


class extract_messages(frontend.ExtractMessages, Command):
"""Message extraction command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.setuptools_frontend import extract_messages

setup(
...
cmdclass = {'extract_messages': extract_messages}
)
"""


class init_catalog(frontend.InitCatalog, Command):
"""New catalog initialization command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.setuptools_frontend import init_catalog

setup(
...
cmdclass = {'init_catalog': init_catalog}
)
"""


class update_catalog(frontend.UpdateCatalog, Command):
"""Catalog merging command for use in ``setup.py`` scripts.

If correctly installed, this command is available to Setuptools-using
setup scripts automatically. For projects using plain old ``distutils``,
the command needs to be registered explicitly in ``setup.py``::

from babel.messages.setuptools_frontend import update_catalog

setup(
...
cmdclass = {'update_catalog': update_catalog}
)

.. versionadded:: 0.9
"""


COMMANDS = {
"compile_catalog": compile_catalog,
"extract_messages": extract_messages,
"init_catalog": init_catalog,
"update_catalog": update_catalog,
}
6 changes: 5 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from _pytest.doctest import DoctestModule

collect_ignore = ['tests/messages/data', 'setup.py']
collect_ignore = [
'babel/messages/setuptools_frontend.py',
'setup.py',
'tests/messages/data',
]
babel_path = Path(__file__).parent / 'babel'


Expand Down
Loading