Coverage for tests/tests.py: 97%

524 statements  

« prev     ^ index     » next       coverage.py v7.3.4, created at 2024-01-20 18:02 +0000

1import inspect 

2import json 

3import os 

4import re 

5import subprocess 

6import sys 

7from io import StringIO 

8from pathlib import Path 

9from typing import Any 

10 

11import django 

12import typer 

13from click.exceptions import UsageError 

14from django.apps import apps 

15from django.core.management import call_command 

16from django.test import TestCase, override_settings 

17from sklearn.feature_extraction.text import TfidfVectorizer 

18from sklearn.metrics.pairwise import cosine_similarity 

19 

20from django_typer import TyperCommand, get_command, group 

21from django_typer.tests.utils import read_django_parameters 

22 

23 

24def similarity(text1, text2): 

25 """ 

26 Compute the cosine similarity between two texts. 

27 https://en.wikipedia.org/wiki/Cosine_similarity 

28 

29 We use this to lazily evaluate the output of --help to our 

30 renderings. 

31 """ 

32 vectorizer = TfidfVectorizer() 

33 tfidf_matrix = vectorizer.fit_transform([text1, text2]) 

34 return cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0] 

35 

36 

37manage_py = Path(__file__).parent.parent.parent / "manage.py" 

38TESTS_DIR = Path(__file__).parent 

39 

40 

41def get_named_arguments(function): 

42 sig = inspect.signature(function) 

43 return [ 

44 name 

45 for name, param in sig.parameters.items() 

46 if param.default != inspect.Parameter.empty 

47 ] 

48 

49 

50def run_command(command, *args, parse_json=True): 

51 cwd = os.getcwd() 

52 try: 

53 os.chdir(manage_py.parent) 

54 result = subprocess.run( 

55 [sys.executable, f"./{manage_py.name}", command, *args], 

56 capture_output=True, 

57 text=True, 

58 ) 

59 

60 # Check the return code to ensure the script ran successfully 

61 if result.returncode != 0: 

62 return result.stderr or result.stdout or "" 

63 

64 # Parse the output 

65 if result.stdout: 

66 if parse_json: 

67 try: 

68 return json.loads(result.stdout) 

69 except json.JSONDecodeError: 

70 return result.stdout or result.stderr or "" 

71 return result.stdout 

72 return result.stderr or "" 

73 finally: 

74 os.chdir(cwd) 

75 

76 

77class BasicTests(TestCase): 

78 def test_command_line(self): 

79 self.assertEqual( 

80 run_command("basic", "a1", "a2"), 

81 {"arg1": "a1", "arg2": "a2", "arg3": 0.5, "arg4": 1}, 

82 ) 

83 

84 self.assertEqual( 

85 run_command("basic", "a1", "a2", "--arg3", "0.75", "--arg4", "2"), 

86 {"arg1": "a1", "arg2": "a2", "arg3": 0.75, "arg4": 2}, 

87 ) 

88 

89 def test_call_command(self): 

90 out = StringIO() 

91 returned_options = json.loads(call_command("basic", ["a1", "a2"], stdout=out)) 

92 self.assertEqual( 

93 returned_options, {"arg1": "a1", "arg2": "a2", "arg3": 0.5, "arg4": 1} 

94 ) 

95 

96 def test_call_command_stdout(self): 

97 out = StringIO() 

98 call_command("basic", ["a1", "a2"], stdout=out) 

99 printed_options = json.loads(out.getvalue()) 

100 self.assertEqual( 

101 printed_options, {"arg1": "a1", "arg2": "a2", "arg3": 0.5, "arg4": 1} 

102 ) 

103 

104 def test_get_version(self): 

105 self.assertEqual( 

106 str(run_command("basic", "--version")).strip(), django.get_version() 

107 ) 

108 

109 def test_call_direct(self): 

110 basic = get_command("basic") 

111 self.assertEqual( 

112 json.loads(basic.handle("a1", "a2")), 

113 {"arg1": "a1", "arg2": "a2", "arg3": 0.5, "arg4": 1}, 

114 ) 

115 

116 from django_typer.tests.test_app.management.commands.basic import ( 

117 Command as Basic, 

118 ) 

119 

120 self.assertEqual( 

121 json.loads(Basic()("a1", "a2", arg3=0.75, arg4=2)), 

122 {"arg1": "a1", "arg2": "a2", "arg3": 0.75, "arg4": 2}, 

123 ) 

124 

125 def test_parser(self): 

126 basic_cmd = get_command("basic") 

127 parser = basic_cmd.create_parser("./manage.py", "basic") 

128 with self.assertRaises(NotImplementedError): 

129 parser.add_argument() 

130 

131 

132class CommandDefinitionTests(TestCase): 

133 def test_group_callback_throws(self): 

134 class CBTestCommand(TyperCommand): 

135 @group() 

136 def grp(): 

137 pass 

138 

139 grp.group() 

140 

141 def grp2(): 

142 pass 

143 

144 with self.assertRaises(NotImplementedError): 

145 

146 class CommandBad(TyperCommand): 

147 @group() 

148 def grp(): 

149 pass 

150 

151 @grp.callback() 

152 def bad_callback(): 

153 pass 

154 

155 with self.assertRaises(NotImplementedError): 

156 

157 class CommandBad(CBTestCommand): 

158 @CBTestCommand.grp.callback() 

159 def bad_callback(): 

160 pass 

161 

162 

163class InterfaceTests(TestCase): 

164 """ 

165 Make sure the django_typer decorator interfaces match the 

166 typer decorator interfaces. We don't simply pass variadic arguments 

167 to the typer decorator because we want the IDE to offer auto complete 

168 suggestions. This is a "developer experience" concession 

169 """ 

170 

171 def test_command_interface_matches(self): 

172 from django_typer import command 

173 

174 command_params = set(get_named_arguments(command)) 

175 typer_params = set(get_named_arguments(typer.Typer.command)) 

176 

