Skip to content

Commit 8242e3b

Browse files
authored
Merge pull request #60 from mxstack/feature/project-path-python
feat: monorepo support by allowing python package to be in subfolder
2 parents baef7f0 + c075006 commit 8242e3b

File tree

20 files changed

+318
-16
lines changed

20 files changed

+318
-16
lines changed

CHANGES.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
# Changelog
22

3-
## 2.0.1
3+
## 2.1.0
44

55
- Enhancement: Use tables in the generated sphinx code for topic/domains.
66
[jensens, 02-11-2025]
7-
7+
- Feature: Add monorepo support with `PROJECT_PATH_PYTHON` setting.
8+
Python projects can now be located in subdirectories while keeping the Makefile at the repository root. Includes auto-detection of `pyproject.toml` in subdirectories on init, `--project-path-python` CLI flag and preseed file support.
9+
Useful for monorepos with multiple applications (e.g., frontend + backend).
10+
See the "Monorepo Support" section in getting-started.md for details.
811
- Feature: Add `--version` (`-v`) command line flag to display mxmake version.
912
[jensens, 02-11-2025]
1013

14+
- Fix: All QA tool domains (ruff, isort, mypy, black, zpretty, pyupgrade, pyrefly) now respect the `PROJECT_PATH_PYTHON` setting when using default source paths.
15+
When `PROJECT_PATH_PYTHON` is set (e.g., to `backend`), the tools automatically look for source code in the correct subdirectory (e.g., `backend/src`) instead of just `src`.
16+
[jensens, 03-11-2025]
17+
1118
## 2.0.0 (2025-10-24)
1219

1320
- **Breaking**: Drop Python 3.9 support. Minimum Python version is now 3.10.

Makefile

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ INCLUDE_MAKEFILE?=include.mk
4242
# No default value.
4343
EXTRA_PATH?=
4444

45+
# Path to Python project relative to Makefile (repository root).
46+
# Leave empty if Python project is in the same directory as Makefile.
47+
# For monorepo setups, set to subdirectory name (e.g., `backend`).
48+
# Future-proofed for multi-language monorepos (e.g., PROJECT_PATH_NODEJS).
49+
# No default value.
50+
PROJECT_PATH_PYTHON?=
51+
4552
## core.mxenv
4653

4754
# Primary Python interpreter to use. It is used to create the
@@ -53,8 +60,8 @@ EXTRA_PATH?=
5360
PRIMARY_PYTHON?=3.14
5461

5562
# Minimum required Python version.
56-
# Default: 3.9
57-
PYTHON_MIN_VERSION?=3.9
63+
# Default: 3.10
64+
PYTHON_MIN_VERSION?=3.10
5865

5966
# Install packages using the given package installer method.
6067
# Supported are `pip` and `uv`. When `uv` is selected, a global installation
@@ -194,6 +201,9 @@ FORMAT_TARGETS?=
194201

195202
export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH)
196203

204+
# Helper variable: adds trailing slash to PROJECT_PATH_PYTHON only if non-empty
205+
PYTHON_PROJECT_PREFIX=$(if $(PROJECT_PATH_PYTHON),$(PROJECT_PATH_PYTHON)/,)
206+
197207
# Defensive settings for make: https://tech.davis-hansson.com/p/make/
198208
SHELL:=bash
199209
.ONESHELL:
@@ -337,6 +347,11 @@ CLEAN_TARGETS+=mxenv-clean
337347
# ruff
338348
##############################################################################
339349

350+
# Adjust RUFF_SRC to respect PROJECT_PATH_PYTHON if still at default
351+
ifeq ($(RUFF_SRC),src)
352+
RUFF_SRC:=$(PYTHON_PROJECT_PREFIX)src
353+
endif
354+
340355
RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel
341356
$(RUFF_TARGET): $(MXENV_TARGET)
342357
@echo "Install Ruff"
@@ -372,6 +387,11 @@ CLEAN_TARGETS+=ruff-clean
372387
# isort
373388
##############################################################################
374389

390+
# Adjust ISORT_SRC to respect PROJECT_PATH_PYTHON if still at default
391+
ifeq ($(ISORT_SRC),src)
392+
ISORT_SRC:=$(PYTHON_PROJECT_PREFIX)src
393+
endif
394+
375395
ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel
376396
$(ISORT_TARGET): $(MXENV_TARGET)
377397
@echo "Install isort"
@@ -474,7 +494,7 @@ else
474494
@echo "[settings]" > $(PROJECT_CONFIG)
475495
endif
476496

477-
LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt)
497+
LOCAL_PACKAGE_FILES:=$(wildcard $(PYTHON_PROJECT_PREFIX)pyproject.toml $(PYTHON_PROJECT_PREFIX)setup.cfg $(PYTHON_PROJECT_PREFIX)setup.py $(PYTHON_PROJECT_PREFIX)requirements.txt $(PYTHON_PROJECT_PREFIX)constraints.txt)
478498

479499
FILES_TARGET:=requirements-mxdev.txt
480500
$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES)
@@ -609,6 +629,11 @@ CLEAN_TARGETS+=coverage-clean
609629
# mypy
610630
##############################################################################
611631

