Skip to content

Commit bd84756

Browse files
committed
Add fixed install mode and deprecate direct mode (issue #54)
This change adds support for non-editable installations to enable production/Docker deployments while maintaining backward compatibility. Changes: - Add 'fixed' install mode for non-editable installations (no -e prefix) - Add 'editable' install mode (with -e prefix) as the new default - Deprecate 'direct' mode as alias for 'editable' with warning - Keep 'skip' mode unchanged Test-Driven Development approach: - Added 4 new test configuration files - Added 4 new configuration tests - Added 2 new processing tests - Updated 1 existing test for new default - All 172 tests pass Fixes #54
1 parent 300c3b0 commit bd84756

File tree

8 files changed

+196
-13
lines changed

8 files changed

+196
-13
lines changed

src/mxdev/config.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,22 @@ def __init__(
5050
# overlapping credential prompts)
5151
settings.setdefault("smart-threading", "true")
5252

53-
mode = settings.get("default-install-mode", "direct")
54-
if mode not in ["direct", "skip"]:
55-
raise ValueError("default-install-mode must be one of 'direct' or 'skip'")
53+
mode = settings.get("default-install-mode", "editable")
54+
55+
# Handle deprecated "direct" mode
56+
if mode == "direct":
57+
logger.warning(
58+
"install-mode 'direct' is deprecated and will be removed in a future version. "
59+
"Please use 'editable' instead."
60+
)
61+
mode = "editable"
62+
settings["default-install-mode"] = "editable"
63+
64+
if mode not in ["editable", "fixed", "skip"]:
65+
raise ValueError(
66+
"default-install-mode must be one of 'editable', 'fixed', or 'skip' "
67+
"('direct' is deprecated, use 'editable')"
68+
)
5669

5770
default_use = to_bool(settings.get("default-use", True))
5871
raw_overrides = settings.get("version-overrides", "").strip()
@@ -108,9 +121,21 @@ def is_ns_member(name) -> bool:
108121
package.setdefault("path", os.path.join(package["target"], name))
109122
if not package.get("url"):
110123
raise ValueError(f"Section {name} has no URL set!")
111-
if package.get("install-mode") not in ["direct", "skip"]:
124+
125+
# Handle deprecated "direct" mode for per-package install-mode
126+
pkg_mode = package.get("install-mode")
127+
if pkg_mode == "direct":
128+
logger.warning(
129+
f"install-mode 'direct' is deprecated and will be removed in a future version. "
130+
f"Please use 'editable' instead (package: {name})."
131+
)
132+
package["install-mode"] = "editable"
133+
pkg_mode = "editable"
134+
135+
if pkg_mode not in ["editable", "fixed", "skip"]:
112136
raise ValueError(
113-
f"install-mode in [{name}] must be one of 'direct' or 'skip'"
137+
f"install-mode in [{name}] must be one of 'editable', 'fixed', or 'skip' "
138+
f"('direct' is deprecated, use 'editable')"
114139
)
115140

116141
# repo_dir = os.path.abspath(f"{package['target']}/{name}")

src/mxdev/processing.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,13 @@ def write_dev_sources(fio, packages: typing.Dict[str, typing.Dict[str, typing.An
220220
continue
221221
extras = f"[{package['extras']}]" if package["extras"] else ""
222222
subdir = f"/{package['subdirectory']}" if package["subdirectory"] else ""
223-
editable = f"""-e ./{package['target']}/{name}{subdir}{extras}\n"""
224-
logger.debug(f"-> {editable.strip()}")
225-
fio.write(editable)
223+
224+
# Add -e prefix only for 'editable' mode (not for 'fixed')
225+
prefix = "-e " if package["install-mode"] == "editable" else ""
226+
install_line = f"""{prefix}./{package['target']}/{name}{subdir}{extras}\n"""
227+
228+
logger.debug(f"-> {install_line.strip()}")
229+
fio.write(install_line)
226230
fio.write("\n\n")
227231

228232

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[settings]
2+
default-install-mode = direct
3+
4+
[example.package]
5+
url = git+https://github.com/example/package.git
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[settings]
2+
default-install-mode = editable
3+
4+
[example.package]
5+
url = git+https://github.com/example/package.git
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[settings]
2+
default-install-mode = fixed
3+
4+
[example.package]
5+
url = git+https://github.com/example/package.git
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[settings]
2+
default-install-mode = editable
3+
4+
[example.package]
5+
url = git+https://github.com/example/package.git
6+
install-mode = direct

tests/test_config.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,72 @@ def test_configuration_with_ignores():
8989
assert "another.ignored" in config.ignore_keys
9090

9191

92+
def test_configuration_editable_install_mode():
93+
"""Test Configuration with editable install mode."""
94+
from mxdev.config import Configuration
95+
96+
base = pathlib.Path(__file__).parent / "data" / "config_samples"
97+
config = Configuration(str(base / "config_editable_mode.ini"))
98+
99+
assert config.settings["default-install-mode"] == "editable"
100+
assert config.packages["example.package"]["install-mode"] == "editable"
101+
102+
103+
def test_configuration_fixed_install_mode():
104+
"""Test Configuration with fixed install mode."""
105+
from mxdev.config import Configuration
106+
107+
base = pathlib.Path(__file__).parent / "data" / "config_samples"
108+
config = Configuration(str(base / "config_fixed_mode.ini"))
109+
110+
assert config.settings["default-install-mode"] == "fixed"
111+
assert config.packages["example.package"]["install-mode"] == "fixed"
112+
113+
114+
def test_configuration_direct_mode_deprecated(caplog):
115+
"""Test Configuration with deprecated 'direct' mode logs warning."""
116+
from mxdev.config import Configuration
117+
118+
base = pathlib.Path(__file__).parent / "data" / "config_samples"
119+
config = Configuration(str(base / "config_deprecated_direct.ini"))
120+
121+
# Mode should be treated as 'editable' internally
122+
assert config.settings["default-install-mode"] == "editable"
123+
assert config.packages["example.package"]["install-mode"] == "editable"
124+
125+
# Should have logged deprecation warning
126+
assert any(
127+
"install-mode 'direct' is deprecated" in record.message
128+
for record in caplog.records
129+
)
130+
131+
132+
def test_configuration_package_direct_mode_deprecated(caplog):
133+
"""Test per-package 'direct' mode logs deprecation warning."""
134+
from mxdev.config import Configuration
135+
136+
base = pathlib.Path(__file__).parent / "data" / "config_samples"
137+
config = Configuration(str(base / "config_package_direct.ini"))
138+
139+
# Package mode should be treated as 'editable' internally
140+
assert config.packages["example.package"]["install-mode"] == "editable"
141+
142+
# Should have logged deprecation warning
143+
assert any(
144+
"install-mode 'direct' is deprecated" in record.message
145+
for record in caplog.records
146+
)
147+
148+
92149
def test_configuration_invalid_default_install_mode():
93150
"""Test Configuration with invalid default-install-mode."""
94151
from mxdev.config import Configuration
95152

96153
base = pathlib.Path(__file__).parent / "data" / "config_samples"
97-
with pytest.raises(ValueError, match="default-install-mode must be one of"):
154+
with pytest.raises(
155+
ValueError,
156+
match=r"default-install-mode must be one of 'editable', 'fixed', or 'skip'",
157+
):
98158
Configuration(str(base / "config_invalid_mode.ini"))
99159

100160

@@ -103,7 +163,10 @@ def test_configuration_invalid_package_install_mode():
103163
from mxdev.config import Configuration
104164

105165
base = pathlib.Path(__file__).parent / "data" / "config_samples"
106-
with pytest.raises(ValueError, match="install-mode in .* must be one of"):
166+
with pytest.raises(
167+
ValueError,
168+
match=r"install-mode in .* must be one of 'editable', 'fixed', or 'skip'",
169+
):
107170
Configuration(str(base / "config_package_invalid_mode.ini"))
108171

109172

@@ -182,7 +245,7 @@ def test_configuration_package_defaults():
182245
assert pkg["extras"] == ""
183246
assert pkg["subdirectory"] == ""
184247
assert pkg["target"] == "sources" # default-target not set, should be "sources"
185-
assert pkg["install-mode"] == "direct" # default mode
248+
assert pkg["install-mode"] == "editable" # default mode changed from 'direct'
186249
assert pkg["vcs"] == "git"
187250
assert "path" in pkg
188251

tests/test_processing.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def test_write_dev_sources(tmp_path):
215215
"target": "sources",
216216
"extras": "",
217217
"subdirectory": "",
218-
"install-mode": "direct",
218+
"install-mode": "editable",
219219
},
220220
"skip.package": {
221221
"target": "sources",
@@ -227,7 +227,7 @@ def test_write_dev_sources(tmp_path):
227227
"target": "sources",
228228
"extras": "test,docs",
229229
"subdirectory": "packages/core",
230-
"install-mode": "direct",
230+
"install-mode": "editable",
231231
},
232232
}
233233

@@ -241,6 +241,76 @@ def test_write_dev_sources(tmp_path):
241241
assert "-e ./sources/extras.package/packages/core[test,docs]" in content
242242

243243

244+
def test_write_dev_sources_fixed_mode(tmp_path):
245+
"""Test write_dev_sources with fixed install mode (no -e prefix)."""
246+
from mxdev.processing import write_dev_sources
247+
248+
packages = {
249+
"fixed.package": {
250+
"target": "sources",
251+
"extras": "",
252+
"subdirectory": "",
253+
"install-mode": "fixed",
254+
},
255+
"fixed.with.extras": {
256+
"target": "sources",
257+
"extras": "test",
258+
"subdirectory": "packages/core",
259+
"install-mode": "fixed",
260+
},
261+
}
262+
263+
outfile = tmp_path / "requirements.txt"
264+
with open(outfile, "w") as fio:
265+
write_dev_sources(fio, packages)
266+
267+
content = outfile.read_text()
268+
# Fixed mode should NOT have -e prefix
269+
assert "./sources/fixed.package" in content
270+
assert "-e ./sources/fixed.package" not in content
271+
assert "./sources/fixed.with.extras/packages/core[test]" in content
272+
assert "-e ./sources/fixed.with.extras/packages/core[test]" not in content
273+
274+
275+
def test_write_dev_sources_mixed_modes(tmp_path):
276+
"""Test write_dev_sources with mixed install modes."""
277+
from mxdev.processing import write_dev_sources
278+
279+
packages = {
280+
"editable.package": {
281+
"target": "sources",
282+
"extras": "",
283+
"subdirectory": "",
284+
"install-mode": "editable",
285+
},
286+
"fixed.package": {
287+
"target": "sources",
288+
"extras": "",
289+
"subdirectory": "",
290+
"install-mode": "fixed",
291+
},
292+
"skip.package": {
293+
"target": "sources",
294+
"extras": "",
295+
"subdirectory": "",
296+
"install-mode": "skip",
297+
},
298+
}
299+
300+
outfile = tmp_path / "requirements.txt"
301+
with open(outfile, "w") as fio:
302+
write_dev_sources(fio, packages)
303+
304+
content = outfile.read_text()
305+
# Editable should have -e prefix
306+
assert "-e ./sources/editable.package" in content
307+
# Fixed should NOT have -e prefix
308+
assert "./sources/fixed.package" in content
309+
assert "-e ./sources/fixed.package" not in content
310+
# Skip should not appear at all
311+
assert "skip.package" not in content
312+
313+
244314
def test_write_dev_sources_empty():
245315
"""Test write_dev_sources with no packages."""
246316
from mxdev.processing import write_dev_sources

0 commit comments

Comments
 (0)