177 self.assertFalse(command_params.symmetric_difference(typer_params)) 

178 

179 def test_initialize_interface_matches(self): 

180 from django_typer import initialize 

181 

182 initialize_params = set(get_named_arguments(initialize)) 

183 typer_params = set(get_named_arguments(typer.Typer.callback)) 

184 

185 self.assertFalse(initialize_params.symmetric_difference(typer_params)) 

186 

187 def test_typer_command_interface_matches(self): 

188 from django_typer import _TyperCommandMeta 

189 

190 typer_command_params = set(get_named_arguments(_TyperCommandMeta.__new__)) 

191 typer_params = set(get_named_arguments(typer.Typer.__init__)) 

192 typer_params.remove("name") 

193 typer_params.remove("add_completion") 

194 self.assertFalse(typer_command_params.symmetric_difference(typer_params)) 

195 

196 def test_group_interface_matches(self): 

197 from django_typer import GroupFunction 

198 

199 typer_command_params = set(get_named_arguments(GroupFunction.group)) 

200 typer_params = set(get_named_arguments(typer.Typer.add_typer)) 

201 typer_params.remove("callback") 

202 self.assertFalse(typer_command_params.symmetric_difference(typer_params)) 

203 

204 def test_group_command_interface_matches(self): 

205 from django_typer import GroupFunction 

206 

207 typer_command_params = set(get_named_arguments(GroupFunction.command)) 

208 typer_params = set(get_named_arguments(typer.Typer.command)) 

209 self.assertFalse(typer_command_params.symmetric_difference(typer_params)) 

210 

211 

212class MultiTests(TestCase): 

213 def test_command_line(self): 

214 self.assertEqual( 

215 run_command("multi", "cmd1", "/path/one", "/path/two"), 

216 {"files": ["/path/one", "/path/two"], "flag1": False}, 

217 ) 

218 

219 self.assertEqual( 

220 run_command("multi", "cmd1", "/path/four", "/path/three", "--flag1"), 

221 {"files": ["/path/four", "/path/three"], "flag1": True}, 

222 ) 

223 

224 self.assertEqual( 

225 run_command("multi", "sum", "1.2", "3.5", " -12.3"), sum([1.2, 3.5, -12.3]) 

226 ) 

227 

228 self.assertEqual(run_command("multi", "cmd3"), {}) 

229 

230 def test_call_command(self): 

231 ret = json.loads(call_command("multi", ["cmd1", "/path/one", "/path/two"])) 

232 self.assertEqual(ret, {"files": ["/path/one", "/path/two"], "flag1": False}) 

233 

234 ret = json.loads( 

235 call_command("multi", ["cmd1", "/path/four", "/path/three", "--flag1"]) 

236 ) 

237 self.assertEqual(ret, {"files": ["/path/four", "/path/three"], "flag1": True}) 

238 

239 ret = json.loads(call_command("multi", ["sum", "1.2", "3.5", " -12.3"])) 

240 self.assertEqual(ret, sum([1.2, 3.5, -12.3])) 

241 

242 ret = json.loads(call_command("multi", ["cmd3"])) 

243 self.assertEqual(ret, {}) 

244 

245 def test_call_command_stdout(self): 

246 out = StringIO() 

247 call_command("multi", ["cmd1", "/path/one", "/path/two"], stdout=out) 

248 self.assertEqual( 

249 json.loads(out.getvalue()), 

250 {"files": ["/path/one", "/path/two"], "flag1": False}, 

251 ) 

252 

253 out = StringIO() 

254 call_command( 

255 "multi", ["cmd1", "/path/four", "/path/three", "--flag1"], stdout=out 

256 ) 

257 self.assertEqual( 

258 json.loads(out.getvalue()), 

259 {"files": ["/path/four", "/path/three"], "flag1": True}, 

260 ) 

261 

262 out = StringIO() 

263 call_command("multi", ["sum", "1.2", "3.5", " -12.3"], stdout=out) 

264 self.assertEqual(json.loads(out.getvalue()), sum([1.2, 3.5, -12.3])) 

265 

266 out = StringIO() 

267 call_command("multi", ["cmd3"], stdout=out) 

268 self.assertEqual(json.loads(out.getvalue()), {}) 

269 

270 def test_get_version(self): 

271 self.assertEqual( 

272 str(run_command("multi", "--version")).strip(), django.get_version() 

273 ) 

274 self.assertEqual( 

275 str(run_command("multi", "--version", "cmd1")).strip(), django.get_version() 

276 ) 

277 self.assertEqual( 

278 str(run_command("multi", "--version", "sum")).strip(), django.get_version() 

279 ) 

280 self.assertEqual( 

281 str(run_command("multi", "--version", "cmd3")).strip(), django.get_version() 

282 ) 

283 

284 def test_call_direct(self): 

285 multi = get_command("multi") 

286 

287 self.assertEqual( 

288 json.loads(multi.cmd1(["/path/one", "/path/two"])), 

289 {"files": ["/path/one", "/path/two"], "flag1": False}, 

290 ) 

291 

292 self.assertEqual( 

293 json.loads(multi.cmd1(["/path/four", "/path/three"], flag1=True)), 

294 {"files": ["/path/four", "/path/three"], "flag1": True}, 

295 ) 

296 

297 self.assertEqual(float(multi.sum([1.2, 3.5, -12.3])), sum([1.2, 3.5, -12.3])) 

298 

299 self.assertEqual(json.loads(multi.cmd3()), {}) 

300 

301 

302class TestGetCommand(TestCase): 

303 def test_get_command(self): 

304 from django_typer.tests.test_app.management.commands.basic import ( 

305 Command as Basic, 

306 ) 

307 

308 basic = get_command("basic") 

309 assert basic.__class__ == Basic 

310 

311 from django_typer.tests.test_app.management.commands.multi import ( 

312 Command as Multi, 

313 ) 

314 

315 multi = get_command("multi") 

316 assert multi.__class__ == Multi 

