Coverage for sources/emcdproj/template.py: 44%

39 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-05-27 21:05 +0000

1# vim: set filetype=python fileencoding=utf-8: 

2# -*- coding: utf-8 -*- 

3 

4#============================================================================# 

5# # 

6# Licensed under the Apache License, Version 2.0 (the "License"); # 

7# you may not use this file except in compliance with the License. # 

8# You may obtain a copy of the License at # 

9# # 

10# http://www.apache.org/licenses/LICENSE-2.0 # 

11# # 

12# Unless required by applicable law or agreed to in writing, software # 

13# distributed under the License is distributed on an "AS IS" BASIS, # 

14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

15# See the License for the specific language governing permissions and # 

16# limitations under the License. # 

17# # 

18#============================================================================# 

19 

20 

21''' Copier template maintenance and validation. ''' 

22 

23 

24from __future__ import annotations 

25 

26import subprocess as _subprocess 

27 

28from . import __ 

29from . import interfaces as _interfaces 

30 

31 

32class CommandDispatcher( 

33 _interfaces.CliCommand, decorators = ( __.standard_tyro_class, ), 

34): 

35 ''' Dispatches commands for Copier template maintenance. ''' 

36 

37 command: __.typx.Union[ 

38 __.typx.Annotated[ 

39 SurveyCommand, 

40 __.tyro.conf.subcommand( 'survey', prefix_name = False ), 

41 ], 

42 __.typx.Annotated[ 

43 ValidateCommand, 

44 __.tyro.conf.subcommand( 'validate', prefix_name = False ), 

45 ], 

46 ] 

47 

48 async def __call__( 

49 self, auxdata: __.Globals, display: _interfaces.ConsoleDisplay 

50 ) -> None: 

51 ictr( 1 )( self.command ) 

52 await self.command( auxdata = auxdata, display = display ) 

53 

54 

55class SurveyCommand( 

56 _interfaces.CliCommand, decorators = ( __.standard_tyro_class, ), 

57): 

58 ''' Surveys available configuration variants. ''' 

59 

60 async def __call__( 

61 self, auxdata: __.Globals, display: _interfaces.ConsoleDisplay 

62 ) -> None: 

63 stream = await display.provide_stream( ) 

64 for variant in survey_variants( auxdata ): 

65 print( variant, file = stream ) 

66 

67 

68class ValidateCommand( 

69 _interfaces.CliCommand, decorators = ( __.standard_tyro_class, ), 

70): 

71 ''' Validates template against configuration variant. ''' 

72 

73 variant: __.typx.Annotated[ 

74 str, 

75 __.typx.Doc( ''' Configuration variant to validate. ''' ), 

76 __.tyro.conf.Positional, 

77 ] 

78 preserve: __.typx.Annotated[ 

79 bool, 

80 __.typx.Doc( ''' Preserve generated project for inspection? ''' ), 

81 ] = False 

82 

83 async def __call__( 

84 self, auxdata: __.Globals, display: _interfaces.ConsoleDisplay 

85 ) -> None: 

86 ''' Copies new project from template for configuration variant. ''' 

87 # TODO: Validate variant argument. 

88 validate_variant( 

89 auxdata, self.variant, preserve = self.preserve ) 

90 

91 

92def copy_template( answers_file: __.Path, projectdir: __.Path ) -> None: 

93 ''' Copies template to target directory using answers. ''' 

94 _subprocess.run( # noqa: S603 

95 ( 'copier', 'copy', '--data-file', str( answers_file ), 

96 '--defaults', '--overwrite', '--vcs-ref', 'HEAD', 

97 '.', str( projectdir ) ), 

98 cwd = __.Path( ), check = True ) 

99 

100 

101def survey_variants( auxdata: __.Globals ) -> __.cabc.Sequence[ str ]: 

102 ''' Surveys available configuration variants. ''' 

103 location = auxdata.distribution.provide_data_location( 'copier' ) 

104 return tuple( 

105 fsent.stem.lstrip( 'answers-' ) 

106 for fsent in location.glob( 'answers-*.yaml' ) 

107 if fsent.is_file( ) ) 

108 

109 

110def validate_variant( 

111 auxdata: __.Globals, variant: str, preserve: bool 

112) -> None: 

113 ''' Validates configuration variant. ''' 

114 answers_file = ( 

115 auxdata.distribution.provide_data_location( 

116 'copier', f"answers-{variant}.yaml" ) ) 

117 if not answers_file.is_file( ): 

118 # TODO: Raise error. 

119 return 

120 with _manage_temporary_directory( preserve = preserve ) as tmpdir: 

121 projectdir = tmpdir / variant 

122 copy_template( answers_file, projectdir ) 

123 validate_variant_project( projectdir ) 

124 

125 

126def validate_variant_project( projectdir: __.Path ) -> None: 

127 ''' Validates standard project as generated from template. ''' 

128 for command in ( 

129 ( 'hatch', '--env', 'develop', 'run', 

130 'python', '-m', 'pip', 'install', 

131 '--upgrade', 'pip', 'build' ), 

132 ( 'hatch', '--env', 'develop', 'run', 'make-all' ), 

133 ): _subprocess.run( command, cwd = str( projectdir ), check = True ) # noqa: S603 

134 

135 

136@__.ctxl.contextmanager 

137def _manage_temporary_directory( 

138 preserve: bool 

139) -> __.cabc.Iterator[ __.Path ]: 

140 # TODO: Python 3.12: Replace with tempfile.TemporaryDirectory, 

141 # ( delete = not preserve ) 

142 location = __.Path( __.tempfile.mkdtemp( ) ) 

143 yield location 

144 if not preserve: __.shutil.rmtree( location )