Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# Changelog

## 2.0.1
## 2.1.0

- Enhancement: Use tables in the generated sphinx code for topic/domains.
[jensens, 02-11-2025]

- Feature: Add monorepo support with `PROJECT_PATH_PYTHON` setting.
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.
Useful for monorepos with multiple applications (e.g., frontend + backend).
See the "Monorepo Support" section in getting-started.md for details.
- Feature: Add `--version` (`-v`) command line flag to display mxmake version.
[jensens, 02-11-2025]

- Fix: All QA tool domains (ruff, isort, mypy, black, zpretty, pyupgrade, pyrefly) now respect the `PROJECT_PATH_PYTHON` setting when using default source paths.
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`.
[jensens, 03-11-2025]

## 2.0.0 (2025-10-24)

- **Breaking**: Drop Python 3.9 support. Minimum Python version is now 3.10.
Expand Down
31 changes: 28 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ INCLUDE_MAKEFILE?=include.mk
# No default value.
EXTRA_PATH?=

# Path to Python project relative to Makefile (repository root).
# Leave empty if Python project is in the same directory as Makefile.
# For monorepo setups, set to subdirectory name (e.g., `backend`).
# Future-proofed for multi-language monorepos (e.g., PROJECT_PATH_NODEJS).
# No default value.
PROJECT_PATH_PYTHON?=

## core.mxenv

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

# Minimum required Python version.
# Default: 3.9
PYTHON_MIN_VERSION?=3.9
# Default: 3.10
PYTHON_MIN_VERSION?=3.10

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

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

# Helper variable: adds trailing slash to PROJECT_PATH_PYTHON only if non-empty
PYTHON_PROJECT_PREFIX=$(if $(PROJECT_PATH_PYTHON),$(PROJECT_PATH_PYTHON)/,)

# Defensive settings for make: https://tech.davis-hansson.com/p/make/
SHELL:=bash
.ONESHELL:
Expand Down Expand Up @@ -337,6 +347,11 @@ CLEAN_TARGETS+=mxenv-clean
# ruff
##############################################################################

# Adjust RUFF_SRC to respect PROJECT_PATH_PYTHON if still at default
ifeq ($(RUFF_SRC),src)
RUFF_SRC:=$(PYTHON_PROJECT_PREFIX)src
endif

RUFF_TARGET:=$(SENTINEL_FOLDER)/ruff.sentinel
$(RUFF_TARGET): $(MXENV_TARGET)
@echo "Install Ruff"
Expand Down Expand Up @@ -372,6 +387,11 @@ CLEAN_TARGETS+=ruff-clean
# isort
##############################################################################

# Adjust ISORT_SRC to respect PROJECT_PATH_PYTHON if still at default
ifeq ($(ISORT_SRC),src)
ISORT_SRC:=$(PYTHON_PROJECT_PREFIX)src
endif

ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel
$(ISORT_TARGET): $(MXENV_TARGET)
@echo "Install isort"
Expand Down Expand Up @@ -474,7 +494,7 @@ else
@echo "[settings]" > $(PROJECT_CONFIG)
endif

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

FILES_TARGET:=requirements-mxdev.txt
$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES)
Expand Down Expand Up @@ -609,6 +629,11 @@ CLEAN_TARGETS+=coverage-clean
# mypy
##############################################################################

# Adjust MYPY_SRC to respect PROJECT_PATH_PYTHON if still at default
ifeq ($(MYPY_SRC),src)
MYPY_SRC:=$(PYTHON_PROJECT_PREFIX)src
endif

MYPY_TARGET:=$(SENTINEL_FOLDER)/mypy.sentinel
$(MYPY_TARGET): $(MXENV_TARGET)
@echo "Install mypy"
Expand Down
95 changes: 95 additions & 0 deletions docs/source/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,101 @@ Both methods create:

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

## Monorepo Support

mxmake supports monorepo setups where your Python project lives in a subdirectory while the Makefile stays at the repository root.
This is useful when you have multiple applications (e.g., frontend + backend) in one repository.
You may need to edit `mx.ini` and set `requirements-in` or `main-package` to the subfolder.

### Example Directory Structure

```
myrepo/ # Repository root
├── Makefile # Generated by mxmake (at root)
├── mx.ini # mxdev config (at root)
├── .venv/ # Virtual environment (at root)
├── .mxmake/ # Generated files (at root)
├── sources/ # mxdev packages (at root)
├── frontend/ # Frontend application
│ └── package.json
└── backend/ # Python project in subdirectory
├── pyproject.toml
├── src/
│ └── myapp/
└── tests/
```

### Setup Methods

#### Method 1: Auto-detection (Recommended)

If `pyproject.toml` is in a subdirectory, mxmake will detect it automatically:

```shell
cd myrepo
uvx mxmake init
# mxmake will prompt: "Use 'backend' as PROJECT_PATH_PYTHON? (Y/n)"
```

#### Method 2: CLI Flag

Specify the project path explicitly:

```shell
uvx mxmake init --project-path-python backend
```

#### Method 3: Preseed File

Include in your preseed YAML:

```yaml
topics:
core:
base:
PROJECT_PATH_PYTHON: backend
```

Then run:

```shell
uvx mxmake init --preseeds preseed.yaml
```

### What Happens

When `PROJECT_PATH_PYTHON` is set:

1. **Makefile**: References Python package files with the correct path
- Looks for `backend/pyproject.toml` instead of `./pyproject.toml`

2. **mx.ini**: Configure test/coverage paths relative to repository root
- Set `mxmake-test-path = backend/tests` and `mxmake-source-path = backend/src`

3. **GitHub Actions**: Cache uses correct path
- `cache-dependency-glob: "backend/pyproject.toml"`

### Configuration

The `PROJECT_PATH_PYTHON` setting appears in your Makefile:

```makefile
# Path to Python project relative to Makefile (repository root)
PROJECT_PATH_PYTHON?=backend
```

In your `mx.ini`, specify paths relative to the repository root (including the project path):

```ini
[settings]
mxmake-test-path = backend/tests
mxmake-source-path = backend/src
```

```{important}
Future-proofing: This setting is named `PROJECT_PATH_PYTHON` to allow for future `PROJECT_PATH_NODEJS` support in multi-language monorepos.
```

## How to change the settings

The `Makefile` consists of three sections:
Expand Down
51 changes: 50 additions & 1 deletion docs/source/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,56 @@ This guide documents breaking changes between mxmake versions and how to migrate

## Version 2.0.1 (unreleased)

**No breaking changes**
### Added: Monorepo Support

**New Feature**: Python projects can now be located in a subdirectory relative to the Makefile.

**Purpose**: Support monorepo setups with multiple applications (e.g., frontend + backend) in one repository.

**New Setting**: `PROJECT_PATH_PYTHON` in the `core.base` domain.

**Example Configuration**:
```makefile
# In your Makefile
PROJECT_PATH_PYTHON?=backend
```

```ini
# In your mx.ini (specify full paths from repo root)
[settings]
mxmake-test-path = backend/tests
mxmake-source-path = backend/src
```

**Setup Methods**:

1. **Auto-detection** (recommended):
```shell
uvx mxmake init
# Prompts if pyproject.toml found in subdirectory
```

2. **CLI flag**:
```shell
uvx mxmake init --project-path-python backend
```

3. **Preseed file**:
```yaml
topics:
core:
base:
PROJECT_PATH_PYTHON: backend
```

**What Changes**:
- Makefile references: `backend/pyproject.toml` instead of `./pyproject.toml`
- mx.ini paths: Specify full paths from repository root (e.g., `mxmake-test-path = backend/tests`)
- GitHub Actions: Cache uses `backend/pyproject.toml` for dependency tracking

**Migration**: None required. This is an opt-in feature with no impact on existing projects.

**See Also**: [Monorepo Support](getting-started.html#monorepo-support) in Getting Started guide.

## Version 2.0.0 (2025-10-24)

Expand Down
60 changes: 56 additions & 4 deletions src/mxmake/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
if yn not in ["n", "N"]:
factory = template.lookup("mx.ini")
mx_ini_template = factory(
target_folder, domains, get_template_environment()
target_folder, domains, get_template_environment(), domain_settings
)
mx_ini_template.write()
elif not prompt and not preseeds and not (target_folder / "mx.ini").exists():
Expand All @@ -251,7 +251,7 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
elif preseeds and "mx-ini" in preseeds and not (target_folder / "mx.ini").exists():
sys.stdout.write("Generate mx configuration file\n")
factory = template.lookup("mx.ini")
mx_ini_template = factory(target_folder, domains, get_template_environment())
mx_ini_template = factory(target_folder, domains, get_template_environment(), domain_settings)
mx_ini_template.write()
else:
sys.stdout.write(
Expand All @@ -273,12 +273,28 @@ def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
)
for template_name in ci_choice["ci"]:
factory = template.lookup(template_name)
factory(get_template_environment()).write()
factory(get_template_environment(), domain_settings).write()
elif preseeds and "ci-templates" in preseeds:
for template_name in preseeds["ci-templates"]:
sys.stdout.write(f"Generate CI file from {template_name} template\n")
factory = template.lookup(template_name)
factory(get_template_environment()).write()
factory(get_template_environment(), domain_settings).write()


def auto_detect_project_path_python() -> str | None:
"""Auto-detect Python project in subdirectories if not in current directory."""
cwd = Path.cwd()

# Check if pyproject.toml exists in current directory
if (cwd / "pyproject.toml").exists():
return None # Project is in same directory as Makefile

# Search immediate subdirectories for pyproject.toml
for subdir in cwd.iterdir():
if subdir.is_dir() and (subdir / "pyproject.toml").exists():
return subdir.name

return None # No Python project detected


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

# Handle project-path-python from CLI or auto-detection
project_path_python = args.project_path_python
if project_path_python is None and not args.preseeds:
# Try auto-detection only if not using preseeds
detected_path = auto_detect_project_path_python()
if detected_path:
sys.stdout.write(
f"Auto-detected Python project in subdirectory: {detected_path}\n"
)
if prompt:
yn = inquirer.text(
message=f"Use '{detected_path}' as PROJECT_PATH_PYTHON? (Y/n)"
)
if yn not in ["n", "N"]:
project_path_python = detected_path
else:
project_path_python = detected_path

# Inject project-path-python into preseeds if specified or detected
if project_path_python:
if preseeds is None:
preseeds = {}
if "topics" not in preseeds:
preseeds["topics"] = {}
if "core" not in preseeds["topics"]:
preseeds["topics"]["core"] = {}
if "base" not in preseeds["topics"]["core"]:
preseeds["topics"]["core"]["base"] = {}
preseeds["topics"]["core"]["base"]["PROJECT_PATH_PYTHON"] = project_path_python
sys.stdout.write(f"Setting PROJECT_PATH_PYTHON={project_path_python}\n\n")

create_config(prompt=prompt, preseeds=preseeds)


init_parser = command_parsers.add_parser("init", help="Initialize project")
init_parser.set_defaults(func=init_command)
init_parser.add_argument("-p", "--preseeds", help="Preseeds file")
init_parser.add_argument(
"--project-path-python",
help="Path to Python project relative to Makefile (for monorepo setups)",
default=None,
)


def update_command(args: argparse.Namespace):
Expand Down
Loading