317 cmd1 = get_command("multi", "cmd1") 

318 assert cmd1.__func__ is multi.cmd1.__func__ 

319 sum = get_command("multi", "sum") 

320 assert sum.__func__ is multi.sum.__func__ 

321 cmd3 = get_command("multi", "cmd3") 

322 assert cmd3.__func__ is multi.cmd3.__func__ 

323 

324 from django_typer.tests.test_app.management.commands.callback1 import ( 

325 Command as Callback1, 

326 ) 

327 

328 callback1 = get_command("callback1") 

329 assert callback1.__class__ == Callback1 

330 

331 # callbacks are not commands 

332 with self.assertRaises(ValueError): 

333 get_command("callback1", "init") 

334 

335 

336class CallbackTests(TestCase): 

337 cmd_name = "callback1" 

338 

339 def test_helps(self, top_level_only=False): 

340 buffer = StringIO() 

341 cmd = get_command(self.cmd_name, stdout=buffer, no_color=True) 

342 

343 help_output_top = run_command(self.cmd_name, "--help") 

344 cmd.print_help("./manage.py", self.cmd_name) 

345 self.assertEqual(help_output_top.strip(), buffer.getvalue().strip()) 

346 self.assertIn(f"Usage: ./manage.py {self.cmd_name} [OPTIONS]", help_output_top) 

347 

348 if not top_level_only: 

349 buffer.truncate(0) 

350 buffer.seek(0) 

351 callback_help = run_command(self.cmd_name, "5", self.cmd_name, "--help") 

352 cmd.print_help("./manage.py", self.cmd_name, self.cmd_name) 

353 self.assertEqual(callback_help.strip(), buffer.getvalue().strip()) 

354 self.assertIn( 

355 f"Usage: ./manage.py {self.cmd_name} P1 {self.cmd_name} [OPTIONS] ARG1 ARG2", 

356 callback_help, 

357 ) 

358 

359 def test_command_line(self): 

360 self.assertEqual( 

361 run_command(self.cmd_name, "5", self.cmd_name, "a1", "a2"), 

362 { 

363 "p1": 5, 

364 "flag1": False, 

365 "flag2": True, 

366 "arg1": "a1", 

367 "arg2": "a2", 

368 "arg3": 0.5, 

369 "arg4": 1, 

370 }, 

371 ) 

372 

373 self.assertEqual( 

374 run_command( 

375 self.cmd_name, 

376 "--flag1", 

377 "--no-flag2", 

378 "6", 

379 self.cmd_name, 

380 "a1", 

381 "a2", 

382 "--arg3", 

383 "0.75", 

384 "--arg4", 

385 "2", 

386 ), 

387 { 

388 "p1": 6, 

389 "flag1": True, 

390 "flag2": False, 

391 "arg1": "a1", 

392 "arg2": "a2", 

393 "arg3": 0.75, 

394 "arg4": 2, 

395 }, 

396 ) 

397 

398 def test_call_command(self, should_raise=True): 

399 ret = json.loads( 

400 call_command( 

401 self.cmd_name, 

402 *["5", self.cmd_name, "a1", "a2"], 

403 **{"p1": 5, "arg1": "a1", "arg2": "a2"}, 

404 ) 

405 ) 

406 self.assertEqual( 

407 ret, 

408 { 

409 "p1": 5, 

410 "flag1": False, 

411 "flag2": True, 

412 "arg1": "a1", 

413 "arg2": "a2", 

414 "arg3": 0.5, 

415 "arg4": 1, 

416 }, 

417 ) 

418 

419 ret = json.loads( 

420 call_command( 

421 self.cmd_name, 

422 *[ 

423 "--flag1", 

424 "--no-flag2", 

425 "6", 

426 self.cmd_name, 

427 "a1", 

428 "a2", 

429 "--arg3", 

430 "0.75", 

431 "--arg4", 

432 "2", 

433 ], 

434 ) 

435 ) 

436 self.assertEqual( 

437 ret, 

438 { 

439 "p1": 6, 

440 "flag1": True, 

441 "flag2": False, 

442 "arg1": "a1", 

443 "arg2": "a2", 

444 "arg3": 0.75, 

445 "arg4": 2, 

446 }, 

447 ) 

448 

449 # show that order matters args vs options 

450 interspersed = [ 

451 lambda: call_command( 

452 self.cmd_name, 

453 *[ 

454 "6", 

455 "--flag1", 

456 "--no-flag2", 

457 self.cmd_name, 

458 "n1", 

459 "n2", 

460 "--arg3", 

461 "0.2", 

462 "--arg4", 

463 "9", 

464 ], 

465 ), 

466 lambda: call_command( 

467 self.cmd_name, 

468 *[ 

469 "--no-flag2", 

470 "6", 

471 "--flag1", 

472 self.cmd_name, 

473 "--arg4", 

474 "9", 

475 "n1", 

476 "n2", 

477 "--arg3", 

478 "0.2", 

479 ], 

480 ), 

481 ] 

482 expected = { 

483 "p1": 6, 

484 "flag1": True, 

485 "flag2": False, 

486 "arg1": "n1", 

487 "arg2": "n2", 

488 "arg3": 0.2, 

489 "arg4": 9, 

490 } 

491 if should_raise: 

492 for call_cmd in interspersed: 

493 if should_raise: 493 ↛ 497line 493 didn't jump to line 497, because the condition on line 493 was never false

494 with self.assertRaises(BaseException): 

495 call_cmd() 

496 else: 

497 self.assertEqual(json.loads(call_cmd()), expected) 

498 

499 def test_call_command_stdout(self): 

500 out = StringIO() 

501 call_command( 

502 self.cmd_name, 

503 [ 

504 "--flag1", 

505 "--no-flag2", 

506 "6", 

507 self.cmd_name, 

508 "a1", 

509 "a2", 

510 "--arg3", 

511 "0.75", 

512 "--arg4", 

513 "2", 

514 ], 

515 stdout=out, 

516 ) 

