Skip to content

Commit 64f2d22

Browse files
liuxiaotongliuxiaotong
andauthored
fix(tests): resolve Python 3.10 mock path shadowing (#7)
* fix(tests): resolve Python 3.10 mock path shadowing Click command 'analyze' imported into datarecipe.cli namespace shadows the module name. Use sys.modules + patch.object to reference the real module, fixing AttributeError on all Python versions. * style: fix ruff import sorting --------- Co-authored-by: liuxiaotong <liuxiaotong@knowlyr.com>
1 parent 5531f7e commit 64f2d22

File tree

2 files changed

+189
-166
lines changed

2 files changed

+189
-166
lines changed

tests/test_cli_commands.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313

1414
import json
1515
import os
16+
17+
# Use sys.modules to get the actual module objects, bypassing the namespace
18+
# shadowing caused by `from datarecipe.cli.analyze import analyze` in __init__.py
19+
# which replaces the module with the Click Command object on Python 3.10.
20+
import sys
1621
import tempfile
1722
import unittest
1823
from pathlib import Path
@@ -21,6 +26,9 @@
2126
from click.testing import CliRunner
2227

2328
from datarecipe.cli import main, recipe_to_markdown, validate_output_path
29+
30+
_cli_analyze_mod = sys.modules["datarecipe.cli.analyze"]
31+
_cli_tools_mod = sys.modules["datarecipe.cli.tools"]
2432
from datarecipe.schema import (
2533
Cost,
2634
GenerationMethod,
@@ -221,6 +229,7 @@ def test_normal_path(self):
221229

222230
def test_relative_resolves_to_absolute(self):
223231
import os
232+
224233
old_cwd = os.getcwd()
225234
try:
226235
os.chdir("/tmp")
@@ -311,9 +320,7 @@ def test_no_ratios(self):
311320
self.assertIn("test/dataset", md)
312321

313322
def test_low_confidence_cost_range(self):
314-
recipe = _make_recipe(
315-
cost=Cost(estimated_total_usd=1000.0, confidence="low")
316-
)
323+
recipe = _make_recipe(cost=Cost(estimated_total_usd=1000.0, confidence="low"))
317324
md = recipe_to_markdown(recipe)
318325
# Low confidence shows a range: $500 - $1,500
319326
self.assertIn("500", md)
@@ -366,39 +373,39 @@ def setUp(self):
366373
self.runner = CliRunner()
367374
self.recipe = _make_recipe()
368375

369-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
376+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
370377
def test_analyze_default_output(self, MockAnalyzer):
371378
mock_instance = MockAnalyzer.return_value
372379
mock_instance.analyze.return_value = self.recipe
373380
result = self.runner.invoke(main, ["analyze", "test/dataset"])
374381
self.assertEqual(result.exit_code, 0)
375382
mock_instance.analyze.assert_called_once_with("test/dataset")
376383

377-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
384+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
378385
def test_analyze_json_flag(self, MockAnalyzer):
379386
mock_instance = MockAnalyzer.return_value
380387
mock_instance.analyze.return_value = self.recipe
381388
result = self.runner.invoke(main, ["analyze", "test/dataset", "--json"])
382389
self.assertEqual(result.exit_code, 0)
383390
self.assertIn('"name"', result.output)
384391

385-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
392+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
386393
def test_analyze_yaml_flag(self, MockAnalyzer):
387394
mock_instance = MockAnalyzer.return_value
388395
mock_instance.analyze.return_value = self.recipe
389396
result = self.runner.invoke(main, ["analyze", "test/dataset", "--yaml"])
390397
self.assertEqual(result.exit_code, 0)
391398
self.assertIn("name:", result.output)
392399

393-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
400+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
394401
def test_analyze_markdown_flag(self, MockAnalyzer):
395402
mock_instance = MockAnalyzer.return_value
396403
mock_instance.analyze.return_value = self.recipe
397404
result = self.runner.invoke(main, ["analyze", "test/dataset", "--markdown"])
398405
self.assertEqual(result.exit_code, 0)
399406
self.assertIn("test/dataset", result.output)
400407

401-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
408+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
402409
def test_analyze_output_json_file(self, MockAnalyzer):
403410
mock_instance = MockAnalyzer.return_value
404411
mock_instance.analyze.return_value = self.recipe
@@ -410,7 +417,7 @@ def test_analyze_output_json_file(self, MockAnalyzer):
410417
data = json.loads(Path(outpath).read_text())
411418
self.assertEqual(data["name"], "test/dataset")
412419

413-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
420+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
414421
def test_analyze_output_md_file(self, MockAnalyzer):
415422
mock_instance = MockAnalyzer.return_value
416423
mock_instance.analyze.return_value = self.recipe
@@ -422,15 +429,15 @@ def test_analyze_output_md_file(self, MockAnalyzer):
422429
content = Path(outpath).read_text()
423430
self.assertIn("test/dataset", content)
424431

425-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
432+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
426433
def test_analyze_value_error(self, MockAnalyzer):
427434
mock_instance = MockAnalyzer.return_value
428435
mock_instance.analyze.side_effect = ValueError("Dataset not found")
429436
result = self.runner.invoke(main, ["analyze", "bad/dataset"])
430437
self.assertNotEqual(result.exit_code, 0)
431438
self.assertIn("Error", result.output)
432439

433-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
440+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
434441
def test_analyze_generic_error(self, MockAnalyzer):
435442
mock_instance = MockAnalyzer.return_value
436443
mock_instance.analyze.side_effect = RuntimeError("Network error")
@@ -463,7 +470,7 @@ def test_show_missing_file(self):
463470
result = self.runner.invoke(main, ["show", "/nonexistent/recipe.yaml"])
464471
self.assertNotEqual(result.exit_code, 0)
465472

466-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
473+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
467474
def test_show_valid_file(self, MockAnalyzer):
468475
recipe = _make_recipe()
469476
mock_instance = MockAnalyzer.return_value
@@ -485,7 +492,7 @@ class TestExportCommand(unittest.TestCase):
485492
def setUp(self):
486493
self.runner = CliRunner()
487494

488-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
495+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
489496
def test_export_success(self, MockAnalyzer):
490497
recipe = _make_recipe()
491498
mock_instance = MockAnalyzer.return_value
@@ -496,7 +503,7 @@ def test_export_success(self, MockAnalyzer):
496503
self.assertEqual(result.exit_code, 0)
497504
mock_instance.export_recipe.assert_called_once()
498505

499-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
506+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
500507
def test_export_analyze_error(self, MockAnalyzer):
501508
mock_instance = MockAnalyzer.return_value
502509
mock_instance.analyze.side_effect = ValueError("not found")
@@ -520,7 +527,7 @@ def setUp(self):
520527
self.runner = CliRunner()
521528

522529
@patch("datarecipe.cost_calculator.CostCalculator")
523-
@patch("datarecipe.cli.tools.DatasetAnalyzer")
530+
@patch.object(_cli_tools_mod, "DatasetAnalyzer")
524531
def test_cost_default(self, MockAnalyzer, MockCalc):
525532
recipe = _make_recipe()
526533
MockAnalyzer.return_value.analyze.return_value = recipe
@@ -546,7 +553,7 @@ def test_cost_default(self, MockAnalyzer, MockCalc):
546553
self.assertIn("Cost", result.output)
547554

548555
@patch("datarecipe.cost_calculator.CostCalculator")
549-
@patch("datarecipe.cli.tools.DatasetAnalyzer")
556+
@patch.object(_cli_tools_mod, "DatasetAnalyzer")
550557
def test_cost_json_flag(self, MockAnalyzer, MockCalc):
551558
recipe = _make_recipe()
552559
MockAnalyzer.return_value.analyze.return_value = recipe
@@ -896,7 +903,7 @@ def setUp(self):
896903

897904
@patch("datarecipe.pipeline.pipeline_to_markdown")
898905
@patch("datarecipe.pipeline.get_pipeline_template")
899-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
906+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
900907
def test_guide_success(self, MockAnalyzer, MockTemplate, MockToMd):
901908
recipe = _make_recipe()
902909
MockAnalyzer.return_value.analyze.return_value = recipe
@@ -914,7 +921,7 @@ def test_guide_success(self, MockAnalyzer, MockTemplate, MockToMd):
914921

915922
@patch("datarecipe.pipeline.pipeline_to_markdown")
916923
@patch("datarecipe.pipeline.get_pipeline_template")
917-
@patch("datarecipe.cli.analyze.DatasetAnalyzer")
924+
@patch.object(_cli_analyze_mod, "DatasetAnalyzer")
918925
def test_guide_with_output(self, MockAnalyzer, MockTemplate, MockToMd):
919926
recipe = _make_recipe()
920927
MockAnalyzer.return_value.analyze.return_value = recipe
@@ -999,7 +1006,7 @@ def setUp(self):
9991006

10001007
@patch("datarecipe.profiler.profile_to_markdown")
10011008
@patch("datarecipe.profiler.AnnotatorProfiler")
1002-
@patch("datarecipe.cli.tools.DatasetAnalyzer")
1009+
@patch.object(_cli_tools_mod, "DatasetAnalyzer")
10031010
def test_profile_json_output(self, MockAnalyzer, MockProfiler, MockToMd):
10041011
recipe = _make_recipe()
10051012
MockAnalyzer.return_value.analyze.return_value = recipe
@@ -1014,7 +1021,7 @@ def test_profile_json_output(self, MockAnalyzer, MockProfiler, MockToMd):
10141021

10151022
@patch("datarecipe.profiler.profile_to_markdown")
10161023
@patch("datarecipe.profiler.AnnotatorProfiler")
1017-
@patch("datarecipe.cli.tools.DatasetAnalyzer")
1024+
@patch.object(_cli_tools_mod, "DatasetAnalyzer")
10181025
def test_profile_markdown_output(self, MockAnalyzer, MockProfiler, MockToMd):
10191026
recipe = _make_recipe()
10201027
MockAnalyzer.return_value.analyze.return_value = recipe
@@ -1066,9 +1073,7 @@ def setUp(self):
10661073
@patch("datarecipe.triggers.RadarWatcher")
10671074
@patch("datarecipe.triggers.TriggerConfig")
10681075
def test_watch_once_no_reports(self, MockConfig, MockWatcher):
1069-
mock_config = MagicMock(
1070-
orgs=[], categories=[], min_downloads=0
1071-
)
1076+
mock_config = MagicMock(orgs=[], categories=[], min_downloads=0)
10721077
MockConfig.return_value = mock_config
10731078

10741079
MockWatcher.return_value.check_once.return_value = []
@@ -1129,9 +1134,7 @@ def test_no_num_examples(self):
11291134
self.assertIn("test/dataset", md)
11301135

11311136
def test_high_confidence_cost(self):
1132-
recipe = _make_recipe(
1133-
cost=Cost(estimated_total_usd=2000.0, confidence="high")
1134-
)
1137+
recipe = _make_recipe(cost=Cost(estimated_total_usd=2000.0, confidence="high"))
11351138
md = recipe_to_markdown(recipe)
11361139
self.assertIn("2,000", md)
11371140

0 commit comments

Comments
 (0)