Coverage for app/venv/lib/python3.14/site-packages/weblate/trans/management/commands/__init__.py: 0%

106 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5"""Helper classes for management commands.""" 

6 

7from __future__ import annotations 

8 

9from typing import TYPE_CHECKING 

10 

11from django.core.management.base import CommandError 

12from django.db import transaction 

13 

14from weblate.lang.models import Language 

15from weblate.trans.models import Component, Translation, Unit 

16from weblate.utils.management.base import BaseCommand 

17 

18if TYPE_CHECKING: 

19 from django.core.management.base import CommandParser 

20 

21 

22class WeblateComponentCommand(BaseCommand): 

23 """Command which accepts project/component/--all params to process.""" 

24 

25 needs_repo = False 

26 

27 def add_arguments(self, parser: CommandParser) -> None: 

28 parser.add_argument( 

29 "--all", 

30 action="store_true", 

31 dest="all", 

32 default=False, 

33 help="process all components", 

34 ) 

35 parser.add_argument( 

36 "--file-format", 

37 help="process all components using given file format", 

38 ) 

39 parser.add_argument( 

40 "component", 

41 nargs="*", 

42 help="Slug <project/component> of component to process", 

43 ) 

44 

45 def get_units(self, **options): 

46 """Return list of units matching parameters.""" 

47 if options["all"]: 

48 return Unit.objects.all() 

49 return Unit.objects.filter( 

50 translation__component__in=self.get_components(**options) 

51 ) 

52 

53 def iterate_units(self, **options): 

54 """Memory effective iteration over units.""" 

55 units = self.get_units(**options).order_by("pk") 

56 count = units.count() 

57 if not count: 

58 return 

59 

60 current = 0 

61 last = units.order_by("-pk")[0].pk 

62 done = 0 

63 step = 1000 

64 

65 # Iterate over chunks 

66 while current < last: 

67 self.stdout.write(f"Processing {done * 100.0 / count:.1f}%") 

68 with transaction.atomic(): 

69 step_units = units.filter(pk__gt=current)[:step].prefetch() 

70 for unit in step_units: 

71 current = unit.pk 

72 done += 1 

73 yield unit 

74 self.stdout.write("Operation completed") 

75 

76 def get_translations(self, **options): 

77 """Return list of translations matching parameters.""" 

78 return Translation.objects.prefetch().filter( 

79 component__in=self.get_components(**options) 

80 ) 

81 

82 def get_components(self, **options): 

83 """Return list of components matching parameters.""" 

84 if self.needs_repo: 

85 base = Component.objects.exclude(repo__startswith="weblate:/") 

86 else: 

87 base = Component.objects.all() 

88 if options["all"]: 

89 # all components 

90 result = base 

91 elif options["file_format"]: 

92 # all components 

93 result = base.filter(file_format=options["file_format"]) 

94 elif options["component"]: 

95 # start with none and add found 

96 result = Component.objects.none() 

97 

98 # process arguments 

99 for arg in options["component"]: 

100 if "/" in arg: 

101 # filter by component 

102 found = base.filter_by_path(arg) 

103 else: 

104 # filter by project 

105 found = base.filter(project__slug=arg) 

106 

107 # warn on no match 

108 if not found.exists(): 

109 self.stderr.write(f"{arg!r} did not match any components") 

110 msg = "Nothing to process!" 

111 raise CommandError(msg) 

112 

113 # merge results 

114 result |= found 

115 else: 

116 # no arguments to filter projects 

117 self.stderr.write("Missing component selection!") 

118 self.stderr.write(" * Use --all to select all components") 

119 self.stderr.write(" * Use --file-format to filter based on the file format") 

120 self.stderr.write(" * Specify at least one <project/component> argument") 

121 msg = "Nothing to process!" 

122 raise CommandError(msg) 

123 

124 return result 

125 

126 def handle(self, *args, **options) -> None: 

127 raise NotImplementedError 

128 

129 

130class WeblateLangCommand(WeblateComponentCommand): 

131 """ 

132 Command accepting additional language parameter. 

133 

134 It can filter list of languages to process. 

135 """ 

136 

137 def add_arguments(self, parser: CommandParser) -> None: 

138 super().add_arguments(parser) 

139 parser.add_argument( 

140 "--lang", 

141 action="store", 

142 dest="lang", 

143 default=None, 

144 help="Limit only to given languages (comma separated list)", 

145 ) 

146 

147 def get_units(self, **options): 

148 """Return list of units matching parameters.""" 

149 units = super().get_units(**options) 

150 

151 if options["lang"] is not None: 

152 units = units.filter(translation__language__code=options["lang"]) 

153 

154 return units 

155 

156 def get_translations(self, **options): 

157 """Return list of translations matching parameters.""" 

158 result = super().get_translations(**options) 

159 

160 if options["lang"] is not None: 

161 langs = options["lang"].split(",") 

162 result = result.filter(language_code__in=langs) 

163 

164 return result 

165 

166 def handle(self, *args, **options) -> None: 

167 raise NotImplementedError 

168 

169 

170class WeblateTranslationCommand(BaseCommand): 

171 """Command with target of one translation.""" 

172 

173 def add_arguments(self, parser: CommandParser) -> None: 

174 parser.add_argument("project", help="Slug of project") 

175 parser.add_argument("component", help="Slug of component") 

176 parser.add_argument("language", help="Slug of language") 

177 

178 def get_translation(self, **options): 

179 """Get translation object.""" 

180 try: 

181 component = Component.objects.get( 

182 project__slug=options["project"], slug=options["component"] 

183 ) 

184 except Component.DoesNotExist as error: 

185 msg = "No matching translation component found!" 

186 raise CommandError(msg) from error 

187 try: 

188 return Translation.objects.get( 

189 component=component, language__code=options["language"] 

190 ) 

191 except Translation.DoesNotExist as error: 

192 if options.get("add"): 

193 language = Language.objects.fuzzy_get(options["language"]) 

194 if component.add_new_language(language, None): 

195 return Translation.objects.get( 

196 component=component, language=language 

197 ) 

198 msg = "No matching translation project found!" 

199 raise CommandError(msg) from error 

200 

201 def handle(self, *args, **options) -> None: 

202 raise NotImplementedError