517 

518 self.assertEqual( 

519 json.loads(out.getvalue()), 

520 { 

521 "p1": 6, 

522 "flag1": True, 

523 "flag2": False, 

524 "arg1": "a1", 

525 "arg2": "a2", 

526 "arg3": 0.75, 

527 "arg4": 2, 

528 }, 

529 ) 

530 

531 def test_get_version(self): 

532 self.assertEqual( 

533 str(run_command(self.cmd_name, "--version")).strip(), django.get_version() 

534 ) 

535 self.assertEqual( 

536 str(run_command(self.cmd_name, "--version", "6", self.cmd_name)).strip(), 

537 django.get_version(), 

538 ) 

539 

540 def test_call_direct(self): 

541 cmd = get_command(self.cmd_name) 

542 

543 self.assertEqual( 

544 json.loads(cmd(arg1="a1", arg2="a2", arg3=0.2)), 

545 {"arg1": "a1", "arg2": "a2", "arg3": 0.2, "arg4": 1}, 

546 ) 

547 

548 

549class Callback2Tests(CallbackTests): 

550 cmd_name = "callback2" 

551 

552 def test_call_command(self): 

553 super().test_call_command(should_raise=False) 

554 

555 def test_helps(self, top_level_only=False): 

556 # we only run the top level help comparison because when 

557 # interspersed args are allowed its impossible to get the 

558 # subcommand to print its help 

559 super().test_helps(top_level_only=True) 

560 

561 

562class TestDjangoParameters(TestCase): 

563 commands = [ 

564 ("dj_params1", []), 

565 ("dj_params2", ["cmd1"]), 

566 ("dj_params2", ["cmd2"]), 

567 ("dj_params3", ["cmd1"]), 

568 ("dj_params3", ["cmd2"]), 

569 ("dj_params4", []), 

570 ] 

571 

572 def test_settings(self): 

573 for cmd, args in self.commands: 

574 run_command(cmd, "--settings", "django_typer.tests.settings2", *args) 

575 self.assertEqual(read_django_parameters().get("settings", None), 2) 

576 

577 def test_color_params(self): 

578 for cmd, args in self.commands: 

579 run_command(cmd, "--no-color", *args) 

580 self.assertEqual(read_django_parameters().get("no_color", False), True) 

581 run_command(cmd, "--force-color", *args) 

582 self.assertEqual(read_django_parameters().get("no_color", True), False) 

583 

584 result = run_command(cmd, "--force-color", "--no-color", *args) 

585 self.assertTrue("CommandError" in result) 

586 self.assertTrue("--no-color" in result) 

587 self.assertTrue("--force-color" in result) 

588 

589 call_command(cmd, args, no_color=True) 

590 self.assertEqual(read_django_parameters().get("no_color", False), True) 

591 call_command(cmd, args, force_color=True) 

592 self.assertEqual(read_django_parameters().get("no_color", True), False) 

593 with self.assertRaises(BaseException): 

594 call_command(cmd, args, force_color=True, no_color=True) 

595 

596 def test_pythonpath(self): 

597 added = str(Path(__file__).parent.absolute()) 

598 self.assertTrue(added not in sys.path) 

599 for cmd, args in self.commands: 

600 run_command(cmd, "--pythonpath", added, *args) 

601 self.assertTrue(added in read_django_parameters().get("python_path", [])) 

602 

603 def test_skip_checks(self): 

604 for cmd, args in self.commands: 

605 result = run_command( 

606 cmd, "--settings", "django_typer.tests.settings_fail_check", *args 

607 ) 

608 self.assertTrue("SystemCheckError" in result) 

609 self.assertTrue("test_app.E001" in result) 

610 

611 result = run_command( 

612 cmd, 

613 "--skip-checks", 

614 "--settings", 

615 "django_typer.tests.settings_fail_check", 

616 *args, 

617 ) 

618 self.assertFalse("SystemCheckError" in result) 

619 self.assertFalse("test_app.E001" in result) 

620 

621 @override_settings(DJANGO_TYPER_FAIL_CHECK=True) 

622 def test_skip_checks_call(self): 

623 for cmd, args in self.commands: 

624 from django.core.management.base import SystemCheckError 

625 

626 with self.assertRaises(SystemCheckError): 

627 call_command(cmd, *args, skip_checks=False) 

628 

629 # when you call_command and don't supply skip_checks, it will default to True! 

630 call_command(cmd, *args, skip_checks=True) 

631 call_command(cmd, *args) 

632 

633 def test_traceback(self): 

634 # traceback does not come into play with call_command 

635 for cmd, args in self.commands: 

636 result = run_command(cmd, *args, "--throw") 

637 if cmd != "dj_params4": 

638 self.assertFalse("Traceback" in result) 

639 else: 

640 self.assertTrue("Traceback" in result) 

641 

642 if cmd != "dj_params4": 

643 result_tb = run_command(cmd, "--traceback", *args, "--throw") 

644 self.assertTrue("Traceback" in result_tb) 

645 else: 

646 result_tb = run_command(cmd, "--no-traceback", *args, "--throw") 

647 self.assertFalse("Traceback" in result_tb) 

648 

649 def test_verbosity(self): 

650 run_command("dj_params3", "cmd1") 

651 self.assertEqual(read_django_parameters().get("verbosity", None), 1) 

652 

653 call_command("dj_params3", ["cmd1"]) 

654 self.assertEqual(read_django_parameters().get("verbosity", None), 1) 

655 

656 run_command("dj_params3", "--verbosity", "2", "cmd1") 

657 self.assertEqual(read_django_parameters().get("verbosity", None), 2) 

658 

659 call_command("dj_params3", ["cmd1"], verbosity=2) 

660 self.assertEqual(read_django_parameters().get("verbosity", None), 2) 

661 

662 run_command("dj_params3", "--verbosity", "0", "cmd2") 

663 self.assertEqual(read_django_parameters().get("verbosity", None), 0) 

