Coverage for tests/test_000_accretive/test_010_internals.py: 100%

188 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-10 22:24 +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''' Assert correct function of internals. ''' 

22 

23# mypy: ignore-errors 

24# pylint: disable=attribute-defined-outside-init 

25# pylint: disable=magic-value-comparison 

26# pylint: disable=missing-class-docstring 

27# pylint: disable=protected-access 

28# ruff: noqa: E711,E712 

29 

30 

31import pytest 

32 

33from platform import python_implementation 

34from types import MappingProxyType as DictionaryProxy 

35 

36from . import PACKAGE_NAME, cache_import_module 

37 

38 

39MODULE_QNAME = f"{PACKAGE_NAME}.__" 

40MODULE_ATTRIBUTE_NAMES = ( 

41 'Absent', 

42 'ConcealerExtension', 

43 'CoreDictionary', 

44 'Docstring', 

45 'Falsifier', 

46 'InternalClass', 

47 'InternalObject', 

48 #'absent', 

49 'calculate_class_fqname', 

50 'calculate_fqname', 

51 'discover_public_attributes', 

52 'generate_docstring', 

53) 

54 

55exceptions = cache_import_module( f"{PACKAGE_NAME}.exceptions" ) 

56module = cache_import_module( MODULE_QNAME ) 

57 

58dictionary_posargs = ( ( ( 'foo', 1 ), ( 'bar', 2 ) ), { 'unicorn': True } ) 

59dictionary_nomargs = DictionaryProxy( dict( orb = False ) ) 

60 

61pypy_skip_mark = pytest.mark.skipif( 

62 'PyPy' == python_implementation( ), 

63 reason = "PyPy handles class cell updates differently" 

64) 

65 

66 

67def test_102_concealer_extension_attribute_visibility( ): 

68 ''' Instance conceals attributes according to visibility rules. ''' 

69 obj = module.ConcealerExtension( ) 

70 obj.public = 42 

71 assert ( 'public', ) == tuple( dir( obj ) ) 

72 obj._hidden = 24 

73 assert ( 'public', ) == tuple( dir( obj ) ) 

74 obj._visible = 12 

75 obj._attribute_visibility_includes_ = frozenset( ( '_visible', ) ) 

76 assert ( '_visible', 'public' ) == tuple( sorted( dir( obj ) ) ) 

77 

78 

79def test_111_internal_class_immutability( ): 

80 ''' Class attributes become immutable after initialization. ''' 

81 factory = module.InternalClass 

82 class Example( metaclass = factory ): value = 42 

83 with pytest.raises( AttributeError ): Example.value = 24 

84 with pytest.raises( AttributeError ): del Example.value 

85 

86 

87def test_112_internal_class_decorator_handling( ): 

88 ''' Class properly handles decorators during creation. ''' 

89 factory = module.InternalClass 

90 def decorator1( cls ): 

91 cls.attr1 = 'one' 

92 return cls 

93 def decorator2( cls ): 

94 cls.attr2 = 'two' 

95 return cls 

96 

97 class Example( 

98 metaclass = factory, decorators = ( decorator1, decorator2 ) 

99 ): pass 

100 

101 assert 'one' == Example.attr1 

102 assert 'two' == Example.attr2 # pylint: disable=no-member 

103 with pytest.raises( AttributeError ): Example.attr1 = 'changed' 

104 

105 

106def test_113_internal_class_attribute_visibility( ): 

107 ''' Class conceals attributes according to visibility rules. ''' 

108 factory = module.InternalClass 

109 

110 class Example( metaclass = factory ): 

111 _class_attribute_visibility_includes_ = frozenset( ( '_visible', ) ) 

112 public = 42 

113 _hidden = 24 

114 _visible = 12 

115 

116 assert ( '_visible', 'public' ) == tuple( sorted( dir( Example ) ) ) 

117 

118 

119@pypy_skip_mark 

120def test_114_internal_class_decorator_replacement( ): 

121 ''' Class properly handles decorators that return new classes. ''' 

122 from dataclasses import dataclass 

123 factory = module.InternalClass 

124 

125 class Example( 

126 metaclass = factory, decorators = ( dataclass( slots = True ), ) 

127 ): 

128 field1: str 

129 field2: int 

130 

131 assert hasattr( Example, '__slots__' ) 

132 with pytest.raises( AttributeError ): Example.field1 = 'changed' 

133 

134 

135def test_115_internal_class_behaviors_extension( ): 

136 ''' Class properly extends existing behaviors. ''' 

137 factory = module.InternalClass 

