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 all 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
145 changes: 46 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 @@
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 @@
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 @@
self.help = 0
self.finalized = 0

def initialize_options(self):
pass

Check warning on line 139 in babel/messages/frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/frontend.py#L139

Added line #L139 was not covered by tests

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(

Check warning on line 147 in babel/messages/frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/frontend.py#L147

Added line #L147 was not covered by tests
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 @@
"""
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``::

from babel.messages.frontend import extract_messages
return cli_directory_filter

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 @@
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 @@
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 @@
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 @@
}

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 @@
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 @@

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 @@
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,16 @@
return keywords


def __getattr__(name: str):
# Re-exports for backwards compatibility;
# `setuptools_frontend` is the canonical import location.
if name in {'check_message_extractors', 'compile_catalog', 'extract_messages', 'init_catalog', 'update_catalog'}:
from babel.messages import setuptools_frontend

Check warning on line 1116 in babel/messages/frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/frontend.py#L1116

Added line #L1116 was not covered by tests

return getattr(setuptools_frontend, name)

Check warning on line 1118 in babel/messages/frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/frontend.py#L1118

Added line #L1118 was not covered by tests

raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


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

Check warning on line 12 in babel/messages/setuptools_frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/setuptools_frontend.py#L11-L12

Added lines #L11 - L12 were not covered by tests

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

Check warning on line 16 in babel/messages/setuptools_frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/setuptools_frontend.py#L14-L16

Added lines #L14 - L16 were not covered by tests


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(

Check warning on line 30 in babel/messages/setuptools_frontend.py

View check run for this annotation

Codecov / codecov/patch

babel/messages/setuptools_frontend.py#L28-L30

Added lines #L28 - L30 were not covered by tests
'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