664 

665 call_command("dj_params3", ["cmd2"], verbosity=0) 

666 self.assertEqual(read_django_parameters().get("verbosity", None), 0) 

667 

668 run_command("dj_params4") 

669 self.assertEqual(read_django_parameters().get("verbosity", None), 1) 

670 

671 call_command("dj_params4") 

672 self.assertEqual(read_django_parameters().get("verbosity", None), 1) 

673 

674 run_command("dj_params4", "--verbosity", "2") 

675 self.assertEqual(read_django_parameters().get("verbosity", None), 2) 

676 

677 call_command("dj_params4", [], verbosity=2) 

678 self.assertEqual(read_django_parameters().get("verbosity", None), 2) 

679 

680 run_command("dj_params4", "--verbosity", "0") 

681 self.assertEqual(read_django_parameters().get("verbosity", None), 0) 

682 

683 call_command("dj_params4", [], verbosity=0) 

684 self.assertEqual(read_django_parameters().get("verbosity", None), 0) 

685 

686 

687class TestHelpPrecedence(TestCase): 

688 def test_help_precedence1(self): 

689 buffer = StringIO() 

690 cmd = get_command("help_precedence1", stdout=buffer) 

691 cmd.print_help("./manage.py", "help_precedence1") 

692 self.assertTrue( 

693 re.search( 

694 r"help_precedence1\s+Test minimal TyperCommand subclass - command method", 

695 buffer.getvalue(), 

696 ) 

697 ) 

698 self.assertIn( 

699 "Test minimal TyperCommand subclass - typer param", buffer.getvalue() 

700 ) 

701 

702 def test_help_precedence2(self): 

703 buffer = StringIO() 

704 cmd = get_command("help_precedence2", stdout=buffer) 

705 cmd.print_help("./manage.py", "help_precedence2") 

706 self.assertIn( 

707 "Test minimal TyperCommand subclass - class member", buffer.getvalue() 

708 ) 

709 self.assertTrue( 

710 re.search( 

711 r"help_precedence2\s+Test minimal TyperCommand subclass - command method", 

712 buffer.getvalue(), 

713 ) 

714 ) 

715 

716 def test_help_precedence3(self): 

717 buffer = StringIO() 

718 cmd = get_command("help_precedence3", stdout=buffer) 

719 cmd.print_help("./manage.py", "help_precedence3") 

720 self.assertTrue( 

721 re.search( 

722 r"help_precedence3\s+Test minimal TyperCommand subclass - command method", 

723 buffer.getvalue(), 

724 ) 

725 ) 

726 self.assertIn( 

727 "Test minimal TyperCommand subclass - callback method", buffer.getvalue() 

728 ) 

729 

730 def test_help_precedence4(self): 

731 buffer = StringIO() 

732 cmd = get_command("help_precedence4", stdout=buffer) 

733 cmd.print_help("./manage.py", "help_precedence4") 

734 self.assertIn( 

735 "Test minimal TyperCommand subclass - callback docstring", buffer.getvalue() 

736 ) 

737 self.assertTrue( 

738 re.search( 

739 r"help_precedence4\s+Test minimal TyperCommand subclass - command method", 

740 buffer.getvalue(), 

741 ) 

742 ) 

743 

744 def test_help_precedence5(self): 

745 buffer = StringIO() 

746 cmd = get_command("help_precedence5", stdout=buffer) 

747 cmd.print_help("./manage.py", "help_precedence5") 

748 self.assertIn( 

749 "Test minimal TyperCommand subclass - command method", buffer.getvalue() 

750 ) 

751 

752 def test_help_precedence6(self): 

753 buffer = StringIO() 

754 cmd = get_command("help_precedence6", stdout=buffer) 

755 cmd.print_help("./manage.py", "help_precedence6") 

756 self.assertIn( 

757 "Test minimal TyperCommand subclass - docstring", buffer.getvalue() 

758 ) 

759 

760 

761class TestGroups(TestCase): 

762 """ 

763 A collection of tests that test complex grouping commands and also that 

764 command inheritance behaves as expected. 

765 """ 

766 

767 def test_group_call(self): 

768 with self.assertRaises(NotImplementedError): 

769 get_command("groups")() 

770 

771 @override_settings( 

772 INSTALLED_APPS=[ 

773 "django_typer.tests.test_app", 

774 "django.contrib.admin", 

775 "django.contrib.auth", 

776 "django.contrib.contenttypes", 

777 "django.contrib.sessions", 

778 "django.contrib.messages", 

779 "django.contrib.staticfiles", 

780 ], 

781 ) 

782 def test_helps(self, app="test_app"): 

783 for cmds in [ 

784 ("groups",), 

785 ("groups", "echo"), 

786 ("groups", "math"), 

787 ("groups", "math", "divide"), 

788 ("groups", "math", "multiply"), 

789 ("groups", "string"), 

790 ("groups", "string", "case"), 

791 ("groups", "string", "case", "lower"), 

792 ("groups", "string", "case", "upper"), 

793 ("groups", "string", "strip"), 

794 ("groups", "string", "split"), 

795 ("groups", "setting"), 

796 ("groups", "setting", "print"), 

797 ]: 

798 if app == "test_app" and cmds[-1] in ["strip", "setting", "print"]: 798 ↛ 799line 798 didn't jump to line 799, because the condition on line 798 was never true

799 with self.assertRaises(ValueError): 

800 cmd = get_command(cmds[0], stdout=buffer) 

801 cmd.print_help("./manage.py", *cmds) 

802 continue 

803 

804 buffer = StringIO() 

805 cmd = get_command(cmds[0], stdout=buffer) 

806 cmd.print_help("./manage.py", *cmds) 

807 hlp = buffer.getvalue() 

808 self.assertGreater( 

809 sim := similarity( 

810 hlp, (TESTS_DIR / app / "helps" / f"{cmds[-1]}.txt").read_text() 

811 ), 

812 0.96, # width inconsistences drive this number < 1 

813 ) 