138 

139 class Base( metaclass = factory ): 

140 _class_behaviors_ = { 'existing' } 

141 

142 assert 'existing' in Base._class_behaviors_ 

143 assert module._immutability_label in Base._class_behaviors_ 

144 

145 

146def test_150_internal_object_immutability( ): 

147 ''' Instance attributes cannot be modified or deleted. ''' 

148 class Example( module.InternalObject ): 

149 def __init__( self ): 

150 # Need to bypass normal setattr to initialize 

151 super( module.InternalObject, self ).__setattr__( 'value', 42 ) 

152 

153 obj = Example( ) 

154 with pytest.raises( AttributeError ): obj.value = 24 

155 with pytest.raises( AttributeError ): obj.new_attr = 'test' 

156 with pytest.raises( AttributeError ): del obj.value 

157 

158 

159def test_160_falsifier_behavior( ): 

160 ''' Falsifier objects are falsey and compare properly. ''' 

161 class Example( module.Falsifier ): pass 

162 

163 obj1 = Example( ) 

164 obj2 = Example( ) 

165 assert not obj1 

166 assert obj1 == obj1 # pylint: disable=comparison-with-itself 

167 assert obj1 != obj2 

168 assert obj1 is not True 

169 assert obj1 is not False 

170 assert obj1 is not None 

171 

172 

173def test_170_absent_singleton( ): 

174 ''' Absent class produces singleton instance. ''' 

175 obj1 = module.Absent( ) 

176 obj2 = module.Absent( ) 

177 assert obj1 is obj2 

178 assert obj1 is module.absent 

179 assert not obj1 

180 assert obj1 == obj1 # pylint: disable=comparison-with-itself 

181 assert obj1 != None # pylint: disable=singleton-comparison 

182 assert obj1 != False # pylint: disable=singleton-comparison 

183 

184 

185def test_171_absent_type_guard( ): 

186 ''' Type guard correctly identifies absent values. ''' 

187 def example( value: module.Optional[ str ] ) -> str: 

188 if not module.is_absent( value ): return value 

189 return 'default' 

190 

191 assert 'test' == example( 'test' ) 

192 assert 'default' == example( module.absent ) 

193 

194 

195def test_200_core_dictionary_instantiation( ): 

196 ''' Class instantiates. ''' 

197 factory = module.CoreDictionary 

198 dct1 = factory( ) 

199 assert isinstance( dct1, factory ) 

200 dct2 = factory( *dictionary_posargs, **dictionary_nomargs ) 

201 assert isinstance( dct2, factory ) 

202 assert 1 == dct2[ 'foo' ] 

203 assert 2 == dct2[ 'bar' ] 

204 assert dct2[ 'unicorn' ] 

205 assert not dct2[ 'orb' ] 

206 assert ( 'foo', 'bar', 'unicorn', 'orb' ) == tuple( dct2.keys( ) ) 

207 assert ( 1, 2, True, False ) == tuple( dct2.values( ) ) 

208 

209 

210def test_201_core_dictionary_duplication( ): 

211 ''' Dictionary is duplicable. ''' 

212 factory = module.CoreDictionary 

213 odct = factory( *dictionary_posargs, **dictionary_nomargs ) 

214 ddct = odct.copy( ) 

215 assert odct == ddct 

216 odct[ 'baz' ] = 42 

217 assert odct != ddct 

218 

219 

220def test_210_core_dictionary_entry_accretion( ): 

221 ''' Dictionary accretes entries. ''' 

222 factory = module.CoreDictionary 

223 dct = factory( *dictionary_posargs, **dictionary_nomargs ) 

224 with pytest.raises( exceptions.IndelibleEntryError ): 

225 dct[ 'foo' ] = 42 

226 with pytest.raises( exceptions.IndelibleEntryError ): 

227 del dct[ 'foo' ] 

228 dct[ 'baz' ] = 3.1415926535 

229 with pytest.raises( exceptions.IndelibleEntryError ): 

230 dct[ 'baz' ] = -1 

231 with pytest.raises( exceptions.IndelibleEntryError ): 

232 del dct[ 'baz' ] 

233 

234 

235def test_211_core_dictionary_entry_accretion_via_update( ): 

236 ''' Dictionary accretes entries via update. ''' 

237 factory = module.CoreDictionary 

238 dct = factory( ) 

239 dct.update( *dictionary_posargs, **dictionary_nomargs ) 

240 with pytest.raises( exceptions.IndelibleEntryError ): 

241 dct[ 'foo' ] = 42 

