1313
1414import json
1515import 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
1621import tempfile
1722import unittest
1823from pathlib import Path
2126from click .testing import CliRunner
2227
2328from 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" ]
2432from 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