unctools 0.1.0__tar.gz → 0.2.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 (46) hide show
  1. {unctools-0.1.0/unctools.egg-info → unctools-0.2.0}/PKG-INFO +1 -1
  2. unctools-0.2.0/docs/api-stability.md +51 -0
  3. {unctools-0.1.0 → unctools-0.2.0}/tests/basic_functionality_test.py +8 -10
  4. {unctools-0.1.0 → unctools-0.2.0}/tests/test_converter.py +1 -19
  5. {unctools-0.1.0 → unctools-0.2.0}/tests/test_converter_v2.py +2 -7
  6. unctools-0.2.0/tests/test_import_stability.py +97 -0
  7. unctools-0.2.0/tests/test_operations.py +361 -0
  8. {unctools-0.1.0 → unctools-0.2.0}/unctools/__init__.py +15 -10
  9. {unctools-0.1.0 → unctools-0.2.0}/unctools/converter.py +97 -18
  10. {unctools-0.1.0 → unctools-0.2.0}/unctools/detector.py +153 -4
  11. unctools-0.2.0/unctools/operations.py +42 -0
  12. {unctools-0.1.0 → unctools-0.2.0}/unctools/utils/compat.py +3 -71
  13. {unctools-0.1.0 → unctools-0.2.0/unctools.egg-info}/PKG-INFO +1 -1
  14. {unctools-0.1.0 → unctools-0.2.0}/unctools.egg-info/SOURCES.txt +2 -0
  15. unctools-0.1.0/tests/test_operations.py +0 -649
  16. unctools-0.1.0/unctools/operations.py +0 -562
  17. {unctools-0.1.0 → unctools-0.2.0}/LICENSE +0 -0
  18. {unctools-0.1.0 → unctools-0.2.0}/MANIFEST.in +0 -0
  19. {unctools-0.1.0 → unctools-0.2.0}/README.md +0 -0
  20. {unctools-0.1.0 → unctools-0.2.0}/docs/implementation-guide.md +0 -0
  21. {unctools-0.1.0 → unctools-0.2.0}/docs/implementation-summary.md +0 -0
  22. {unctools-0.1.0 → unctools-0.2.0}/docs/integration-guide.md +0 -0
  23. {unctools-0.1.0 → unctools-0.2.0}/examples/basic_usage.py +0 -0
  24. {unctools-0.1.0 → unctools-0.2.0}/examples/batch_operations.py +0 -0
  25. {unctools-0.1.0 → unctools-0.2.0}/examples/windows_zone_fix.py +0 -0
  26. {unctools-0.1.0 → unctools-0.2.0}/pyproject.toml +0 -0
  27. {unctools-0.1.0 → unctools-0.2.0}/setup.cfg +0 -0
  28. {unctools-0.1.0 → unctools-0.2.0}/setup.py +0 -0
  29. {unctools-0.1.0 → unctools-0.2.0}/tests/__init__.py +0 -0
  30. {unctools-0.1.0 → unctools-0.2.0}/tests/conftest.py +0 -0
  31. {unctools-0.1.0 → unctools-0.2.0}/tests/test_detector.py +0 -0
  32. {unctools-0.1.0 → unctools-0.2.0}/tests/test_framework.py +0 -0
  33. {unctools-0.1.0 → unctools-0.2.0}/tests/test_win32net_warning.py +0 -0
  34. {unctools-0.1.0 → unctools-0.2.0}/tests/test_windows.py +0 -0
  35. {unctools-0.1.0 → unctools-0.2.0}/tests/test_windows_imports.py +0 -0
  36. {unctools-0.1.0 → unctools-0.2.0}/unctools/utils/__init__.py +0 -0
  37. {unctools-0.1.0 → unctools-0.2.0}/unctools/utils/logger.py +0 -0
  38. {unctools-0.1.0 → unctools-0.2.0}/unctools/utils/validation.py +0 -0
  39. {unctools-0.1.0 → unctools-0.2.0}/unctools/windows/__init__.py +0 -0
  40. {unctools-0.1.0 → unctools-0.2.0}/unctools/windows/network.py +0 -0
  41. {unctools-0.1.0 → unctools-0.2.0}/unctools/windows/registry.py +0 -0
  42. {unctools-0.1.0 → unctools-0.2.0}/unctools/windows/security.py +0 -0
  43. {unctools-0.1.0 → unctools-0.2.0}/unctools.egg-info/dependency_links.txt +0 -0
  44. {unctools-0.1.0 → unctools-0.2.0}/unctools.egg-info/not-zip-safe +0 -0
  45. {unctools-0.1.0 → unctools-0.2.0}/unctools.egg-info/requires.txt +0 -0
  46. {unctools-0.1.0 → unctools-0.2.0}/unctools.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unctools
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A comprehensive toolkit for handling UNC paths, network drives, and substituted drives
5
5
  Home-page: https://github.com/djdacy/unctools
