Skip to content

Commit a785cf1

Browse files
authored
Merge pull request #62 from mxstack/feature/54-fixed-install-mode
Add fixed install mode for production deployments
2 parents 4c114f0 + 9487408 commit a785cf1

File tree

11 files changed

+212
-15
lines changed

11 files changed

+212
-15
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 4.1.2 (unreleased)
44

5+
- Fix #54: Add `fixed` install mode for non-editable installations to support production and Docker deployments. The new `editable` mode replaces `direct` as the default (same behavior, clearer naming). The `direct` mode is now deprecated but still works with a warning. Install modes: `editable` (with `-e`, for development), `fixed` (without `-e`, for production/Docker), `skip` (clone only).
6+
[jensens]
7+
58
- Fix #35: Add `smart-threading` configuration option to prevent overlapping credential prompts when using HTTPS URLs. When enabled (default), HTTPS packages are processed serially first to ensure clean credential prompts, then other packages are processed in parallel for speed. Can be disabled with `smart-threading = false` if you have credential helpers configured.
69
[jensens]
710

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,24 @@ main-package = -e .[test]
287287
url = git+https://github.com/org/package1.git
288288
branch = feature-branch
289289
extras = test
290+
install-mode = editable
290291

291292
[package2]
292293
url = git+https://github.com/org/package2.git
293294
branch = main
295+
install-mode = fixed
296+
297+
[package3]
298+
url = git+https://github.com/org/package3.git
294299
install-mode = skip
295300
```
296301

302+
**Install mode options:**
303+
- `editable` (default): Installs with `-e` prefix for development
304+
- `fixed`: Installs without `-e` prefix for production/Docker deployments
305+
- `skip`: Only clones, doesn't install
306+
- `direct`: Deprecated alias for `editable` (logs warning)
307+
297308
**Using includes for shared configurations:**
298309
```ini
299310
[settings]

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ The **main section** must be called `[settings]`, even if kept empty.
8686
| `threads` | Number of parallel threads for fetching sources | `4` |
8787
| `smart-threading` | Process HTTPS packages serially to avoid overlapping credential prompts (see below) | `True` |
8888
| `offline` | Skip all VCS fetch operations (handy for offline work) | `False` |
89-
| `default-install-mode` | Default `install-mode` for packages: `direct` or `skip` | `direct` |
89+
| `default-install-mode` | Default `install-mode` for packages: `editable`, `fixed`, or `skip` (see below) | `editable` |
9090
| `default-update` | Default update behavior: `yes` or `no` | `yes` |
9191
| `default-use` | Default use behavior (when false, sources not checked out) | `True` |
9292

@@ -220,7 +220,7 @@ For package sources, the section name is the package name: `[PACKAGENAME]`
220220

221221
| Option | Description | Default |
222222
|--------|-------------|---------|
223-
| `install-mode` | `direct`: Install with `pip -e PACKAGEPATH`<br>`skip`: Only clone, don't install | `default-install-mode` |
223+
| `install-mode` | `editable`: Install with `-e` (development mode)<br>`fixed`: Install without `-e` (production/Docker)<br>`skip`: Only clone, don't install<br>⚠️ `direct` is deprecated, use `editable` | `default-install-mode` |
224224
| `use` | When `false`, source is not checked out and version not overridden | `default-use` |
225225

226226
#### Git-Specific Options

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
@@ -221,9 +221,13 @@ def write_dev_sources(fio, packages: typing.Dict[str, typing.Dict[str, typing.An
221221
continue
222222
extras = f"[{package['extras']}]" if package["extras"] else ""
223223
subdir = f"/{package['subdirectory']}" if package["subdirectory"] else ""
224-
editable = f"""-e ./{package['target']}/{name}{subdir}{extras}\n"""
225-
logger.debug(f"-> {editable.strip()}")
226-
fio.write(editable)
224+
225+
# Add -e prefix only for 'editable' mode (not for 'fixed')
226+
prefix = "-e " if package["install-mode"] == "editable" else ""
227+
install_line = f"""{prefix}./{package['target']}/{name}{subdir}{extras}\n"""
228+
229+
logger.debug(f"-> {install_line.strip()}")
230+
fio.write(install_line)
227231
fio.write("\n\n")
228232

229233

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

0 commit comments

Comments
 (0)