python-package-folder 8.2.0__tar.gz → 8.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/PKG-INFO +1 -1
  2. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/coverage.svg +2 -2
  3. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/pyproject.toml +1 -1
  4. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/analyzer.py +64 -5
  5. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/manager.py +232 -19
  6. python_package_folder-8.4.0/tests/test_subfolder_build.py +3554 -0
  7. python_package_folder-8.2.0/tests/test_subfolder_build.py +0 -1912
  8. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.copier-answers.yml +0 -0
  9. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  10. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  11. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.cursor/rules/general.mdc +0 -0
  12. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.cursor/rules/python.mdc +0 -0
  13. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.github/workflows/ci.yml +0 -0
  14. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.github/workflows/publish.yml +0 -0
  15. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.gitignore +0 -0
  16. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/.vscode/settings.json +0 -0
  17. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/LICENSE +0 -0
  18. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/MANIFEST.in +0 -0
  19. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/Makefile +0 -0
  20. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/README.md +0 -0
  21. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/development.md +0 -0
  22. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/docs/DEVELOPMENT.md +0 -0
  23. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/docs/INSTALLATION.md +0 -0
  24. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/docs/PUBLISHING.md +0 -0
  25. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/docs/REFERENCE.md +0 -0
  26. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/docs/USAGE.md +0 -0
  27. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/docs/VERSION_RESOLUTION.md +0 -0
  28. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/installation.md +0 -0
  29. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/publishing.md +0 -0
  30. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/__init__.py +0 -0
  31. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/__main__.py +0 -0
  32. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/finder.py +0 -0
  33. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/publisher.py +0 -0
  34. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/py.typed +0 -0
  35. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/python_package_folder.py +0 -0
  36. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/subfolder_build.py +0 -0
  37. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/types.py +0 -0
  38. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/utils.py +0 -0
  39. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/src/python_package_folder/version_calculator.py +0 -0
  41. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/conftest.py +0 -0
  42. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/some_globals.py +0 -0
  43. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  44. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  45. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  46. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  47. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  48. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  49. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_build_with_external_deps.py +0 -0
  50. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_exclude_patterns.py +0 -0
  51. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_linting.py +0 -0
  52. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_preserve_directory_structure.py +0 -0
  53. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_publisher.py +0 -0
  54. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_shared_subdirectory_imports.py +0 -0
  55. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  56. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_utils.py +0 -0
  58. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_version_calculator.py +0 -0
  59. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/test_version_manager.py +0 -0
  60. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/tests/tests.py +0 -0
  61. {python_package_folder-8.2.0 → python_package_folder-8.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 8.2.0
3
+ Version: 8.4.0
4
4
  Summary: Python package to automatically package and build a folder, fetching all relevant dependencies.
5
5
  Project-URL: Repository, https://github.com/alelom/python-package-folder
6
6
  Author-email: Alessio Lombardi <work@alelom.com>
@@ -14,7 +14,7 @@
14
14
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
15
15
  <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
16
16
  <text x="31.5" y="14">coverage</text>
17
- <text x="81" y="15" fill="#010101" fill-opacity=".3">67%</text>
18
- <text x="81" y="14">67%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">66%</text>
18
+ <text x="81" y="14">66%</text>
19
19
  </g>
20
20
  </svg>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "8.2.0"
46
+ version = "8.4.0"
47
47
  [dependency-groups]
48
48
  dev = [
49
49
  "pytest>=8.3.5",
@@ -396,7 +396,9 @@ class ImportAnalyzer:
396
396
  Check if a module is a third-party package.
397
397
 
398
398
  Uses importlib to find the module and checks if its location
399
- is in site-packages or dist-packages.
399
+ is in site-packages or dist-packages. Also tries to import the module
400
+ to check if it's available, which helps catch cases where packages
401
+ are installed but metadata lookup fails.
400
402
 
401
403
  Args:
402
404
  module_name: Name of the module to check
@@ -405,12 +407,69 @@ class ImportAnalyzer:
405
407
  True if the module is a third-party package, False otherwise
406
408
  """
407
409
  root_module = module_name.split(".")[0]
410
+
411
+ # Skip if already known as stdlib
412
+ stdlib_modules = self.get_stdlib_modules()
413
+ if root_module in stdlib_modules:
414
+ return False
415
+
408
416
  try:
409
417
  spec = importlib.util.find_spec(root_module)
410
418
  if spec and spec.origin:
411
419
  origin_path = Path(spec.origin)
412
- # Check if it's in site-packages
413
- return "site-packages" in str(origin_path) or "dist-packages" in str(origin_path)
414
- except (ImportError, ValueError, AttributeError):
415
- pass
420
+ origin_str = str(origin_path)
421
+
422
+ # Check if it's in site-packages or dist-packages
423
+ if "site-packages" in origin_str or "dist-packages" in origin_str:
424
+ return True
425
+
426
+ # Check if it's outside the standard library directory
427
+ # Standard library is typically in the Python installation directory
428
+ # and not in site-packages
429
+ stdlib_paths = [
430
+ "lib/python",
431
+ "Lib\\python", # Windows
432
+ "lib64/python",
433
+ "Lib64\\python", # Windows
434
+ ]
435
+ is_stdlib_path = any(stdlib_path in origin_str for stdlib_path in stdlib_paths)
436
+ if not is_stdlib_path and "site-packages" not in origin_str:
437
+ # If it's not in stdlib paths and not in site-packages,
438
+ # and it's not in the project directory, it might be third-party
439
+ # Check if it's outside the project directory
440
+ try:
441
+ origin_path.resolve().relative_to(self.project_root.resolve())
442
+ # It's in the project, so it's not third-party
443
+ return False
444
+ except ValueError:
445
+ # It's outside the project, might be third-party
446
+ # Try to import it to verify
447
+ try:
448
+ __import__(root_module)
449
+ # If import succeeds and it's outside project, likely third-party
450
+ return True
451
+ except ImportError:
452
+ pass
453
+
454
+ except (ImportError, ValueError, AttributeError, Exception):
455
+ # If importlib fails, try direct import as fallback
456
+ try:
457
+ imported_module = __import__(root_module)
458
+ # Check if the module's __file__ points to site-packages
459
+ if hasattr(imported_module, "__file__") and imported_module.__file__:
460
+ module_file = str(imported_module.__file__)
461
+ if "site-packages" in module_file or "dist-packages" in module_file:
462
+ return True
463
+ # Check if it's outside the project
464
+ try:
465
+ Path(imported_module.__file__).resolve().relative_to(
466
+ self.project_root.resolve()
467
+ )
468
+ return False # It's in the project
469
+ except ValueError:
470
+ # It's outside the project, likely third-party
471
+ return True
472
+ except ImportError:
473
+ pass
474
+
416
475
  return False
@@ -605,6 +605,51 @@ class BuildManager:
605
605
  Returns:
606
606
  List of unique third-party package names (e.g., ["pypdf", "requests", "pymupdf"])
607
607
  """
608
+ # Common third-party packages that are often misclassified as ambiguous
609
+ COMMON_THIRD_PARTY_PACKAGES = {
610
+ "torch",
611
+ "torchvision",
612
+ "numpy",
613
+ "pandas",
614
+ "matplotlib",
615
+ "scipy",
616
+ "sklearn",
617
+ "tensorflow",
618
+ "keras",
619
+ "pillow",
620
+ "PIL",
621
+ "cv2",
622
+ "opencv",
623
+ "requests",
624
+ "flask",
625
+ "django",
626
+ "pytest",
627
+ "pydantic",
628
+ "fastapi",
629
+ "sqlalchemy",
630
+ "pymongo",
631
+ "redis",
632
+ "celery",
633
+ "boto3",
634
+ "azure",
635
+ "google",
636
+ "openai",
637
+ "langchain",
638
+ "fiftyone",
639
+ "pycocotools",
640
+ "albumentations",
641
+ "loguru",
642
+ "tqdm",
643
+ "rich",
644
+ "pypdf",
645
+ "pymupdf",
646
+ "networkx",
647
+ "openpyxl",
648
+ "nltk",
649
+ "spacy",
650
+ "textdistance",
651
+ "IPython",
652
+ }
608
653
  third_party_packages: set[str] = set()
609
654
  # Cache package name lookups to avoid repeated expensive searches
610
655
  package_name_cache: dict[str, str | None] = {}
@@ -648,23 +693,34 @@ class BuildManager:
648
693
  # Fallback to using the import name
649
694
  third_party_packages.add(root_module)
650
695
  # If it's ambiguous or unresolved, and not stdlib/local/external,
651
- # only add as dependency if we can verify it's actually an installed package
696
+ # check if it's a common third-party package or can be verified
652
697
  elif imp.classification == "ambiguous" or imp.classification is None:
653
698
  # Check if it's not a local or external module
654
699
  if not imp.resolved_path:
655
- # Try to verify it's actually an installed package before adding
656
- # Check cache first
657
- if root_module not in package_name_cache:
658
- package_name_cache[root_module] = self._get_package_name_from_import(
659
- imp.module_name
660
- )
661
- actual_package = package_name_cache[root_module]
662
- # Only add if we can verify it's an actual installed package
663
- # Don't add ambiguous imports that we can't verify
664
- if actual_package:
665
- third_party_packages.add(actual_package)
666
- # If we can't verify it's a package, don't add it
667
- # (it's likely a local file that wasn't resolved properly)
700
+ # Check if it's a common third-party package
701
+ if root_module in COMMON_THIRD_PARTY_PACKAGES:
702
+ # Try to get package name, or use module name as fallback
703
+ if root_module not in package_name_cache:
704
+ package_name_cache[root_module] = self._get_package_name_from_import(
705
+ imp.module_name
706
+ )
707
+ actual_package = package_name_cache[root_module]
708
+ if actual_package:
709
+ third_party_packages.add(actual_package)
710
+ else:
711
+ # Fallback: use module name (common packages like torch, numpy)
712
+ third_party_packages.add(root_module)
713
+ else:
714
+ # For other ambiguous imports, only add if verified
715
+ if root_module not in package_name_cache:
716
+ package_name_cache[root_module] = self._get_package_name_from_import(
717
+ imp.module_name
718
+ )
719
+ actual_package = package_name_cache[root_module]
720
+ if actual_package:
721
+ third_party_packages.add(actual_package)
722
+ # If we can't verify it's a package, don't add it
723
+ # (it's likely a local file that wasn't resolved properly)
668
724
 
669
725
  if total_files > 50:
670
726
  print() # New line after progress indicator
@@ -765,6 +821,44 @@ class BuildManager:
765
821
  file=sys.stderr,
766
822
  )
767
823
 
824
+ def _calculate_relative_import_depth(
825
+ self, file_path: Path, module_path: Path, src_dir: Path
826
+ ) -> str:
827
+ """
828
+ Calculate the relative import prefix (., .., ...) based on directory depth.
829
+
830
+ Args:
831
+ file_path: Path to the file containing the import
832
+ module_path: Path to the imported module (file or directory)
833
+ src_dir: Source directory (package root)
834
+
835
+ Returns:
836
+ String with appropriate number of dots (e.g., ".", "..", "...")
837
+ """
838
+ try:
839
+ # Get relative paths from src_dir
840
+ file_rel = file_path.parent.relative_to(src_dir)
841
+ module_rel = module_path.parent.relative_to(src_dir)
842
+
843
+ # Calculate depth difference
844
+ # If paths are ".", depth is 0
845
+ file_depth = len(file_rel.parts) if file_rel.parts != (".",) else 0
846
+ module_depth = len(module_rel.parts) if module_rel.parts != (".",) else 0
847
+
848
+ depth_diff = file_depth - module_depth
849
+
850
+ if depth_diff == 0:
851
+ return "." # Same level
852
+ elif depth_diff > 0:
853
+ # File is deeper than module, need to go up
854
+ return "." * (depth_diff + 1) # Go up depth_diff levels
855
+ else:
856
+ # Module is deeper than file, use single dot
857
+ return "."
858
+ except ValueError:
859
+ # If paths are not relative to src_dir, fall back to single dot
860
+ return "."
861
+
768
862
  def _convert_imports_to_relative(
769
863
  self, python_files: list[Path], external_deps: list[ExternalDependency]
770
864
  ) -> None:
@@ -777,6 +871,9 @@ class BuildManager:
777
871
  1. Imports of copied dependencies (e.g., `from _shared.image_utils` -> `from ._shared.image_utils`)
778
872
  2. Imports of local files within the subfolder (e.g., `from detect_empty_drawings_utils` -> `from .detect_empty_drawings_utils`)
779
873
 
874
+ Only imports classified as "external" or "local" are converted. Imports classified
875
+ as "ambiguous", "third_party", or "stdlib" are left unchanged.
876
+
780
877
  Args:
781
878
  python_files: List of Python files in the source directory
782
879
  external_deps: List of external dependencies that were copied
@@ -784,6 +881,9 @@ class BuildManager:
784
881
  import ast
785
882
  import re
786
883
 
884
+ # Create analyzer for classifying imports
885
+ analyzer = ImportAnalyzer(self.project_root)
886
+
787
887
  # Build a set of import names that were copied
788
888
  copied_import_names: set[str] = set()
789
889
  for dep in external_deps:
@@ -838,8 +938,39 @@ class BuildManager:
838
938
  if node.module is None:
839
939
  continue
840
940
 
841
- # Check if this import matches a copied dependency or a local file
941
+ # Extract root module (first part before the first dot)
842
942
  root_module = node.module.split(".")[0]
943
+
944
+ # Classify the root module first to check if it's third-party/ambiguous/stdlib
945
+ # If root is third-party/ambiguous/stdlib, skip ALL its submodules
946
+ root_import_info = ImportInfo(
947
+ module_name=root_module,
948
+ import_type="from",
949
+ line_number=node.lineno,
950
+ file_path=file_path,
951
+ )
952
+ analyzer.classify_import(root_import_info, self.src_dir)
953
+
954
+ # If root is third-party/ambiguous/stdlib, skip ALL submodules
955
+ if root_import_info.classification in ("third_party", "ambiguous", "stdlib"):
956
+ continue
957
+
958
+ # Now classify the full import to determine if it should be converted
959
+ import_info = ImportInfo(
960
+ module_name=node.module,
961
+ import_type="from",
962
+ line_number=node.lineno,
963
+ file_path=file_path,
964
+ )
965
+ analyzer.classify_import(import_info, self.src_dir)
966
+
967
+ # Only convert imports classified as "external" or "local"
968
+ # Skip ambiguous, third_party, and stdlib imports
969
+ if import_info.classification not in ("external", "local"):
970
+ continue
971
+
972
+ # Check if this import matches a copied dependency or a local file
973
+ # (additional safety check, but classification takes precedence)
843
974
  is_copied_dependency = (
844
975
  root_module in copied_import_names or node.module in copied_import_names
845
976
  )
@@ -859,11 +990,36 @@ class BuildManager:
859
990
  if original_line.strip().startswith("from ."):
860
991
  continue
861
992
 
993
+ # Find the target path for the imported module
994
+ module_target_path = None
995
+ for dep in external_deps:
996
+ if dep.import_name == node.module or node.module.startswith(dep.import_name + "."):
997
+ module_target_path = dep.target_path
998
+ break
999
+
1000
+ # If not found in external deps, check if it's a local file
1001
+ if module_target_path is None:
1002
+ # Try to find it as a local file
1003
+ module_parts = node.module.split(".")
1004
+ potential_path = self.src_dir / "/".join(module_parts)
1005
+ if (potential_path / "__init__.py").exists():
1006
+ module_target_path = potential_path / "__init__.py"
1007
+ elif potential_path.with_suffix(".py").exists():
1008
+ module_target_path = potential_path.with_suffix(".py")
1009
+ else:
1010
+ # Fallback: assume same level
1011
+ module_target_path = file_path.parent
1012
+
1013
+ # Calculate the correct relative import depth
1014
+ dots = self._calculate_relative_import_depth(
1015
+ file_path, module_target_path, self.src_dir
1016
+ )
1017
+
862
1018
  # Convert absolute import to relative import
863
- # from _shared.image_utils import ... -> from ._shared.image_utils import ...
1019
+ # from _shared.image_utils import ... -> from .._shared.image_utils import ...
864
1020
  new_line = re.sub(
865
1021
  rf"^(\s*)from\s+{re.escape(node.module)}\s+import",
866
- rf"\1from .{node.module} import",
1022
+ rf"\1from {dots}{node.module} import",
867
1023
  original_line,
868
1024
  )
869
1025
 
@@ -874,7 +1030,39 @@ class BuildManager:
874
1030
  elif isinstance(node, ast.Import):
875
1031
  # Handle "import X" statements
876
1032
  for alias in node.names:
1033
+ # Extract root module (first part before the first dot)
877
1034
  root_module = alias.name.split(".")[0]
1035
+
1036
+ # Classify the root module first to check if it's third-party/ambiguous/stdlib
1037
+ # If root is third-party/ambiguous/stdlib, skip ALL its submodules
1038
+ root_import_info = ImportInfo(
1039
+ module_name=root_module,
1040
+ import_type="import",
1041
+ line_number=node.lineno,
1042
+ file_path=file_path,
1043
+ )
1044
+ analyzer.classify_import(root_import_info, self.src_dir)
1045
+
1046
+ # If root is third-party/ambiguous/stdlib, skip ALL submodules
1047
+ if root_import_info.classification in ("third_party", "ambiguous", "stdlib"):
1048
+ continue
1049
+
1050
+ # Now classify the full import to determine if it should be converted
1051
+ import_info = ImportInfo(
1052
+ module_name=alias.name,
1053
+ import_type="import",
1054
+ line_number=node.lineno,
1055
+ file_path=file_path,
1056
+ )
1057
+ analyzer.classify_import(import_info, self.src_dir)
1058
+
1059
+ # Only convert imports classified as "external" or "local"
1060
+ # Skip ambiguous, third_party, and stdlib imports
1061
+ if import_info.classification not in ("external", "local"):
1062
+ continue
1063
+
1064
+ # Check if this import matches a copied dependency or a local file
1065
+ # (additional safety check, but classification takes precedence)
878
1066
  is_copied_dependency = root_module in copied_import_names
879
1067
  is_local_file = root_module in local_file_names
880
1068
 
@@ -891,11 +1079,36 @@ class BuildManager:
891
1079
  if original_line.strip().startswith("import ."):
892
1080
  continue
893
1081
 
894
- # Convert "import _shared" to "from . import _shared"
1082
+ # Find the target path for the imported module
1083
+ module_target_path = None
1084
+ for dep in external_deps:
1085
+ if dep.import_name == alias.name or alias.name.startswith(dep.import_name + "."):
1086
+ module_target_path = dep.target_path
1087
+ break
1088
+
1089
+ # If not found in external deps, check if it's a local file
1090
+ if module_target_path is None:
1091
+ # Try to find it as a local file
1092
+ module_parts = alias.name.split(".")
1093
+ potential_path = self.src_dir / "/".join(module_parts)
1094
+ if (potential_path / "__init__.py").exists():
1095
+ module_target_path = potential_path / "__init__.py"
1096
+ elif potential_path.with_suffix(".py").exists():
1097
+ module_target_path = potential_path.with_suffix(".py")
1098
+ else:
1099
+ # Fallback: assume same level
1100
+ module_target_path = file_path.parent
1101
+
1102
+ # Calculate the correct relative import depth
1103
+ dots = self._calculate_relative_import_depth(
1104
+ file_path, module_target_path, self.src_dir
1105
+ )
1106
+
1107
+ # Convert "import _shared" to "from .. import _shared" (with correct depth)
895
1108
  # This is more complex, so we'll use a regex replacement
896
1109
  new_line = re.sub(
897
1110
  rf"^(\s*)import\s+{re.escape(alias.name)}\b",
898
- rf"\1from . import {alias.name}",
1111
+ rf"\1from {dots} import {alias.name}",
899
1112
  original_line,
900
1113
  )
901
1114