814 print(f'{app}: {" ".join(cmds)} = {sim:.2f}') 

815 

816 @override_settings( 

817 INSTALLED_APPS=[ 

818 "django_typer.tests.test_app2", 

819 "django_typer.tests.test_app", 

820 "django.contrib.admin", 

821 "django.contrib.auth", 

822 "django.contrib.contenttypes", 

823 "django.contrib.sessions", 

824 "django.contrib.messages", 

825 "django.contrib.staticfiles", 

826 ], 

827 ) 

828 def test_helps_override(self): 

829 self.test_helps.__wrapped__(self, app="test_app2") 

830 

831 @override_settings( 

832 INSTALLED_APPS=[ 

833 "django_typer.tests.test_app", 

834 "django.contrib.admin", 

835 "django.contrib.auth", 

836 "django.contrib.contenttypes", 

837 "django.contrib.sessions", 

838 "django.contrib.messages", 

839 "django.contrib.staticfiles", 

840 ], 

841 ) 

842 def test_command_line(self, settings=None): 

843 override = settings is not None 

844 settings = ("--settings", settings) if settings else [] 

845 

846 self.assertEqual( 

847 run_command("groups", *settings, "echo", "hey!").strip(), 

848 "hey!", 

849 ) 

850 

851 self.assertEqual( 

852 call_command("groups", "echo", "hey!").strip(), 

853 "hey!", 

854 ) 

855 self.assertEqual( 

856 get_command("groups", "echo")("hey!").strip(), 

857 "hey!", 

858 ) 

859 self.assertEqual( 

860 get_command("groups", "echo")(message="hey!").strip(), 

861 "hey!", 

862 ) 

863 

864 self.assertEqual(get_command("groups").echo("hey!").strip(), "hey!") 

865 

866 self.assertEqual(get_command("groups").echo(message="hey!").strip(), "hey!") 

867 

868 result = run_command("groups", *settings, "echo", "hey!", "5") 

869 if override: 

870 self.assertEqual(result.strip(), ("hey! " * 5).strip()) 

871 self.assertEqual( 

872 get_command("groups").echo("hey!", 5).strip(), ("hey! " * 5).strip() 

873 ) 

874 self.assertEqual( 

875 get_command("groups").echo(message="hey!", echoes=5).strip(), 

876 ("hey! " * 5).strip(), 

877 ) 

878 self.assertEqual( 

879 call_command("groups", "echo", "hey!", "5").strip(), 

880 ("hey! " * 5).strip(), 

881 ) 

882 self.assertEqual( 

883 call_command("groups", "echo", "hey!", echoes=5).strip(), 

884 ("hey! " * 5).strip(), 

885 ) 

886 self.assertEqual( 

887 call_command("groups", "echo", "hey!", echoes="5").strip(), 

888 ("hey! " * 5).strip(), 

889 ) 

890 else: 

891 self.assertIn("UsageError", result) 

892 with self.assertRaises(TypeError): 

893 call_command("groups", "echo", "hey!", echoes=5) 

894 with self.assertRaises(TypeError): 

895 get_command("groups").echo(message="hey!", echoes=5) 

896 

897 self.assertEqual( 

898 run_command( 

899 "groups", 

900 *settings, 

901 "math", 

902 "--precision", 

903 "5", 

904 "multiply", 

905 "1.2", 

906 "3.5", 

907 " -12.3", 

908 parse_json=False, 

909 ).strip(), 

910 "-51.66000", 

911 ) 

912 

913 grp_cmd = get_command("groups") 

914 grp_cmd.math(precision=5) 

915 self.assertEqual(grp_cmd.multiply(1.2, 3.5, [-12.3]), "-51.66000") 

916 

917 self.assertEqual( 

918 call_command( 

919 "groups", "math", "multiply", "1.2", "3.5", " -12.3", precision=5 

920 ), 

921 "-51.66000", 

922 ) 

923 

924 self.assertEqual( 

925 call_command( 

926 "groups", "math", "multiply", "1.2", "3.5", " -12.3", precision="5" 

927 ), 

928 "-51.66000", 

929 ) 

930 

931 self.assertEqual( 

932 run_command( 

933 "groups", 

934 *settings, 

935 "math", 

936 "divide", 

937 "1.2", 

938 "3.5", 

939 " -12.3", 

940 parse_json=False, 

941 ).strip(), 

942 "-0.03", 

943 ) 

944 

945 self.assertEqual( 

946 call_command( 

947 "groups", 

948 "math", 

949 "divide", 

950 "1.2", 

951 "3.5", 

952 " -12.3", 

953 ), 

954 "-0.03", 

955 ) 

956 

957 self.assertEqual(get_command("groups").divide(1.2, 3.5, [-12.3]), "-0.03") 

958 self.assertEqual( 

959 get_command("groups", "math", "divide")(1.2, 3.5, [-12.3]), "-0.03" 

960 ) 

961 

962 self.assertEqual( 

963 run_command( 

964 "groups", *settings, "string", "ANNAmontes", "case", "lower" 

965 ).strip(), 

966 "annamontes", 

967 ) 

968 

969 self.assertEqual( 

970 call_command("groups", "string", "ANNAmontes", "case", "lower"), 

971 "annamontes", 

972 ) 

973 

974 grp_cmd = get_command("groups") 

975 grp_cmd.string("ANNAmontes") 

976 self.assertEqual(grp_cmd.lower(), "annamontes") 

977 

978 self.assertEqual( 

979 run_command( 

980 "groups", *settings, "string", "annaMONTES", "case", "upper" 

981 ).strip(), 

982 "ANNAMONTES", 

983 ) 

984 

985 grp_cmd.string("annaMONTES") 

986 self.assertEqual(grp_cmd.upper(), "ANNAMONTES") 

987 

