python-package-folder 8.3.0__tar.gz → 9.0.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.3.0 → python_package_folder-9.0.0}/PKG-INFO +1 -1
  2. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/coverage.svg +2 -2
  3. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/pyproject.toml +1 -1
  4. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/analyzer.py +64 -5
  5. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/manager.py +219 -22
  6. python_package_folder-9.0.0/tests/test_subfolder_build.py +3848 -0
  7. python_package_folder-8.3.0/tests/test_subfolder_build.py +0 -2101
  8. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.copier-answers.yml +0 -0
  9. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.cursor/plans/optional_version_+_semantic-release_efed88a6.plan.md +0 -0
  10. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.cursor/plans/replace_node.js_semantic-release_with_custom_python_implementation_64e05e1a.plan.md +0 -0
  11. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.cursor/rules/general.mdc +0 -0
  12. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.cursor/rules/python.mdc +0 -0
  13. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.github/workflows/ci.yml +0 -0
  14. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.github/workflows/publish.yml +0 -0
  15. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.gitignore +0 -0
  16. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/.vscode/settings.json +0 -0
  17. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/LICENSE +0 -0
  18. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/MANIFEST.in +0 -0
  19. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/Makefile +0 -0
  20. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/README.md +0 -0
  21. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/development.md +0 -0
  22. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/docs/DEVELOPMENT.md +0 -0
  23. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/docs/INSTALLATION.md +0 -0
  24. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/docs/PUBLISHING.md +0 -0
  25. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/docs/REFERENCE.md +0 -0
  26. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/docs/USAGE.md +0 -0
  27. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/docs/VERSION_RESOLUTION.md +0 -0
  28. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/installation.md +0 -0
  29. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/publishing.md +0 -0
  30. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/__init__.py +0 -0
  31. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/__main__.py +0 -0
  32. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/finder.py +0 -0
  33. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/publisher.py +0 -0
  34. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/py.typed +0 -0
  35. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/python_package_folder.py +0 -0
  36. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/subfolder_build.py +0 -0
  37. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/types.py +0 -0
  38. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/utils.py +0 -0
  39. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/version.py +0 -0
  40. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/src/python_package_folder/version_calculator.py +0 -0
  41. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/conftest.py +0 -0
  42. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/some_globals.py +0 -0
  43. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/subfolder_to_build/README.md +0 -0
  44. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/subfolder_to_build/__init__.py +0 -0
  45. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/subfolder_to_build/some_function.py +0 -0
  46. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/subfolder_to_build/some_globals.py +0 -0
  47. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/utility_folder/_SS/some_superseded_file.py +0 -0
  48. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/folder_structure/utility_folder/some_utility.py +0 -0
  49. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_build_with_external_deps.py +0 -0
  50. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_exclude_patterns.py +0 -0
  51. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_linting.py +0 -0
  52. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_preserve_directory_structure.py +0 -0
  53. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_publisher.py +0 -0
  54. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_shared_subdirectory_imports.py +0 -0
  55. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_spreadsheet_creation_imports.py +0 -0
  56. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_third_party_dependencies.py +0 -0
  57. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_utils.py +0 -0
  58. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_version_calculator.py +0 -0
  59. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/test_version_manager.py +0 -0
  60. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/tests/tests.py +0 -0
  61. {python_package_folder-8.3.0 → python_package_folder-9.0.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-package-folder
3
- Version: 8.3.0
3
+ Version: 9.0.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">66%</text>
18
- <text x="81" y="14">66%</text>
17
+ <text x="81" y="15" fill="#010101" fill-opacity=".3">67%</text>
18
+ <text x="81" y="14">67%</text>
19
19
  </g>
20
20
  </svg>
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  # ---- Dev dependencies ----
45
45
 
46
- version = "8.3.0"
46
+ version = "9.0.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,65 @@ 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_dir = file_path.parent
841
+ # If module_path has a file extension (.py), treat it as a file and use its parent
842
+ # Otherwise, treat it as a directory
843
+ # This handles cases where the file doesn't exist yet (is_file() would return False)
844
+ if module_path.suffix == ".py" or (module_path.is_file() if module_path.exists() else False):
845
+ module_dir = module_path.parent
846
+ else:
847
+ module_dir = module_path
848
+
849
+ # Normalize both to be relative to src_dir
850
+ try:
851
+ file_rel = file_dir.relative_to(src_dir)
852
+ module_rel = module_dir.relative_to(src_dir)
853
+ except ValueError:
854
+ # If not relative to src_dir, fall back to single dot
855
+ return "."
856
+
857
+ # Calculate depth difference
858
+ # If paths are ".", depth is 0
859
+ file_depth = len(file_rel.parts) if file_rel.parts and file_rel.parts != (".",) else 0
860
+ module_depth = len(module_rel.parts) if module_rel.parts and module_rel.parts != (".",) else 0
861
+
862
+ depth_diff = file_depth - module_depth
863
+
864
+ if depth_diff == 0:
865
+ # Same depth - check if they're the same directory or siblings
866
+ if file_rel == module_rel:
867
+ return "." # Same directory
868
+ else:
869
+ # Sibling directories at same depth - need to go up one level
870
+ # e.g., PytorchCoco/ and _shared/ both at depth 1, need ..
871
+ return ".."
872
+ elif depth_diff > 0:
873
+ # File is deeper than module, need to go up
874
+ # e.g., file at depth 2, module at depth 0, need ...
875
+ return "." * (depth_diff + 1) # Go up depth_diff levels
876
+ else:
877
+ # Module is deeper than file, use single dot
878
+ return "."
879
+ except (ValueError, AttributeError):
880
+ # If paths are not relative to src_dir, fall back to single dot
881
+ return "."
882
+
768
883
  def _convert_imports_to_relative(
769
884
  self, python_files: list[Path], external_deps: list[ExternalDependency]
770
885
  ) -> None:
@@ -844,7 +959,24 @@ class BuildManager:
844
959
  if node.module is None:
845
960
  continue
846
961
 
847
- # Classify the import to determine if it should be converted
962
+ # Extract root module (first part before the first dot)
963
+ root_module = node.module.split(".")[0]
964
+
965
+ # Classify the root module first to check if it's third-party/ambiguous/stdlib
966
+ # If root is third-party/ambiguous/stdlib, skip ALL its submodules
967
+ root_import_info = ImportInfo(
968
+ module_name=root_module,
969
+ import_type="from",
970
+ line_number=node.lineno,
971
+ file_path=file_path,
972
+ )
973
+ analyzer.classify_import(root_import_info, self.src_dir)
974
+
975
+ # If root is third-party/ambiguous/stdlib, skip ALL submodules
976
+ if root_import_info.classification in ("third_party", "ambiguous", "stdlib"):
977
+ continue
978
+
979
+ # Now classify the full import to determine if it should be converted
848
980
  import_info = ImportInfo(
849
981
  module_name=node.module,
850
982
  import_type="from",
@@ -860,7 +992,6 @@ class BuildManager:
860
992
 
861
993
  # Check if this import matches a copied dependency or a local file
862
994
  # (additional safety check, but classification takes precedence)
863
- root_module = node.module.split(".")[0]
864
995
  is_copied_dependency = (
865
996
  root_module in copied_import_names or node.module in copied_import_names
866
997
  )
@@ -880,11 +1011,36 @@ class BuildManager:
880
1011
  if original_line.strip().startswith("from ."):
881
1012
  continue
882
1013
 
1014
+ # Find the target path for the imported module
1015
+ module_target_path = None
1016
+ for dep in external_deps:
1017
+ if dep.import_name == node.module or node.module.startswith(dep.import_name + "."):
1018
+ module_target_path = dep.target_path
1019
+ break
1020
+
1021
+ # If not found in external deps, check if it's a local file
1022
+ if module_target_path is None:
1023
+ # Try to find it as a local file
1024
+ module_parts = node.module.split(".")
1025
+ potential_path = self.src_dir / "/".join(module_parts)
1026
+ if (potential_path / "__init__.py").exists():
1027
+ module_target_path = potential_path / "__init__.py"
1028
+ elif potential_path.with_suffix(".py").exists():
1029
+ module_target_path = potential_path.with_suffix(".py")
1030
+ else:
1031
+ # Fallback: assume same level
1032
+ module_target_path = file_path.parent
1033
+
1034
+ # Calculate the correct relative import depth
1035
+ dots = self._calculate_relative_import_depth(
1036
+ file_path, module_target_path, self.src_dir
1037
+ )
1038
+
883
1039
  # Convert absolute import to relative import
884
- # from _shared.image_utils import ... -> from ._shared.image_utils import ...
1040
+ # from _shared.image_utils import ... -> from .._shared.image_utils import ...
885
1041
  new_line = re.sub(
886
1042
  rf"^(\s*)from\s+{re.escape(node.module)}\s+import",
887
- rf"\1from .{node.module} import",
1043
+ rf"\1from {dots}{node.module} import",
888
1044
  original_line,
889
1045
  )
890
1046
 
@@ -895,7 +1051,24 @@ class BuildManager:
895
1051
  elif isinstance(node, ast.Import):
896
1052
  # Handle "import X" statements
897
1053
  for alias in node.names:
898
- # Classify the import to determine if it should be converted
1054
+ # Extract root module (first part before the first dot)
1055
+ root_module = alias.name.split(".")[0]
1056
+
1057
+ # Classify the root module first to check if it's third-party/ambiguous/stdlib
1058
+ # If root is third-party/ambiguous/stdlib, skip ALL its submodules
1059
+ root_import_info = ImportInfo(
1060
+ module_name=root_module,
1061
+ import_type="import",
1062
+ line_number=node.lineno,
1063
+ file_path=file_path,
1064
+ )
1065
+ analyzer.classify_import(root_import_info, self.src_dir)
1066
+
1067
+ # If root is third-party/ambiguous/stdlib, skip ALL submodules
1068
+ if root_import_info.classification in ("third_party", "ambiguous", "stdlib"):
1069
+ continue
1070
+
1071
+ # Now classify the full import to determine if it should be converted
899
1072
  import_info = ImportInfo(
900
1073
  module_name=alias.name,
901
1074
  import_type="import",
@@ -911,7 +1084,6 @@ class BuildManager:
911
1084
 
912
1085
  # Check if this import matches a copied dependency or a local file
913
1086
  # (additional safety check, but classification takes precedence)
914
- root_module = alias.name.split(".")[0]
915
1087
  is_copied_dependency = root_module in copied_import_names
916
1088
  is_local_file = root_module in local_file_names
917
1089
 
@@ -928,11 +1100,36 @@ class BuildManager:
928
1100
  if original_line.strip().startswith("import ."):
929
1101
  continue
930
1102
 
931
- # Convert "import _shared" to "from . import _shared"
1103
+ # Find the target path for the imported module
1104
+ module_target_path = None
1105
+ for dep in external_deps:
1106
+ if dep.import_name == alias.name or alias.name.startswith(dep.import_name + "."):
1107
+ module_target_path = dep.target_path
1108
+ break
1109
+
1110
+ # If not found in external deps, check if it's a local file
1111
+ if module_target_path is None:
1112
+ # Try to find it as a local file
1113
+ module_parts = alias.name.split(".")
1114
+ potential_path = self.src_dir / "/".join(module_parts)
1115
+ if (potential_path / "__init__.py").exists():
1116
+ module_target_path = potential_path / "__init__.py"
1117
+ elif potential_path.with_suffix(".py").exists():
1118
+ module_target_path = potential_path.with_suffix(".py")
1119
+ else:
1120
+ # Fallback: assume same level
1121
+ module_target_path = file_path.parent
1122
+
1123
+ # Calculate the correct relative import depth
1124
+ dots = self._calculate_relative_import_depth(
1125
+ file_path, module_target_path, self.src_dir
1126
+ )
1127
+
1128
+ # Convert "import _shared" to "from .. import _shared" (with correct depth)
932
1129
  # This is more complex, so we'll use a regex replacement
933
1130
  new_line = re.sub(
934
1131
  rf"^(\s*)import\s+{re.escape(alias.name)}\b",
935
- rf"\1from . import {alias.name}",
1132
+ rf"\1from {dots} import {alias.name}",
936
1133
  original_line,
937
1134
  )
938
1135