632+
# Adjust MYPY_SRC to respect PROJECT_PATH_PYTHON if still at default
633+
ifeq ($(MYPY_SRC),src)
634+
MYPY_SRC:=$(PYTHON_PROJECT_PREFIX)src
635+
endif
636+
612637
MYPY_TARGET:=$(SENTINEL_FOLDER)/mypy.sentinel
613638
$(MYPY_TARGET): $(MXENV_TARGET)
614639
@echo "Install mypy"

docs/source/getting-started.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,101 @@ Both methods create:
9191

9292
To update an existing Makefile without interactive prompts, run `mxmake update`.
9393

94+
## Monorepo Support
95+
96+
mxmake supports monorepo setups where your Python project lives in a subdirectory while the Makefile stays at the repository root.
97+
This is useful when you have multiple applications (e.g., frontend + backend) in one repository.
98+
You may need to edit `mx.ini` and set `requirements-in` or `main-package` to the subfolder.
99+
100+
### Example Directory Structure
101+
102+
```
103+
myrepo/ # Repository root
104+
├── Makefile # Generated by mxmake (at root)
105+
├── mx.ini # mxdev config (at root)
106+
├── .venv/ # Virtual environment (at root)
107+
├── .mxmake/ # Generated files (at root)
108+
├── sources/ # mxdev packages (at root)
109+
├── frontend/ # Frontend application
110+
│ └── package.json
111+
└── backend/ # Python project in subdirectory
112+
├── pyproject.toml
113+
├── src/
114+
│ └── myapp/
115+
└── tests/
116+
```
117+
118+
### Setup Methods
119+
120+
#### Method 1: Auto-detection (Recommended)
121+
122+
If `pyproject.toml` is in a subdirectory, mxmake will detect it automatically:
123+
124+
```shell
125+
cd myrepo
126+
uvx mxmake init
127+
# mxmake will prompt: "Use 'backend' as PROJECT_PATH_PYTHON? (Y/n)"
128+
```
129+
130+
#### Method 2: CLI Flag
131+
132+
Specify the project path explicitly:
133+
134+
```shell
135+
uvx mxmake init --project-path-python backend
136+
```
137+
138+
#### Method 3: Preseed File
139+
140+
Include in your preseed YAML:
141+
142+
```yaml
143+
topics:
144+
core:
145+
base:
146+
PROJECT_PATH_PYTHON: backend
147+
```
148+
149+
Then run:
150+
151+
```shell
152+
uvx mxmake init --preseeds preseed.yaml
153+
```
154+
155+
### What Happens
156+
157+
When `PROJECT_PATH_PYTHON` is set:
158+
159+
1. **Makefile**: References Python package files with the correct path
160+
- Looks for `backend/pyproject.toml` instead of `./pyproject.toml`
161+
162+
2. **mx.ini**: Configure test/coverage paths relative to repository root
163+
- Set `mxmake-test-path = backend/tests` and `mxmake-source-path = backend/src`
164+
165+
3. **GitHub Actions**: Cache uses correct path
166+
- `cache-dependency-glob: "backend/pyproject.toml"`
167+
168+
### Configuration
169+
170+
The `PROJECT_PATH_PYTHON` setting appears in your Makefile:
171+
172+
```makefile
173+
# Path to Python project relative to Makefile (repository root)
174+
PROJECT_PATH_PYTHON?=backend
175+
```
176+
177+
In your `mx.ini`, specify paths relative to the repository root (including the project path):
178+
179+
```ini
180+
[settings]
181+
mxmake-test-path = backend/tests
182+
mxmake-source-path = backend/src
183+
```
184+
185+
```{important}
186+
Future-proofing: This setting is named `PROJECT_PATH_PYTHON` to allow for future `PROJECT_PATH_NODEJS` support in multi-language monorepos.
187+
```
188+
94189
## How to change the settings
95190

96191
The `Makefile` consists of three sections:

docs/source/migration.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,56 @@ This guide documents breaking changes between mxmake versions and how to migrate
44

55
## Version 2.0.1 (unreleased)
66