6
6
  Author: Dustin Darcy
@@ -0,0 +1,51 @@
1
+ # API Stability
2
+
3
+ UNCtools is the L0 path-identity layer of the
4
+ [DazzleLib stack](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md).
5
+ Its public surface is locked and machine-checked by
6
+ `tests/test_import_stability.py` -- if that canary fails, a consumer somewhere
7
+ breaks: follow the policy below, never silently fix the test.
8
+
9
+ ## Policy
10
+
11
+ 1. **Locked symbols never vanish silently.** Removal/rename ships a NOISY
12
+ `DeprecationWarning` shim naming the new home and removal version,
13
+ registered in the stack's alias register, removed on schedule.
14
+ 2. **The layer charter is not negotiable** (STACK-MAP rule 3a): this library
15
+ may probe the filesystem read-only to answer identity questions; it never
16
+ mutates or transfers content. Functions that do are rejected, not deprecated.
17
+ 3. **Name hygiene** (STACK-MAP rule 7): before exporting a public symbol,
18
+ grep the stack -- same-name-different-semantics requires a layer-teaching
19
+ rename (that is how `classify_path_origin` got its name).
20
+
21
+ ## Locked surface (0.2.0)
22
+
23
+ | Module | Symbols |
24
+ |---|---|
25
+ | `unctools` (top level) | `convert_to_local`, `convert_to_unc`, `batch_convert`, `get_unc_path_elements`, `build_unc_path`, `is_unc_path`, `is_network_drive`, `is_subst_drive`, `classify_path_origin`, `get_network_mappings`, `detect_path_issues`, `file_exists`, `is_path_accessible`, `find_accessible_path`, `configure_logging`, `get_version` |
26
+ | `unctools.converter` | `UNCConverter`, `convert_to_local`, `convert_to_unc`, `batch_convert`, `get_unc_path_elements`, `build_unc_path`, `refresh_mappings`, `get_mappings`, `parse_unc_path`, `join_unc_path` |
27
+ | `unctools.detector` | the origin classifiers + probes listed above, plus `get_drive_type`, `get_subst_target`, `get_network_target`, `is_server_in_intranet_zone` |
28
+ | `unctools.windows.*` | Windows-only security/network/registry surface (unchanged in 0.2.0) |
29
+
30
+ ## Active deprecations
31
+
32
+ | Symbol | Replacement | Warns since | Removed in |
33
+ |---|---|---|---|
34
+ | `get_path_type` | `classify_path_origin` | 0.2.0 | 0.3.0 |
35
+ | `unctools.operations` (module facade) | top-level imports / `converter` / `detector` | 0.2.0 | 0.3.0 |
36
+
37
+ ## Known consumers
38
+
39
+ | Consumer | Symbols | Notes |
40
+ |---|---|---|
41
+ | dazzlecmd (`safedel/_volumes.py`, `fixpath.py`) | `get_drive_type`, `is_network_drive`, `is_unc_path`, `convert_to_local` | optional imports today; harden in stack P4 |
42
+ | dazzlesum | top-level conversion/probe block | soft-imports today; harden in stack P4 |
43
+ | modified_datetime_fix | mixed (incl. a vendored copy to retire) | stack P4 |
44
+ | dazzle-filekit | `[unctools]` extra pin (no runtime import) | becomes a real edge only if/when filekit consumes identity at runtime |
45
+
46
+ ## Consolidation candidates (0.3.0)
47
+
48
+ - `parse_unc_path`/`join_unc_path` vs `get_unc_path_elements`/`build_unc_path`
49
+ (near-duplicates; the latter preserve forward slashes in the relative part)
50
+ - dazzle-lib adoption: derive errors from `dazzle_lib.PathIdentityError`
51
+ (deliberately deferred from 0.2.0 to keep the surgery diff reviewable)
@@ -62,9 +62,9 @@ def run_tests():
62
62
  print("\nTesting core module imports...")
63
63
  try:
