Skip to content

Commit bf7b2ca

Browse files
akxoprypin
andauthored
Decouple pybabel frontend from distutils/setuptools; remove dependency (#1041)
* Decouple `pybabel` frontend from distutils/setuptools; remove dependency * Add tox configuration for testing with setuptools too * Use `__getattr__` for re-export Fixes #1040 Co-authored-by: Oleh Prypin <oleh@pryp.in>
1 parent a6c52b3 commit bf7b2ca

File tree

9 files changed

+322
-178
lines changed

9 files changed

+322
-178
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ repos:
1919
exclude: (tests/messages/data/)
2020
- id: name-tests-test
2121
args: [ '--django' ]
22-
exclude: (tests/messages/data/)
22+
exclude: (tests/messages/data/|.*(consts|utils).py)
2323
- id: requirements-txt-fixer
2424
- id: trailing-whitespace

babel/messages/frontend.py

Lines changed: 46 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,17 @@
4040

4141
log = logging.getLogger('babel')
4242

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

48-
try:
49-
from setuptools.errors import BaseError, OptionError, SetupError
50-
except ImportError: # Error aliases only added in setuptools 59 (2021-11).
51-
OptionError = SetupError = BaseError = Exception
44+
class BaseError(Exception):
45+
pass
5246

53-
except ImportError:
54-
from distutils import log as distutils_log
55-
from distutils.cmd import Command as _Command
56-
from distutils.errors import DistutilsError as BaseError
57-
from distutils.errors import DistutilsOptionError as OptionError
58-
from distutils.errors import DistutilsSetupError as SetupError
47+
48+
class OptionError(BaseError):
49+
pass
50+
51+
52+
class SetupError(BaseError):
53+
pass
5954

6055

6156
def listify_value(arg, split=None):
@@ -100,7 +95,7 @@ def listify_value(arg, split=None):
10095
return out
10196

10297

103-
class Command(_Command):
98+
class CommandMixin:
10499
# This class is a small shim between Distutils commands and
105100
# optparse option parsing in the frontend command line.
106101

@@ -128,7 +123,7 @@ class Command(_Command):
128123
option_choices = {}
129124

130125
#: Log object. To allow replacement in the script command line runner.
131-
log = distutils_log
126+
log = log
132127

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

138+
def initialize_options(self):
139+
pass
143140

144-
class compile_catalog(Command):
145-
"""Catalog compilation command for use in ``setup.py`` scripts.
146-
147-
If correctly installed, this command is available to Setuptools-using
148-
setup scripts automatically. For projects using plain old ``distutils``,
149-
the command needs to be registered explicitly in ``setup.py``::
150-
151-
from babel.messages.frontend import compile_catalog
141+
def ensure_finalized(self):
142+
if not self.finalized:
143+
self.finalize_options()
144+
self.finalized = 1
152145

153-
setup(
154-
...
155-
cmdclass = {'compile_catalog': compile_catalog}
146+
def finalize_options(self):
147+
raise RuntimeError(
148+
f"abstract method -- subclass {self.__class__} must override",
156149
)
157150

158-
.. versionadded:: 0.9
159-
"""
160151

152+
class CompileCatalog(CommandMixin):
161153
description = 'compile message catalogs to binary MO files'
162154
user_options = [
163155
('domain=', 'D',
@@ -280,31 +272,19 @@ def _make_directory_filter(ignore_patterns):
280272
"""
281273
Build a directory_filter function based on a list of ignore patterns.
282274
"""
275+
283276
def cli_directory_filter(dirname):
284277
basename = os.path.basename(dirname)
285278
return not any(
286279
fnmatch.fnmatch(basename, ignore_pattern)
287280
for ignore_pattern
288281
in ignore_patterns
289282
)
290-
return cli_directory_filter
291-
292283

293-
class extract_messages(Command):
294-
"""Message extraction command for use in ``setup.py`` scripts.
295-
296-
If correctly installed, this command is available to Setuptools-using
297-
setup scripts automatically. For projects using plain old ``distutils``,
298-
the command needs to be registered explicitly in ``setup.py``::
299-
300-
from babel.messages.frontend import extract_messages
284+
return cli_directory_filter
301285

302-
setup(
303-
...
304-
cmdclass = {'extract_messages': extract_messages}
305-
)
306-
"""
307286

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

502483
def run(self):
@@ -572,38 +553,7 @@ def _get_mappings(self):
572553
return mappings
573554

574555

575-
def check_message_extractors(dist, name, value):
576-
"""Validate the ``message_extractors`` keyword argument to ``setup()``.
577-
578-
:param dist: the distutils/setuptools ``Distribution`` object
579-
:param name: the name of the keyword argument (should always be
580-
"message_extractors")
581-
:param value: the value of the keyword argument
582-
:raise `DistutilsSetupError`: if the value is not valid
583-
"""
584-
assert name == 'message_extractors'
585-
if not isinstance(value, dict):
586-
raise SetupError(
587-
'the value of the "message_extractors" '
588-
'parameter must be a dictionary'
589-
)
590-
591-
592-
class init_catalog(Command):
593-
"""New catalog initialization command for use in ``setup.py`` scripts.
594-
595-
If correctly installed, this command is available to Setuptools-using
596-
setup scripts automatically. For projects using plain old ``distutils``,
597-
the command needs to be registered explicitly in ``setup.py``::
598-
599-
from babel.messages.frontend import init_catalog
600-
601-
setup(
602-
...
603-
cmdclass = {'init_catalog': init_catalog}
604-
)
605-
"""
606-
556+
class InitCatalog(CommandMixin):
607557
description = 'create a new catalog based on a POT file'
608558
user_options = [
609559
('domain=', 'D',
@@ -678,23 +628,7 @@ def run(self):
678628
write_po(outfile, catalog, width=self.width)
679629

680630

681-
class update_catalog(Command):
682-
"""Catalog merging command for use in ``setup.py`` scripts.
683-
684-
If correctly installed, this command is available to Setuptools-using
685-
setup scripts automatically. For projects using plain old ``distutils``,
686-
the command needs to be registered explicitly in ``setup.py``::
687-
688-
from babel.messages.frontend import update_catalog
689-
690-
setup(
691-
...
692-
cmdclass = {'update_catalog': update_catalog}
693-
)
694-
695-
.. versionadded:: 0.9
696-
"""
697-
631+
class UpdateCatalog(CommandMixin):
698632
description = 'update message catalogs from a POT file'
699633
user_options = [
700634
('domain=', 'D',
@@ -911,10 +845,10 @@ class CommandLineInterface:
911845
}
912846

913847
command_classes = {
914-
'compile': compile_catalog,
915-
'extract': extract_messages,
916-
'init': init_catalog,
917-
'update': update_catalog,
848+
'compile': CompileCatalog,
849+
'extract': ExtractMessages,
850+
'init': InitCatalog,
851+
'update': UpdateCatalog,
918852
}
919853

920854
log = None # Replaced on instance level
@@ -996,7 +930,7 @@ def _configure_command(self, cmdname, argv):
996930
cmdinst = cmdclass()
997931
if self.log:
998932
cmdinst.log = self.log # Use our logger, not distutils'.
999-
assert isinstance(cmdinst, Command)
933+
assert isinstance(cmdinst, CommandMixin)
1000934
cmdinst.initialize_options()
1001935

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

11141048
return method_map, options_map
11151049

1116-
def _parse_spec(s: str) -> tuple[int | None, tuple[int|tuple[int, str], ...]]:
1050+
1051+
def _parse_spec(s: str) -> tuple[int | None, tuple[int | tuple[int, str], ...]]:
11171052
inds = []
11181053
number = None
11191054
for x in s.split(','):
@@ -1125,6 +1060,7 @@ def _parse_spec(s: str) -> tuple[int | None, tuple[int|tuple[int, str], ...]]:
11251060
inds.append(int(x))
11261061
return number, tuple(inds)
11271062

1063+
11281064
def parse_keywords(strings: Iterable[str] = ()):
11291065
"""Parse keywords specifications from the given list of strings.
11301066
@@ -1173,5 +1109,16 @@ def parse_keywords(strings: Iterable[str] = ()):
11731109
return keywords
11741110

11751111

1112+
def __getattr__(name: str):
1113+
# Re-exports for backwards compatibility;
1114+
# `setuptools_frontend` is the canonical import location.
1115+
if name in {'check_message_extractors', 'compile_catalog', 'extract_messages', 'init_catalog', 'update_catalog'}:
1116+
from babel.messages import setuptools_frontend
1117+
1118+
return getattr(setuptools_frontend, name)
1119+
1120+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
1121+
1122+
11761123
if __name__ == '__main__':
11771124
main()

babel/messages/setuptools_frontend.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from __future__ import annotations
2+
3+
from babel.messages import frontend
4+
5+
try:
6+
# See: https://setuptools.pypa.io/en/latest/deprecated/distutils-legacy.html
7+
from setuptools import Command
8+
9+
try:
10+
from setuptools.errors import BaseError, OptionError, SetupError
11+
except ImportError: # Error aliases only added in setuptools 59 (2021-11).
12+
OptionError = SetupError = BaseError = Exception
13+
14+
except ImportError:
15+
from distutils.cmd import Command
16+
from distutils.errors import DistutilsSetupError as SetupError
17+
18+
19+
def check_message_extractors(dist, name, value):
20+
"""Validate the ``message_extractors`` keyword argument to ``setup()``.
21+
22+
:param dist: the distutils/setuptools ``Distribution`` object
23+
:param name: the name of the keyword argument (should always be
24+
"message_extractors")
25+
:param value: the value of the keyword argument
26+
:raise `DistutilsSetupError`: if the value is not valid
27+
"""
28+
assert name == "message_extractors"
29+
if not isinstance(value, dict):
30+
raise SetupError(
31+
'the value of the "message_extractors" parameter must be a dictionary'
32+
)
33+
34+
35+
class compile_catalog(frontend.CompileCatalog, Command):
36+
"""Catalog compilation command for use in ``setup.py`` scripts.
37+
38+
If correctly installed, this command is available to Setuptools-using
39+
setup scripts automatically. For projects using plain old ``distutils``,
40+
the command needs to be registered explicitly in ``setup.py``::
41+
42+
from babel.messages.setuptools_frontend import compile_catalog
43+
44+
setup(
45+
...
46+
cmdclass = {'compile_catalog': compile_catalog}
47+
)
48+
49+
.. versionadded:: 0.9
50+
"""
51+
52+
53+
class extract_messages(frontend.ExtractMessages, Command):
54+
"""Message extraction command for use in ``setup.py`` scripts.
55+
56+
If correctly installed, this command is available to Setuptools-using
57+
setup scripts automatically. For projects using plain old ``distutils``,
58+
the command needs to be registered explicitly in ``setup.py``::
59+
60+
from babel.messages.setuptools_frontend import extract_messages
61+
62+
setup(
63+
...
64+
cmdclass = {'extract_messages': extract_messages}
65+
)
66+
"""
67+
68+
69+
class init_catalog(frontend.InitCatalog, Command):
70+
"""New catalog initialization command for use in ``setup.py`` scripts.
71+
72+
If correctly installed, this command is available to Setuptools-using
73+
setup scripts automatically. For projects using plain old ``distutils``,
74+
the command needs to be registered explicitly in ``setup.py``::
75+
76+
from babel.messages.setuptools_frontend import init_catalog
77+
78+
setup(
79+
...
80+
cmdclass = {'init_catalog': init_catalog}
81+
)
82+
"""
83+
84+
85+
class update_catalog(frontend.UpdateCatalog, Command):
86+
"""Catalog merging command for use in ``setup.py`` scripts.
87+
88+
If correctly installed, this command is available to Setuptools-using
89+
setup scripts automatically. For projects using plain old ``distutils``,
90+
the command needs to be registered explicitly in ``setup.py``::
91+
92+
from babel.messages.setuptools_frontend import update_catalog
93+
94+
setup(
95+
...
96+
cmdclass = {'update_catalog': update_catalog}
97+
)
98+
99+
.. versionadded:: 0.9
100+
"""
101+
102+
103+
COMMANDS = {
104+
"compile_catalog": compile_catalog,
105+
"extract_messages": extract_messages,
106+
"init_catalog": init_catalog,
107+
"update_catalog": update_catalog,
108+
}

conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
from _pytest.doctest import DoctestModule
44

5-
collect_ignore = ['tests/messages/data', 'setup.py']
5+
collect_ignore = [
6+
'babel/messages/setuptools_frontend.py',
7+
'setup.py',
8+
'tests/messages/data',
9+
]
610
babel_path = Path(__file__).parent / 'babel'
711

812

0 commit comments

Comments
 (0)