988 self.assertEqual( 

989 run_command( 

990 "groups", 

991 *settings, 

992 "string", 

993 "ANNAMONTES", 

994 "case", 

995 "lower", 

996 "--begin", 

997 "4", 

998 "--end", 

999 "9", 

1000 ).strip(), 

1001 "ANNAmonteS", 

1002 ) 

1003 

1004 self.assertEqual( 

1005 call_command( 

1006 "groups", 

1007 "string", 

1008 "ANNAMONTES", 

1009 "case", 

1010 "lower", 

1011 "--begin", 

1012 "4", 

1013 "--end", 

1014 "9", 

1015 ).strip(), 

1016 "ANNAmonteS", 

1017 ) 

1018 

1019 self.assertEqual( 

1020 call_command( 

1021 "groups", "string", "ANNAMONTES", "case", "lower", begin=4, end=9 

1022 ).strip(), 

1023 "ANNAmonteS", 

1024 ) 

1025 

1026 grp_cmd.string("ANNAMONTES") 

1027 self.assertEqual(grp_cmd.lower(begin=4, end=9), "ANNAmonteS") 

1028 grp_cmd.string("ANNAMONTES") 

1029 self.assertEqual(grp_cmd.lower(4, 9), "ANNAmonteS") 

1030 

1031 result = run_command( 

1032 "groups", *settings, "string", "annamontes", "case", "upper", "4", "9" 

1033 ).strip() 

1034 if override: 

1035 self.assertIn("UsageError", result) 

1036 grp_cmd.string("annamontes") 

1037 with self.assertRaises(TypeError): 

1038 self.assertEqual(grp_cmd.upper(4, 9), "annaMONTEs") 

1039 

1040 with self.assertRaises(UsageError): 

1041 self.assertEqual( 

1042 call_command( 

1043 "groups", "string", "annamontes", "case", "upper", "4", "9" 

1044 ).strip(), 

1045 "annaMONTEs", 

1046 ) 

1047 else: 

1048 self.assertEqual(result, "annaMONTEs") 

1049 grp_cmd.string("annamontes") 

1050 self.assertEqual(grp_cmd.upper(4, 9), "annaMONTEs") 

1051 self.assertEqual( 

1052 call_command( 

1053 "groups", "string", "annamontes", "case", "upper", "4", "9" 

1054 ).strip(), 

1055 "annaMONTEs", 

1056 ) 

1057 

1058 result = run_command( 

1059 "groups", *settings, "string", " emmatc ", "strip", parse_json=False 

1060 ) 

1061 if override: 

1062 self.assertEqual(result, "emmatc\n") 

1063 self.assertEqual( 

1064 call_command("groups", "string", " emmatc ", "strip"), "emmatc" 

1065 ) 

1066 grp_cmd.string(" emmatc ") 

1067 self.assertEqual(grp_cmd.strip(), "emmatc") 

1068 else: 

1069 self.assertIn("UsageError", result) 

1070 with self.assertRaises(UsageError): 

1071 self.assertEqual( 

1072 call_command("groups", "string", " emmatc ", "strip"), "emmatc" 

1073 ) 

1074 with self.assertRaises(AttributeError): 

1075 grp_cmd.string(" emmatc ") 

1076 self.assertEqual(grp_cmd.strip(), "emmatc") 

1077 

1078 self.assertEqual( 

1079 run_command( 

1080 "groups", *settings, "string", "c,a,i,t,l,y,n", "split", "--sep", "," 

1081 ).strip(), 

1082 "c a i t l y n", 

1083 ) 

1084 self.assertEqual( 

1085 call_command( 

1086 "groups", "string", "c,a,i,t,l,y,n", "split", "--sep", "," 

1087 ).strip(), 

1088 "c a i t l y n", 

1089 ) 

1090 self.assertEqual( 

1091 call_command("groups", "string", "c,a,i,t,l,y,n", "split", sep=",").strip(), 

1092 "c a i t l y n", 

1093 ) 

1094 grp_cmd.string("c,a,i,t,l,y,n") 

1095 self.assertEqual(grp_cmd.split(sep=","), "c a i t l y n") 

1096 grp_cmd.string("c,a,i,t,l,y,n") 

1097 self.assertEqual(grp_cmd.split(","), "c a i t l y n") 

1098 

1099 @override_settings( 

1100 INSTALLED_APPS=[ 

1101 "django_typer.tests.test_app2", 

1102 "django_typer.tests.test_app", 

1103 "django.contrib.admin", 

1104 "django.contrib.auth", 

1105 "django.contrib.contenttypes", 

1106 "django.contrib.sessions", 

1107 "django.contrib.messages", 

1108 "django.contrib.staticfiles", 

1109 ], 

1110 ) 

1111 def test_command_line_override(self): 

1112 self.test_command_line.__wrapped__(self, settings="django_typer.tests.override") 

1113 

1114 

1115class TestCallCommandArgs(TestCase): 

1116 @override_settings( 

1117 INSTALLED_APPS=[ 

1118 "django_typer.tests.test_app2", 

1119 "django_typer.tests.test_app", 

1120 "django.contrib.admin", 

1121 "django.contrib.auth", 

1122 "django.contrib.contenttypes", 

1123 "django.contrib.sessions", 

1124 "django.contrib.messages", 

1125 "django.contrib.staticfiles", 

1126 ], 

1127 ) 

1128 def test_completion_args(self): 

1129 # call_command converts all args to strings - todo - fix this? or accept it? fixing it 

1130 # would require a monkey patch. I think accepting it and trying to allow Arguments to 

1131 # be passed in as named parameters would be a good compromise. Users can always invoke 

1132 # the typer commands directly using () or the functions directly. 

1133 

1134 # if autocompletion ends up requiring a monkey patch, consider fixing it 

1135 

1136 # turns out call_command will also turn options values into strings you've flagged them as required 

1137 # and they're passed in as named parameters 

1138 

1139 test_app = apps.get_app_config("django_typer_tests_test_app") 

1140 test_app2 = apps.get_app_config("django_typer_tests_test_app2") 