64
64
  from unctools import (
65
- convert_to_local, convert_to_unc, normalize_path,
65
+ convert_to_local, convert_to_unc, classify_path_origin,
66
66
  is_unc_path, is_network_drive, is_subst_drive,
67
- safe_open, batch_convert
67
+ file_exists, batch_convert
68
68
  )
69
69
  check(True, "Import core functions")
70
70
  except ImportError as e:
@@ -111,12 +111,12 @@ def run_tests():
111
111
  test_unc_path = r"\\server\share\folder"
112
112
  check(is_unc_path(test_unc_path), f"is_unc_path({test_unc_path}) should be True")
113
113
 
114
- # Test path normalization
114
+ # Test origin classification (normalize_path removed in 0.2.0, D4)
115
115
  try:
116
- normalized = normalize_path(test_path)
117
- check(True, f"normalize_path({test_path}) => {normalized}")
116
+ origin = classify_path_origin(test_path)
117
+ check(True, f"classify_path_origin({test_path}) => {origin}")
118
118
  except Exception as e:
119
- check(False, f"normalize_path({test_path}): {e}")
119
+ check(False, f"classify_path_origin({test_path}): {e}")
120
120
 
121
121
  # Test path conversion (just ensure it doesn't error)
122
122
  try:
@@ -157,10 +157,8 @@ def run_tests():
157
157
  temp_filename = f.name
158
158
  f.write("UNCtools test file")
159
159
 
160
- # Test safe_open
161
- with safe_open(temp_filename, 'r') as f:
162
- content = f.read()
163
- check(content == "UNCtools test file", f"safe_open() and read content: '{content}'")
160
+ # Test file_exists probe (safe_open removed in 0.2.0, D7)
161
+ check(file_exists(temp_filename), f"file_exists({temp_filename}) should be True")
164
162
 
165
163
  # Clean up
166
164
  os.unlink(temp_filename)
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  from unittest import mock
11
11
 
12
12
  from unctools.converter import (
13
- UNCConverter, convert_to_local, convert_to_unc, normalize_path,
13
+ UNCConverter, convert_to_local, convert_to_unc,
14
14
  parse_unc_path, join_unc_path
15
15
  )
16
16
 
@@ -180,24 +180,6 @@ class TestModuleFunctions:
180
180
  assert result == Path(TEST_UNC_PATH)
181
181
  mock_converter.convert_to_unc.assert_called_once_with(TEST_LOCAL_PATH)
182
182
 
183
- def test_normalize_path(self):
184
- """Test normalize_path function."""
185
- # Test with prefer_unc=False (default)
186
- with mock.patch('unctools.converter.convert_to_local') as mock_convert_local:
187
- mock_convert_local.return_value = Path(TEST_LOCAL_PATH)
188
-
189
- result = normalize_path(TEST_UNC_PATH)
190
- assert result == Path(TEST_LOCAL_PATH)
191
- mock_convert_local.assert_called_once_with(Path(TEST_UNC_PATH))
192
-
193
- # Test with prefer_unc=True
194
- with mock.patch('unctools.converter.convert_to_unc') as mock_convert_unc:
195
- mock_convert_unc.return_value = Path(TEST_UNC_PATH)
196
-
197
- result = normalize_path(TEST_LOCAL_PATH, prefer_unc=True)
198
- assert result == Path(TEST_UNC_PATH)
199
- mock_convert_unc.assert_called_once_with(Path(TEST_LOCAL_PATH))
200
-
201
183
  def test_parse_unc_path(self):
202
184
  """Test parse_unc_path function."""
203
185
  # Test with a valid UNC path