242 with pytest.raises( exceptions.IndelibleEntryError ): 

243 del dct[ 'foo' ] 

244 dct[ 'baz' ] = 3.1415926535 

245 with pytest.raises( exceptions.IndelibleEntryError ): 

246 dct[ 'baz' ] = -1 

247 with pytest.raises( exceptions.IndelibleEntryError ): 

248 del dct[ 'baz' ] 

249 

250 

251def test_212_core_dictionary_update_validation( ): 

252 ''' Dictionary update properly handles various input types. ''' 

253 factory = module.CoreDictionary 

254 dct = factory( ) 

255 dct.update( { 'a': 1, 'b': 2 } ) 

256 assert 1 == dct[ 'a' ] 

257 dct.update( [ ( 'c', 3 ), ( 'd', 4 ) ] ) 

258 assert 3 == dct[ 'c' ] 

259 dct.update( e = 5, f = 6 ) 

260 assert 5 == dct[ 'e' ] 

261 dct.update( { 'g': 7 }, [ ( 'h', 8 ) ], i = 9 ) 

262 assert 7 == dct[ 'g' ] 

263 assert 8 == dct[ 'h' ] 

264 assert 9 == dct[ 'i' ] 

265 with pytest.raises( exceptions.IndelibleEntryError ): 

266 dct.update( { 'a': 10 } ) 

267 

268 

269def test_220_core_dictionary_operation_prevention( ): 

270 ''' Dictionary cannot perform entry deletions and mutations. ''' 

271 factory = module.CoreDictionary 

272 dct = factory( *dictionary_posargs, **dictionary_nomargs ) 

273 with pytest.raises( exceptions.InvalidOperationError ): 

274 dct.clear( ) 

275 with pytest.raises( exceptions.InvalidOperationError ): 

276 dct.pop( 'foo' ) 

277 with pytest.raises( exceptions.InvalidOperationError ): 

278 dct.pop( 'foo', default = -1 ) 

279 with pytest.raises( exceptions.InvalidOperationError ): 

280 dct.pop( 'baz' ) 

281 with pytest.raises( exceptions.InvalidOperationError ): 

282 dct.popitem( ) 

283 

284 

285def test_300_fqname_discovery( ): 

286 ''' Fully-qualified name of object is discovered. ''' 

287 assert 'builtins.NoneType' == module.calculate_fqname( None ) 

288 assert ( 

289 'builtins.type' 

290 == module.calculate_fqname( module.ConcealerExtension ) ) 

291 obj = module.ConcealerExtension( ) 

292 assert ( 

293 f"{MODULE_QNAME}.ConcealerExtension" 

294 == module.calculate_fqname( obj ) ) 

295 

296 

297@pytest.mark.parametrize( 

298 'provided, expected', 

299 ( 

300 ( { 'foo': 12 }, ( ) ), 

301 ( { '_foo': cache_import_module }, ( ) ), 

302 ( 

303 { name: getattr( module, name ) 

304 for name in MODULE_ATTRIBUTE_NAMES }, 

305 MODULE_ATTRIBUTE_NAMES 

306 ), 

307 ) 

308) 

309def test_400_public_attribute_discovery( provided, expected ): 

310 ''' Public attributes are discovered from dictionary. ''' 

311 assert expected == module.discover_public_attributes( provided ) 

312 

313 

314def test_500_docstring_generation_argument_acceptance( ): 

315 ''' Docstring generator errors on invalid arguments. ''' 

316 class Foo: pass # pylint: disable=missing-class-docstring 

317 with pytest.raises( KeyError ): 

318 module.generate_docstring( 1 ) 

319 with pytest.raises( KeyError ): 

320 module.generate_docstring( '8-bit theater' ) 

321 assert not module.generate_docstring( Foo ) 

322 assert module.generate_docstring( 'instance attributes accretion' ) 

323 assert module.generate_docstring( module.Docstring( 'foo bar' ) ) 

324 

325 

326def test_501_docstring_generation_validity( ): 

327 ''' Generated docstrings are correctly formatted. ''' 

328 from inspect import getdoc 

329 

330 class Foo: 

331 ''' headline 

332 

333 additional information 

334 ''' 

335 

336 docstring_generated = module.generate_docstring( 

337 Foo, 

338 module.Docstring( 'foo bar' ), 

339 'class attributes accretion' ) 

340 docstring_expected = '\n\n'.join( ( 

341 getdoc( Foo ), 

342 'foo bar', 

343 module.generate_docstring( 'class attributes accretion' ) ) ) 

344 assert docstring_expected == docstring_generated