Coverage for src/lektor_ng/cli_utils.py: 94%

86 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-02 20:38 +0000

1# pylint: disable=import-outside-toplevel 

2from __future__ import annotations 

3 

4import json 

5import os 

6from pathlib import Path 

7from typing import Any 

8 

9import click 

10 

11from lektor_ng.i18n import get_default_lang, is_valid_language 

12from lektor_ng.project import Project 

13 

14 

15def echo_json(data): 

16 click.echo(json.dumps(data, indent=2).rstrip()) 

17 

18 

19def pruneflag(cli): 

20 return click.option( 

21 "--prune/--no-prune", 

22 default=True, 

23 help="Whether outdated artifacts are pruned. The default is to prune.", 

24 )(cli) 

25 

26 

27def extraflag(cli): 

28 return click.option( 

29 "-f", 

30 "--extra-flag", 

31 "extra_flags", 

32 multiple=True, 

33 help="Defines an arbitrary flag. These can be used by plugins " 

34 "to customize the build and deploy process. More information can be " 

35 "found in the documentation of affected plugins.", 

36 )(cli) 

37 

38 

39class AliasedGroup(click.Group): 

40 # pylint: disable=inconsistent-return-statements 

41 def get_command(self, ctx, cmd_name): 

42 rv = click.Group.get_command(self, ctx, cmd_name) 

43 if rv is not None: 

44 return rv 

45 matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] 

46 if not matches: 

47 return None 

48 if len(matches) == 1: 

49 return click.Group.get_command(self, ctx, matches[0]) 

50 ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 

51 

52 

53class Context: 

54 def __init__(self): 

55 self._project_path = os.environ.get("LEKTOR_PROJECT") or None 

56 self._project = None 

57 self._env = None 

58 self._ui_lang = None 

59 

60 def _get_ui_lang(self): 

61 rv = self._ui_lang 

62 if rv is None: 

63 rv = self._ui_lang = get_default_lang() 

64 return rv 

65 

66 def _set_ui_lang(self, value): 

67 self._ui_lang = value 

68 

69 ui_lang = property(_get_ui_lang, _set_ui_lang) 

70 del _get_ui_lang, _set_ui_lang 

71 

72 def set_project_path(self, value): 

73 self._project_path = value 

74 self._project = None 

75 

76 def get_project(self, silent=False): 

77 if self._project is not None: 

78 return self._project 

79 if self._project_path is not None: 

80 rv = Project.from_path(self._project_path) 

81 else: 

82 rv = Project.discover() 

83 if rv is None: 

84 if silent: 

85 return None 

86 if self._project_path is None: 

87 raise click.UsageError( 

88 "Could not automatically discover a " 

89 "project in the working or ancestor directories. " 

90 "The --project global parameter or the " 

91 "LEKTOR_PROJECT environment variable may be used " 

92 "to explicitly specify the path to the project." 

93 ) 

94 raise click.UsageError(f'Could not find project "{self._project_path}"') 

95 self._project = rv 

96 return rv 

97 

98 def get_default_output_path(self): 

99 rv = os.environ.get("LEKTOR_OUTPUT_PATH") 

100 if rv is not None: 

101 return rv 

102 return self.get_project().get_output_path() 

103 

104 def get_env(self, extra_flags=None): 

105 if self._env is not None: 

106 return self._env 

107 from lektor_ng.environment import Environment 

108 

109 env = Environment(self.get_project(), load_plugins=False, extra_flags=extra_flags) 

110 self._env = env 

111 return env 

112 

113 def load_plugins(self, reinstall=False, extra_flags=None): 

114 from .packages import load_packages 

115 

116 load_packages(self.get_env(extra_flags=extra_flags), reinstall=reinstall) 

117 

118 if not reinstall: 

119 from .pluginsystem import initialize_plugins 

120 

121 initialize_plugins(self.get_env()) 

122 

123 

124pass_context = click.make_pass_decorator(Context, ensure=True) 

125 

126 

127def validate_language(ctx, param, value): 

128 if value is not None and not is_valid_language(value): 

129 raise click.BadParameter(f'Unsupported language "{value}".') 

130 return value 

131 

132 

133class ResolvedPath(click.Path): 

134 """A click paramter type for a resolved path. 

135 

136 We could just use ``click.Path(resolve_path=True)`` except that that 

137 fails sometimes under Windows running python <= 3.9. 

138 

139 See https://github.com/pallets/click/issues/2466 

140 """ 

141 

142 def __init__(self, writable=False, file_okay=True): 

143 super().__init__(resolve_path=True, allow_dash=False, writable=writable, file_okay=file_okay) 

144 

145 def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> Any: 

146 abspath = Path(value).absolute() 

147 # fsdecode to ensure that the return value is a str. 

148 # (with click<8.0.3 Path.convert will return Path if passed a Path) 

149 return os.fsdecode(super().convert(abspath, param, ctx))