7-
**No breaking changes**
7+
### Added: Monorepo Support
8+
9+
**New Feature**: Python projects can now be located in a subdirectory relative to the Makefile.
10+
11+
**Purpose**: Support monorepo setups with multiple applications (e.g., frontend + backend) in one repository.
12+
13+
**New Setting**: `PROJECT_PATH_PYTHON` in the `core.base` domain.
14+
15+
**Example Configuration**:
16+
```makefile
17+
# In your Makefile
18+
PROJECT_PATH_PYTHON?=backend
19+
```
20+
21+
```ini
22+
# In your mx.ini (specify full paths from repo root)
23+
[settings]
24+
mxmake-test-path = backend/tests
25+
mxmake-source-path = backend/src
26+
```
27+
28+
**Setup Methods**:
29+
30+
1. **Auto-detection** (recommended):
31+
```shell
32+
uvx mxmake init
33+
# Prompts if pyproject.toml found in subdirectory
34+
```
35+
36+
2. **CLI flag**:
37+
```shell
38+
uvx mxmake init --project-path-python backend
39+
```
40+
41+
3. **Preseed file**:
42+
```yaml
43+
topics:
44+
core:
45+
base:
46+
PROJECT_PATH_PYTHON: backend
47+
```
48+
49+
**What Changes**:
50+
- Makefile references: `backend/pyproject.toml` instead of `./pyproject.toml`
51+
- mx.ini paths: Specify full paths from repository root (e.g., `mxmake-test-path = backend/tests`)
52+
- GitHub Actions: Cache uses `backend/pyproject.toml` for dependency tracking
53+
54+
**Migration**: None required. This is an opt-in feature with no impact on existing projects.
55+
56+
**See Also**: [Monorepo Support](getting-started.html#monorepo-support) in Getting Started guide.
857

958
## Version 2.0.0 (2025-10-24)
1059

src/mxmake/main.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
241241
if yn not in ["n", "N"]:
242242
factory = template.lookup("mx.ini")
243243
mx_ini_template = factory(
244-
target_folder, domains, get_template_environment()
244+
target_folder, domains, get_template_environment(), domain_settings
245245
)
246246
mx_ini_template.write()
247247
elif not prompt and not preseeds and not (target_folder / "mx.ini").exists():
@@ -251,7 +251,7 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
251251
elif preseeds and "mx-ini" in preseeds and not (target_folder / "mx.ini").exists():
252252
sys.stdout.write("Generate mx configuration file\n")
253253
factory = template.lookup("mx.ini")
254-
mx_ini_template = factory(target_folder, domains, get_template_environment())
254+
mx_ini_template = factory(target_folder, domains, get_template_environment(), domain_settings)
255255
mx_ini_template.write()
256256
else:
257257
sys.stdout.write(
@@ -273,12 +273,28 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
273273
)
274274
for template_name in ci_choice["ci"]:
275275
factory = template.lookup(template_name)
276-
factory(get_template_environment()).write()
276+
factory(get_template_environment(), domain_settings).write()
277277
elif preseeds and "ci-templates" in preseeds:
278278
for template_name in preseeds["ci-templates"]:
279279
sys.stdout.write(f"Generate CI file from {template_name} template\n")
280280
factory = template.lookup(template_name)
281-
factory(get_template_environment()).write()
281+
factory(get_template_environment(), domain_settings).write()
282+
283+
284+
def auto_detect_project_path_python() -> str | None:
285+
"""Auto-detect Python project in subdirectories if not in current directory."""
286+
cwd = Path.cwd()
287+
288+
# Check if pyproject.toml exists in current directory
289+
if (cwd / "pyproject.toml").exists():
290+
return None # Project is in same directory as Makefile
291+
292+
# Search immediate subdirectories for pyproject.toml
293+
for subdir in cwd.iterdir():
294+
if subdir.is_dir() and (subdir / "pyproject.toml").exists():
295+
return subdir.name
296+
297+
return None # No Python project detected
282298

283299

284300
def init_command(args: argparse.Namespace):
@@ -293,12 +309,48 @@ def init_command(args: argparse.Namespace):
293309
with open(args.preseeds) as fd:
294310
preseeds = yaml.load(fd.read(), yaml.SafeLoader)
295311

312+
# Handle project-path-python from CLI or auto-detection
313+
project_path_python = args.project_path_python
314+
if project_path_python is None and not args.preseeds:
315+
# Try auto-detection only if not using preseeds
316+
detected_path = auto_detect_project_path_python()
317+
if detected_path:
318+
sys.stdout.write(
319+
f"Auto-detected Python project in subdirectory: {detected_path}\n"
320+
)
321+
if prompt:
322+
yn = inquirer.text(
323+
message=f"Use '{detected_path}' as PROJECT_PATH_PYTHON? (Y/n)"
324+
)
325+
if yn not in ["n", "N"]:
326+
project_path_python = detected_path
327+
else:
328+
project_path_python = detected_path
329+
330+
# Inject project-path-python into preseeds if specified or detected
331+
if project_path_python:
332+
if preseeds is None:
333+
preseeds = {}
334+
if "topics" not in preseeds:
335+
preseeds["topics"] = {}
336+
if "core" not in preseeds["topics"]:
337+
preseeds["topics"]["core"] = {}
338+
if "base" not in preseeds["topics"]["core"]:
339+
preseeds["topics"]["core"]["base"] = {}
340+
preseeds["topics"]["core"]["base"]["PROJECT_PATH_PYTHON"] = project_path_python
341+
sys.stdout.write(f"Setting PROJECT_PATH_PYTHON={project_path_python}\n\n")
342+
296343
create_config(prompt=prompt, preseeds=preseeds)
297344

298345

299346
init_parser = command_parsers.add_parser("init", help="Initialize project")
300347
init_parser.set_defaults(func=init_command)
301348
init_parser.add_argument("-p", "--preseeds", help="Preseeds file")
349+
init_parser.add_argument(
350+
"--project-path-python",
351+
help="Path to Python project relative to Makefile (for monorepo setups)",
352+
default=None,
353+
)
302354

303355

304356
def update_command(args: argparse.Namespace):

0 commit comments

Comments
 (0)