@@ -23,7 +23,7 @@ from tests.test_framework import (
23
23
  # Import UNCtools
24
24
  import unctools
25
25
  from unctools.converter import (
26
- UNCConverter, convert_to_local, convert_to_unc, normalize_path,
26
+ UNCConverter, convert_to_local, convert_to_unc,
27
27
  parse_unc_path, join_unc_path
28
28
  )
29
29
 
@@ -155,12 +155,7 @@ def test_module_functions():
155
155
  path = convert_to_unc(TEST_LOCAL_PATH)
156
156
  assert_is_not_none(path)
157
157
 
158
- # Test normalize_path
159
- path = normalize_path(TEST_UNC_PATH)
160
- assert_is_not_none(path)
161
-
162
- path = normalize_path(TEST_LOCAL_PATH, prefer_unc=True)
163
- assert_is_not_none(path)
158
+ # normalize_path was removed in 0.2.0 (D4) -- the explicit converts ARE the API
164
159
 
165
160
  def test_parse_unc_path():
166
161
  """Test parse_unc_path function."""
@@ -0,0 +1,97 @@
1
+ """Import-stability canary + 0.2.0 surgery contract (see docs/api-stability.md).
2
+
3
+ Locked surface: if this test fails, a consumer breaks -- follow the
4
+ api-stability process (noisy shim, register, slate removal), never a silent fix.
5
+
6
+ Also asserts the 0.2.0 surgery outcomes (STACK-MAP D4/D7/D8):
7
+ - deleted content-I/O wrappers are GONE (probe-not-mutate, rule 3a)
8
+ - moved survivors live in their new homes (and the top-level namespace)
9
+ - the get_path_type -> classify_path_origin shim warns (alias A4)
10
+ - the operations facade warns on import (removed in 0.3.0)
11
+ """
12
+
13
+ import importlib
14
+ import warnings
15
+
16
+ import pytest
17
+
18
+ LOCKED_SURFACE = {
19
+ "unctools": [
20
+ # identity conversion
21
+ "convert_to_local", "convert_to_unc", "batch_convert",
22
+ "get_unc_path_elements", "build_unc_path",
23
+ # origin classification + probes
24
+ "is_unc_path", "is_network_drive", "is_subst_drive",
25
+ "classify_path_origin", "get_network_mappings", "detect_path_issues",
26
+ "file_exists", "is_path_accessible", "find_accessible_path",
27
+ # package plumbing
28
+ "configure_logging", "get_version",
29
+ ],
30
+ "unctools.converter": [
31
+ "UNCConverter", "convert_to_local", "convert_to_unc", "batch_convert",
32
+ "get_unc_path_elements", "build_unc_path", "refresh_mappings",
33
+ ],
34
+ "unctools.detector": [
35
+ "is_unc_path", "is_network_drive", "is_subst_drive",
36
+ "classify_path_origin", "get_network_mappings", "detect_path_issues",
37
+ "file_exists", "is_path_accessible", "find_accessible_path",
38
+ ],
39
+ }
40
+
41
+ DELETED_FOREVER = {
42
+ # D7: content I/O has no home at L0 (probe, never mutate)
43
+ "unctools.operations": ["safe_open", "safe_copy", "batch_copy",
44
+ "process_files", "replace_in_file",
45
+ "batch_replace_in_files"],
46
+ # D8: case handling merged into dazzle-filekit
47
+ "unctools.utils.compat": ["path_exists_case_sensitive",
48
+ "get_case_sensitive_path"],
49
+ # D4: the explicit converts ARE the API
50
+ "unctools.converter": ["normalize_path"],
51
+ }
52
+
53
+
54
+ def test_locked_surface_importable():
55
+ missing = []
56
+ for module_name, symbols in LOCKED_SURFACE.items():
57
+ module = importlib.import_module(module_name)
58
+ for symbol in symbols:
59
+ if not hasattr(module, symbol):
60
+ missing.append(f"{module_name}.{symbol}")
61
+ assert not missing, f"Locked API symbols missing: {missing}"
62
+
63
+
64
+ def test_deleted_symbols_stay_deleted():
65
+ present = []
66
+ for module_name, symbols in DELETED_FOREVER.items():
67
+ with warnings.catch_warnings():
68
+ warnings.simplefilter("ignore") # operations facade warns on import
69
+ module = importlib.import_module(module_name)
70
+ for symbol in symbols:
71
+ if hasattr(module, symbol):
72
+ present.append(f"{module_name}.{symbol}")
73
+ assert not present, (
74
+ f"Deleted-by-contract symbols re-appeared: {present} -- "
75
+ f"these were removed by STACK-MAP D4/D7/D8 and must not return."
76
+ )
77
+
78
+
79
+ def test_get_path_type_shim_warns_and_delegates():
80
+ """A4: old name works through 0.2.x, warns naming the new home."""
81
+ import unctools
82
+ with pytest.warns(DeprecationWarning, match="classify_path_origin"):
83
+ result = unctools.get_path_type("C:\\")
84
+ assert result == unctools.classify_path_origin("C:\\")
85
+
86
+
87
+ def test_operations_facade_warns_on_import():
88
+ """The dissolved operations module is a 0.2.x-only facade (gone in 0.3.0)."""
89
+ import sys
90
+ sys.modules.pop("unctools.operations", None)
91
+ with pytest.warns(DeprecationWarning, match="0.3.0"):
92
+ import unctools.operations # noqa: F401
93
+
94
+
95
+ def test_version_is_0_2_0():
96
+ import unctools
97
+ assert unctools.__version__ == "0.2.0"
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for the unctools.operations module using the test framework.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ import logging
10
+ import shutil
11
+ from pathlib import Path
12
+ from unittest import mock
13
+
14
+ # Add parent directory to path for imports
15
+ sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
16
+
17
+ # Import test framework
18
+ from tests.test_framework import (
19
+ TestSuite, assert_true, assert_false, assert_equal,
20
+ assert_not_equal, assert_is_none, assert_is_not_none,
21
+ skip_if_not_windows, skip_if_windows, skip_if_no_module,
22
+ run_test_suites, SkipTest
23
+ )
24
+
25
+ # Import UNCtools
26
+ import unctools
27
+ # 0.2.0 (STACK-MAP D7): content-I/O wrappers were deleted; survivors moved
28
+ # to their layer-correct homes. Tests import from the new locations.
29
+ from unctools.converter import (
30
+ batch_convert, get_unc_path_elements, build_unc_path
31
+ )
32
+ from unctools.detector import (
33
+ file_exists, is_path_accessible, find_accessible_path
34
+ )
35
+ from unctools.detector import is_unc_path, PATH_TYPE_UNC
36
+
37
+ # Configure logging
38
+ logging.basicConfig(level=logging.INFO,
39
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
40
+
41
+ # Test data
42
+ TEST_UNC_PATH = "\\\\server\\share\\folder\\file.txt"
43
+ TEST_LOCAL_PATH = "C:\\Users\\username\\Documents\\file.txt"
44
+
45
+ class TestEnvironment:
46
+ """Manages a temporary test environment with files."""
47
+
48
+ def __init__(self):
49
+ """Initialize the test environment."""
50
+ self.temp_dir = None
51
+ self.test_files = []
52
+ self.output_dir = None
53
+
54
+ def setup(self):
55
+ """Set up the test environment."""
56
+ # Create a temporary directory
57
+ self.temp_dir = tempfile.mkdtemp(prefix='unctools_test_')
58
+
59
+ # Create an output directory for copy tests
60
+ self.output_dir = os.path.join(self.temp_dir, 'output')
61
+ os.makedirs(self.output_dir, exist_ok=True)
62
+
63
+ # Create test files
64
+ self._create_test_files()
65
+
66
+ # Return self for convenience
67
+ return self
68
+
69
+ def teardown(self):
70
+ """Clean up the test environment."""
71
+ # Remove the temporary directory
72
+ if self.temp_dir and os.path.exists(self.temp_dir):
73
+ shutil.rmtree(self.temp_dir)
74
+ self.temp_dir = None
75
+ self.test_files = []
76
+ self.output_dir = None
77
+
78
+ def _create_test_files(self):
79
+ """Create test files in the temporary directory."""
80
+ # Create files with different content
81
+ file_paths = []
82
+
83
+ # Create a simple text file
84
+ text_file = os.path.join(self.temp_dir, 'text_file.txt')
85
+ with open(text_file, 'w') as f:
86
+ f.write('This is a test file.')
87
+ file_paths.append(text_file)
88
+
89
+ # Create a nested directory
90
+ nested_dir = os.path.join(self.temp_dir, 'nested')
91
+ os.makedirs(nested_dir, exist_ok=True)
92
+
93
+ # Create a file in the nested directory
94
+ nested_file = os.path.join(nested_dir, 'nested_file.txt')
95
+ with open(nested_file, 'w') as f:
96
+ f.write('This is a nested file.')
97
+ file_paths.append(nested_file)
98
+
99
+ # Create binary file
100
+ binary_file = os.path.join(self.temp_dir, 'binary_file.bin')
101
+ with open(binary_file, 'wb') as f:
102
+ f.write(b'\x00\x01\x02\x03\x04')
103
+ file_paths.append(binary_file)
104
+
105
+ # Create multiple text files for batch operations
106
+ #for i in range(3):
107
+ # batch_file = os.path.join(self.temp_dir, f'batch_file_{i}.txt')
108
+ # with open(batch_file, 'w') as f:
109
+ # f.write(f'This is batch file {i}.')
110
+ # file_paths.append(batch_file)
111
+
112
+ # Store the file paths
113
+ self.test_files = file_paths
114
+
115
+ return file_paths
116
+
117
+ # Create test suite setup and teardown functions
118
+ def setup_test_environment():
119
+ """Set up a test environment for tests."""
120
+ env = TestEnvironment()
121
+ return env.setup()
122
+
123
+ def teardown_test_environment(env=None):
124
+ """Clean up the test environment."""
125
+ if env:
126
+ env.teardown()
127
+
128
+
129
+ def test_file_exists(env):
130
+ """Test file_exists function."""
131
+ # Test with an existing file
132
+ test_file = env.test_files[0]
133
+ assert_true(file_exists(test_file), "Existing file should be detected")
134
+
135
+ # Test with a non-existent file
136
+ non_existent = os.path.join(env.temp_dir, 'non_existent.txt')
137
+ assert_false(file_exists(non_existent), "Non-existent file should not be detected")
138
+
139
+ # Test with a directory
140
+ assert_true(file_exists(env.temp_dir), "Directory should be detected as existing")
141
+
142
+ # Test with convert_paths behavior
143
+ # Mock convert_to_local and convert_to_unc to return alternate paths
144
+ # and os.path.exists to return True for the converted path
145
+
146
+ def mock_convert_to_local(path):
147
+ return Path(str(path) + ".local")
148
+
149
+ def mock_convert_to_unc(path):
150
+ return Path(str(path) + ".unc")
151
+
152
+ def mock_is_unc_path(path):
153
+ path_str = str(path) # Convert Path to string before checking
154
+ return path_str.startswith("\\\\") or path_str.startswith("//")
155
+
156
+ def mock_exists(path):
157
+ return str(path).endswith(".local") or str(path).endswith(".unc")
158
+
159
+ with mock.patch('unctools.converter.convert_to_local', side_effect=mock_convert_to_local), \
160
+ mock.patch('unctools.converter.convert_to_unc', side_effect=mock_convert_to_unc), \
161
+ mock.patch('unctools.detector.is_unc_path', side_effect=mock_is_unc_path), \
162
+ mock.patch('os.path.exists', side_effect=mock_exists):
163
+
164
+ # Test with a UNC path (should try convert_to_local)
165
+ assert_true(file_exists(TEST_UNC_PATH, check_both_paths=True),
166
+ "UNC path should be detected via local path conversion")
167
+
168
+ # Test with a local path (should try convert_to_unc)
169
+ assert_true(file_exists(TEST_LOCAL_PATH, check_both_paths=True),
170
+ "Local path should be detected via UNC path conversion")
171
+
172
+
173
+ def test_batch_convert(env):
174
+ """Test batch_convert function."""
175
+ # Get a list of test files
176
+ test_files = env.test_files[:3] # Take the first 3 files
177
+
178
+ # Mock convert_to_local and convert_to_unc to return predictable results
179
+ def mock_convert_to_local(path):
180
+ return Path(str(path) + ".local")
181
+
182
+ def mock_convert_to_unc(path):
183
+ return Path(str(path) + ".unc")
184
+
185
+ with mock.patch('unctools.converter.convert_to_local', side_effect=mock_convert_to_local), \
186
+ mock.patch('unctools.converter.convert_to_unc', side_effect=mock_convert_to_unc):
187
+
188
+ # Test batch convert to UNC
189
+ results = batch_convert([str(f) for f in test_files], to_unc=True)
190
+
191
+ # Verify results
192
+ assert_equal(len(results), len(test_files),
193
+ "Should have results for all input files")
194
+
195
+ for original, converted in results.items():
196
+ assert_equal(converted, original + ".unc",
197
+ f"Converted path for {original} should end with .unc")
198
+
199
+ # Test batch convert to local
200
+ results = batch_convert([str(f) for f in test_files], to_unc=False)
201
+
202
+ # Verify results
203
+ assert_equal(len(results), len(test_files),
204
+ "Should have results for all input files")
205
+
206
+ for original, converted in results.items():
207
+ assert_equal(converted, original + ".local",
208
+ f"Converted path for {original} should end with .local")
209
+
210
+
211
+
212
+ def test_get_unc_path_elements(env):
213
+ """Test get_unc_path_elements function."""
214
+ # Test with a valid UNC path
215
+ result = get_unc_path_elements(TEST_UNC_PATH)
216
+ assert_equal(result, ("server", "share", "folder\\file.txt"),
217
+ "Should extract the correct components")
218
+
219
+ # Test with a UNC path without a relative path
220
+ result = get_unc_path_elements("\\\\server\\share")
221
+ assert_equal(result, ("server", "share", ""),
222
+ "Should extract the correct components without relative path")
223
+
224
+ # Test with a non-UNC path
225
+ result = get_unc_path_elements(TEST_LOCAL_PATH)
226
+ assert_is_none(result, "Should return None for non-UNC path")
227
+
228
+ # Test with forward slashes
229
+ result = get_unc_path_elements("//server/share/folder/file.txt")
230
+ assert_equal(result, ("server", "share", "folder/file.txt"),
231
+ "Should extract the correct components with forward slashes")
232
+
233
+ def test_build_unc_path(env=None):
234
+ """Test build_unc_path function."""
235
+ # Note: This function doesn't need the env parameter but should accept it
236
+ # for consistency with other test functions.
237
+
238
+ # Test with all components
239
+ result = build_unc_path("server", "share", "folder\\file.txt")
240
+ assert_equal(result, "\\\\server\\share\\folder\\file.txt",
241
+ "Should build the correct UNC path")
242
+
243
+ # Test without relative path
244
+ result = build_unc_path("server", "share")
245
+ assert_equal(result, "\\\\server\\share",
246
+ "Should build the correct UNC path without relative path")
247
+
248
+ # Test with empty relative path
249
+ result = build_unc_path("server", "share", "")
250
+ assert_equal(result, "\\\\server\\share",
251
+ "Should build the correct UNC path with empty relative path")
252
+
253
+ # Test with relative path that starts with backslash
254
+ result = build_unc_path("server", "share", "\\folder\\file.txt")
255
+ assert_equal(result, "\\\\server\\share\\folder\\file.txt",
256
+ "Should build the correct UNC path and handle leading backslash")
257
+
258
+ def test_is_path_accessible(env):
259
+ """Test is_path_accessible function."""
260
+ # Test with an accessible file
261
+ test_file = env.test_files[0]
262
+ assert_true(is_path_accessible(test_file), "File should be accessible")
263
+
264
+ # Test with an accessible directory
265
+ assert_true(is_path_accessible(env.temp_dir), "Directory should be accessible")
266
+
267
+ # Test with a non-existent file
268
+ non_existent = os.path.join(env.temp_dir, 'non_existent.txt')
269
+ assert_false(is_path_accessible(non_existent), "Non-existent file should not be accessible")
270
+
271
+ # Test with convert_paths behavior
272
+ # Mock conversion and path access functions
273
+ def mock_convert_to_local(path):
274
+ return Path(env.test_files[0]) # Return a valid file
275
+
276
+ def mock_convert_to_unc(path):
277
+ return Path(env.test_files[0]) # Return a valid file
278
+
279
+ def mock_is_unc_path(path):
280
+ path_str = str(path)
281
+ return path_str.startswith("\\\\") or path_str.startswith("//")
282
+
283
+ with mock.patch('unctools.converter.convert_to_local', side_effect=mock_convert_to_local), \
284
+ mock.patch('unctools.converter.convert_to_unc', side_effect=mock_convert_to_unc), \
285
+ mock.patch('unctools.detector.is_unc_path', side_effect=mock_is_unc_path):
286
+
287
+ # Test with a UNC path (should try convert_to_local)
288
+ assert_true(is_path_accessible(TEST_UNC_PATH, check_both_paths=True),
289
+ "UNC path should be accessible after conversion")
290
+
291
+ # Test with a non-UNC path (should try convert_to_unc)
292
+ assert_true(is_path_accessible(non_existent, check_both_paths=True),
293
+ "Path should be accessible after conversion")
294
+
295
+ def test_find_accessible_path(env):
296
+ """Test find_accessible_path function."""
297
+ # Test with an accessible file
298
+ test_file = env.test_files[0]
299
+ path = find_accessible_path(test_file)
300
+ assert_is_not_none(path, "Accessible path should be found")
301
+ assert_equal(str(path), test_file, "Found path should match the original")
302
+
303
+ # Test with a non-existent file
304
+ non_existent = os.path.join(env.temp_dir, 'non_existent.txt')
305
+
306
+ # Mock conversion and path access functions
307
+ def mock_convert_to_local(path):
308
+ return Path(env.test_files[0]) # Return a valid file
309
+
310
+ def mock_convert_to_unc(path):
311
+ return Path(env.test_files[0]) # Return a valid file
312
+
313
+ def mock_is_unc_path(path):
314
+ path_str = str(path)
315
+ return path_str.startswith("\\\\") or path_str.startswith("//")
316
+
317
+ def mock_is_path_accessible(path, check_both_paths=True):
318
+ return str(path) == env.test_files[0] or \
319
+ str(path) == str(Path(env.test_files[0]))
320
+
321
+ with mock.patch('unctools.converter.convert_to_local', side_effect=mock_convert_to_local), \
322
+ mock.patch('unctools.converter.convert_to_unc', side_effect=mock_convert_to_unc), \
323
+ mock.patch('unctools.detector.is_unc_path', side_effect=mock_is_unc_path), \
324
+ mock.patch('unctools.detector.is_path_accessible', side_effect=mock_is_path_accessible):
325
+
326
+ # Test with a UNC path (should try convert_to_local)
327
+ path = find_accessible_path(TEST_UNC_PATH)
328
+ assert_is_not_none(path, "Accessible path should be found after conversion")
329
+
330
+ # Test with a non-UNC path (should try convert_to_unc)
331
+ path = find_accessible_path(non_existent)
332
+ assert_is_not_none(path, "Accessible path should be found after conversion")
333
+
334
+ # Test with a path that can't be made accessible
335
+ with mock.patch('unctools.detector.is_path_accessible', return_value=False):
336
+ path = find_accessible_path(non_existent)
337
+ assert_is_none(path, "No accessible path should be found")
338
+
339
+
340
+
341
+ def run_tests():
342
+ """Run all operations tests."""
343
+ suite = TestSuite("UNCtools Operations Tests")
344
+
345
+ # Set suite setup and teardown
346
+ suite.set_setup(setup_test_environment)
347
+ suite.set_teardown(teardown_test_environment)
348
+
349
+ # Add tests
350
+ suite.add_test(test_file_exists)
351
+ suite.add_test(test_batch_convert)
352
+ suite.add_test(test_get_unc_path_elements)
353
+ suite.add_test(test_build_unc_path)
354
+ suite.add_test(test_is_path_accessible)
355
+ suite.add_test(test_find_accessible_path)
356
+
357
+ # Run suite
358
+ return run_test_suites([suite])
359
+
360
+ if __name__ == "__main__":
361
+ sys.exit(run_tests())
@@ -32,7 +32,7 @@ Advanced Windows functionality:
32
32
  mappings = get_network_mappings()
33
33
  """
34
34
 
35
- __version__ = "0.1.0"
35
+ __version__ = "0.2.0"
36
36
 
37
37
  import os
38
38
  import sys
@@ -42,16 +42,21 @@ from pathlib import Path
42
42
  # Set up package-level logger
43
43
  logger = logging.getLogger(__name__)
44
44
 
45
- # Import core functionality into the main namespace
46
- from .converter import convert_to_local, convert_to_unc, normalize_path
47
- from .detector import (
48
- is_unc_path, is_network_drive, is_subst_drive,
49
- get_path_type, get_network_mappings, detect_path_issues
45
+ # Import core functionality into the main namespace.
46
+ # 0.2.0 (STACK-MAP D4/D7/D8): normalize_path and the content-I/O wrappers
47
+ # (safe_open, safe_copy, batch_copy, process_files, replace_in_file*) were
48
+ # REMOVED; get_path_type was renamed classify_path_origin (deprecated shim
49
+ # kept through 0.2.x); the operations module dissolved into converter
50
+ # (path algebra) and detector (read-only probes).
51
+ from .converter import (
52
+ convert_to_local, convert_to_unc,
53
+ batch_convert, get_unc_path_elements, build_unc_path,
50
54
  )
51
- from .operations import (
52
- safe_open, safe_copy, batch_convert, batch_copy,
53
- process_files, file_exists, replace_in_file, batch_replace_in_files,
54
- get_unc_path_elements, build_unc_path, is_path_accessible, find_accessible_path
55
+ from .detector import (
56
+ is_unc_path, is_network_drive, is_subst_drive,
57
+ classify_path_origin, get_path_type, # get_path_type = deprecated shim (A4)
58
+ get_network_mappings, detect_path_issues,
59
+ file_exists, is_path_accessible, find_accessible_path,
55
60
  )
56
61
 
57
62
  # Determine if we're running on Windows