Skip to content

Commit a3341ac

Browse files
committed
feat: better monorepo support by allowing python package to sit in a subfolder
1 parent ed517f0 commit a3341ac

File tree

13 files changed

+265
-14
lines changed

13 files changed

+265
-14
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
- Enhancement: Use tables in the generated sphinx code for topic/domains.
66
[jensens, 02-11-2025]
77

8+
- Feature: Add monorepo support with `PROJECT_PATH_PYTHON` setting.
9+
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.
10+
Useful for monorepos with multiple applications (e.g., frontend + backend).
11+
See the "Monorepo Support" section in getting-started.md for details.
12+
[jensens, 02-11-2025]
13+
814
## 2.0.0 (2025-10-24)
915

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

Makefile

Lines changed: 13 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:
@@ -474,7 +484,7 @@ else
474484
@echo "[settings]" > $(PROJECT_CONFIG)
475485
endif
476486

477-
LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt)
487+
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)
478488

479489
FILES_TARGET:=requirements-mxdev.txt
480490
$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES)

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
@@ -234,7 +234,7 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
234234
if yn not in ["n", "N"]:
235235
factory = template.lookup("mx.ini")
236236
mx_ini_template = factory(
237-
target_folder, domains, get_template_environment()
237+
target_folder, domains, get_template_environment(), domain_settings
238238
)
239239
mx_ini_template.write()
240240
elif not prompt and not preseeds and not (target_folder / "mx.ini").exists():
@@ -244,7 +244,7 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
244244
elif preseeds and "mx-ini" in preseeds and not (target_folder / "mx.ini").exists():
245245
sys.stdout.write("Generate mx configuration file\n")
246246
factory = template.lookup("mx.ini")
247-
mx_ini_template = factory(target_folder, domains, get_template_environment())
247+
mx_ini_template = factory(target_folder, domains, get_template_environment(), domain_settings)
248248
mx_ini_template.write()
249249
else:
250250
sys.stdout.write(
@@ -266,12 +266,28 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
266266
)
267267
for template_name in ci_choice["ci"]:
268268
factory = template.lookup(template_name)
269-
factory(get_template_environment()).write()
269+
factory(get_template_environment(), domain_settings).write()
270270
elif preseeds and "ci-templates" in preseeds:
271271
for template_name in preseeds["ci-templates"]:
272272
sys.stdout.write(f"Generate CI file from {template_name} template\n")
273273
factory = template.lookup(template_name)
274-
factory(get_template_environment()).write()
274+
factory(get_template_environment(), domain_settings).write()
275+
276+
277+
def auto_detect_project_path_python() -> str | None:
278+
"""Auto-detect Python project in subdirectories if not in current directory."""
279+
cwd = Path.cwd()
280+
281+
# Check if pyproject.toml exists in current directory
282+
if (cwd / "pyproject.toml").exists():
283+
return None # Project is in same directory as Makefile
284+
285+
# Search immediate subdirectories for pyproject.toml
286+
for subdir in cwd.iterdir():
287+
if subdir.is_dir() and (subdir / "pyproject.toml").exists():
288+
return subdir.name
289+
290+
return None # No Python project detected
275291

276292

277293
def init_command(args: argparse.Namespace):
@@ -286,12 +302,48 @@ def init_command(args: argparse.Namespace):
286302
with open(args.preseeds) as fd:
287303
preseeds = yaml.load(fd.read(), yaml.SafeLoader)
288304

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

291338

292339
init_parser = command_parsers.add_parser("init", help="Initialize project")
293340
init_parser.set_defaults(func=init_command)
294341
init_parser.add_argument("-p", "--preseeds", help="Preseeds file")
342+
init_parser.add_argument(
343+
"--project-path-python",
344+
help="Path to Python project relative to Makefile (for monorepo setups)",
345+
default=None,
346+
)
295347

296348

297349
def update_command(args: argparse.Namespace):

src/mxmake/templates.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def template_variables(self) -> dict[str, typing.Any]:
265265
name=setting.name,
266266
description=setting.description.split("\n"),
267267
default=setting.default,
268-
value=self.domain_settings[sfqn],
268+
value=self.domain_settings.get(sfqn, setting.default),
269269
)
270270
)
271271
# render domain sections
@@ -333,10 +333,12 @@ def __init__(
333333
target_folder: Path,
334334
domains: list[Domain],
335335
environment: Environment | None = None,
336+
settings: dict[str, str] | None = None,
336337
) -> None:
337338
super().__init__(environment)
338339
self.target_folder = target_folder
339340
self.domains = domains
341+
self.settings = settings or {}
340342

341343
@property
342344
def template_variables(self) -> dict[str, typing.Any]:
@@ -359,7 +361,10 @@ def template_variables(self) -> dict[str, typing.Any]:
359361
),
360362
)
361363
mxmake_templates.append(template)
362-
return dict(mxmake_templates=mxmake_templates, mxmake_env=mxmake_env)
364+
return dict(
365+
mxmake_templates=mxmake_templates,
366+
mxmake_env=mxmake_env,
367+
)
363368

364369

365370
##############################################################################
@@ -442,7 +447,19 @@ def __call__(self, ob: type["Template"]) -> type["Template"]:
442447

443448

444449
class GHActionsTemplate(Template):
445-
template_variables = dict()
450+
def __init__(
451+
self,
452+
environment: Environment | None = None,
453+
settings: dict[str, str] | None = None,
454+
) -> None:
455+
super().__init__(environment)
456+
self.settings = settings or {}
457+
458+
@property
459+
def template_variables(self) -> dict[str, typing.Any]:
460+
return dict(
461+
project_path_python=self.settings.get("PROJECT_PATH_PYTHON", ""),
462+
)
446463

447464
@property
448465
def target_folder(self) -> Path:

src/mxmake/templates/gh-actions-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
uses: astral-sh/setup-uv@v5
1414
with:
1515
enable-cache: true
16-
cache-dependency-glob: "pyproject.toml"
16+
cache-dependency-glob: "{% if project_path_python %}{{ project_path_python }}/{% endif %}pyproject.toml"
1717

1818
- name: Set up Python 3.10
1919
run: uv python install 3.10

src/mxmake/templates/gh-actions-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
uses: astral-sh/setup-uv@v5
2424
with:
2525
enable-cache: true
26-
cache-dependency-glob: "pyproject.toml"
26+
cache-dependency-glob: "{% if project_path_python %}{{ project_path_python }}/{% endif %}pyproject.toml"
2727

2828
- name: Set up Python {{ "${{ matrix.python }}" }}
2929
run: uv python install {{ "${{ matrix.python }}" }}

0 commit comments

Comments
 (0)