1141 

1142 out = StringIO() 

1143 call_command( 

1144 "completion", 

1145 ["django_typer_tests_test_app", "django_typer_tests_test_app2"], 

1146 stdout=out, 

1147 ) 

1148 printed_options = json.loads(out.getvalue()) 

1149 self.assertEqual( 

1150 printed_options, 

1151 ["django_typer_tests_test_app", "django_typer_tests_test_app2"], 

1152 ) 

1153 

1154 out = StringIO() 

1155 printed_options = json.loads(get_command("completion")([test_app, test_app2])) 

1156 self.assertEqual( 

1157 printed_options, 

1158 ["django_typer_tests_test_app", "django_typer_tests_test_app2"], 

1159 ) 

1160 

1161 

1162class TestTracebackConfig(TestCase): 

1163 rich_installed = True 

1164 

1165 def test_default_traceback(self): 

1166 result = run_command("test_command1", "delete", "me", "--throw") 

1167 self.assertIn("Traceback (most recent call last)", result) 

1168 self.assertIn("Exception: This is a test exception", result) 

1169 if self.rich_installed: 

1170 self.assertIn("────────", result) 

1171 # locals should be present 

1172 self.assertIn("name = 'me'", result) 

1173 self.assertIn("throw = True", result) 

1174 # by default we get only the last frame 

1175 self.assertEqual(len(re.findall(r"\.py:\d+", result) or []), 1) 

1176 else: 

1177 self.assertNotIn("────────", result) 

1178 

1179 def test_tb_command_overrides(self): 

1180 result = run_command("test_tb_overrides", "delete", "me", "--throw") 

1181 self.assertIn("Traceback (most recent call last)", result) 

1182 self.assertIn("Exception: This is a test exception", result) 

1183 if self.rich_installed: 

1184 self.assertIn("────────", result) 

1185 # locals should be present 

1186 self.assertNotIn("name = 'me'", result) 

1187 self.assertNotIn("throw = True", result) 

1188 # should get a full length stack trace 

1189 self.assertGreater(len(re.findall(r"\.py:\d+", result) or []), 1) 

1190 else: 

1191 self.assertNotIn("────────", result) 

1192 

1193 def test_turn_traceback_off_false(self): 

1194 result = run_command( 

1195 "test_command1", 

1196 "--settings", 

1197 "django_typer.tests.settings_tb_false", 

1198 "delete", 

1199 "me", 

1200 "--throw", 

1201 ) 

1202 self.assertNotIn("────────", result) 

1203 self.assertIn("Traceback (most recent call last)", result) 

1204 self.assertIn("Exception: This is a test exception", result) 

1205 

1206 def test_turn_traceback_off_none(self): 

1207 result = run_command( 

1208 "test_command1", 

1209 "--settings", 

1210 "django_typer.tests.settings_tb_none", 

1211 "delete", 

1212 "me", 

1213 "--throw", 

1214 ) 

1215 self.assertNotIn("────────", result) 

1216 self.assertIn("Traceback (most recent call last)", result) 

1217 self.assertIn("Exception: This is a test exception", result) 

1218 

1219 def test_traceback_no_locals_short_false(self): 

1220 result = run_command( 

1221 "test_command1", 

1222 "--settings", 

1223 "django_typer.tests.settings_tb_change_defaults", 

1224 "delete", 

1225 "me", 

1226 "--throw", 

1227 ) 

1228 self.assertIn("Traceback (most recent call last)", result) 

1229 self.assertIn("Exception: This is a test exception", result) 

1230 # locals should not be present 

1231 self.assertNotIn("name = 'me'", result) 

1232 self.assertNotIn("throw = True", result) 

1233 if self.rich_installed: 

1234 self.assertIn("────────", result) 

1235 self.assertGreater(len(re.findall(r"\.py:\d+", result) or []), 1) 

1236 else: 

1237 self.assertNotIn("────────", result) 

1238 

1239 def test_rich_install(self): 

1240 if self.rich_installed: 

1241 result = run_command( 

1242 "test_command1", 

1243 "--settings", 

1244 "django_typer.tests.settings_throw_init_exception", 

1245 "delete", 

1246 "me", 

1247 ) 

1248 self.assertIn("Traceback (most recent call last)", result) 

1249 self.assertIn("Exception: Test ready exception", result) 

1250 self.assertIn("────────", result) 

1251 self.assertIn("── locals ──", result) 

1252 

1253 @override_settings(DJ_RICH_TRACEBACK_CONFIG={"no_install": True}) 

1254 def test_tb_no_install(self): 

1255 if self.rich_installed: 

1256 result = run_command( 

1257 "test_command1", 

1258 "--settings", 

1259 "django_typer.tests.settings_tb_no_install", 

1260 "delete", 

1261 "me", 

1262 ) 

1263 self.assertIn("Traceback (most recent call last)", result) 

1264 self.assertIn("Exception: Test ready exception", result) 

1265 self.assertNotIn("────────", result) 

1266 self.assertNotIn("── locals ──", result) 

1267 

1268 

1269class TestTracebackConfigNoRich(TestTracebackConfig): 

1270 rich_installed = False 

1271 

1272 def setUp(self): 

1273 subprocess.run(["pip", "uninstall", "-y", "rich"], check=True) 

1274 

1275 def tearDown(self): 

1276 subprocess.run(["pip", "install", "rich"], check=True) 

1277 

1278 

1279class TestSettingsSystemCheck(TestCase): 

1280 def test_warning_thrown(self): 

1281 result = run_command( 

1282 "noop", "--settings", "django_typer.tests.settings_tb_bad_config" 

1283 ) 

1284 self.assertIn( 

1285 "django_typer.tests.settings_tb_bad_config: (django_typer.W001) DT_RICH_TRACEBACK_CONFIG", 

1286 result, 

1287 ) 

1288 self.assertIn( 

1289 "HINT: Unexpected parameters encountered: unexpected_setting.", result 

1290 )