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
4 changes: 0 additions & 4 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ jobs:
- windows-latest
- macos-latest
python-config:
- version: "3.8"
tox-env: "py38"
- version: "3.9"
tox-env: "py39"
- version: "3.10"
tox-env: "py310"
- version: "3.11"
Expand Down
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## Changes

## 5.0.0 (unreleased)

**Breaking Changes:**
- Drop support for Python 3.8 and 3.9. Minimum required version is now Python 3.10.
[jensens]

**Code Modernization:**
- Modernize type hints to use Python 3.10+ syntax (PEP 604: `X | Y` instead of `Union[X, Y]`)
- Use built-in generic types (`list`, `dict`, `tuple`) instead of `typing.List`, `typing.Dict`, `typing.Tuple`
[jensens]

## 4.1.2 (unreleased)

- 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).
Expand Down
14 changes: 7 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ make coverage-html # Run tests + combine + open HTML report
Coverage is automatically collected and combined from all matrix test runs in GitHub Actions:

**Process:**
1. Each test job (Python 3.8-3.14, Ubuntu/Windows/macOS) uploads its `.coverage.*` file as an artifact
1. Each test job (Python 3.10-3.14, Ubuntu/Windows/macOS) uploads its `.coverage.*` file as an artifact
2. A dedicated `coverage` job downloads all artifacts
3. Coverage is combined using `coverage combine`
4. Reports are generated:
Expand Down Expand Up @@ -129,7 +129,7 @@ isort src/mxdev

### Testing Multiple Python Versions (using uvx tox with uv)
```bash
# Run tests on all supported Python versions (Python 3.8-3.14)
# Run tests on all supported Python versions (Python 3.10-3.14)
# This uses uvx to run tox with tox-uv plugin for much faster testing (10-100x speedup)
uvx --with tox-uv tox

Expand Down Expand Up @@ -254,7 +254,7 @@ The codebase follows a three-phase pipeline:
1. **Minimal dependencies**: Only `packaging` at runtime - no requests, no YAML parsers
2. **Standard library first**: Uses `configparser`, `urllib`, `threading` instead of third-party libs
3. **No pip invocation**: mxdev generates files; users run pip separately
4. **Backward compatibility**: Supports Python 3.8+ with version detection for Git commands
4. **Backward compatibility**: Supports Python 3.10+ with version detection for Git commands

## Configuration System

Expand Down Expand Up @@ -403,7 +403,7 @@ myext-package_setting = value

- **Formatting**: Black-compatible (max line length: 120)
- **Import sorting**: isort with `force_alphabetical_sort = true`, `force_single_line = true`
- **Type hints**: Use throughout (Python 3.8+ compatible)
- **Type hints**: Use throughout (Python 3.10+ compatible)
- **Path handling**: Prefer `pathlib.Path` over `os.path` for path operations
- Use `pathlib.Path().as_posix()` for cross-platform path comparison
- Use `/` operator for path joining: `Path("dir") / "file.txt"`
Expand All @@ -424,9 +424,9 @@ The project uses GitHub Actions for continuous integration, configured in [.gith

**Test Job:**
- **Matrix testing** across:
- Python versions: 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14
- Python versions: 3.10, 3.11, 3.12, 3.13, 3.14
- Operating systems: Ubuntu, Windows, macOS
- Total: 21 combinations (7 Python × 3 OS)
- Total: 15 combinations (5 Python × 3 OS)
- Uses: `uvx --with tox-uv tox -e py{version}`
- Leverages `astral-sh/setup-uv@v7` action for uv installation

Expand Down Expand Up @@ -674,7 +674,7 @@ gh pr checks <PR_NUMBER>

## Requirements

- **Python**: 3.8+
- **Python**: 3.10+
- **pip**: 23+ (required for proper operation)
- **Runtime dependencies**: Only `packaging`
- **VCS tools**: Install git, svn, hg, bzr, darcs as needed for VCS operations
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ PRIMARY_PYTHON?=3.14

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

# Install packages using the given package installer method.
# Supported are `pip` and `uv`. If uv is used, its global availability is
Expand Down
6 changes: 2 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ keywords = ["pip", "vcs", "git", "development"]
authors = [
{name = "MX Stack Developers", email = "dev@bluedynamics.com" }
]
requires-python = ">=3.8"
requires-python = ">=3.10"
license = { text = "BSD 2-Clause License" }
classifiers = [
"Development Status :: 5 - Production/Stable",
Expand All @@ -15,8 +15,6 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -173,7 +171,7 @@ directory = "htmlcov"

[tool.tox]
requires = ["tox>=4", "tox-uv>=1"]
env_list = ["lint", "py38", "py39", "py310", "py311", "py312", "py313", "py314"]
env_list = ["lint", "py310", "py311", "py312", "py313", "py314"]

[tool.tox.env_run_base]
description = "Run tests with pytest and coverage"
Expand Down
18 changes: 9 additions & 9 deletions src/mxdev/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ def to_bool(value):


class Configuration:
settings: typing.Dict[str, str]
overrides: typing.Dict[str, str]
ignore_keys: typing.List[str]
packages: typing.Dict[str, typing.Dict[str, str]]
hooks: typing.Dict[str, typing.Dict[str, str]]
settings: dict[str, str]
overrides: dict[str, str]
ignore_keys: list[str]
packages: dict[str, dict[str, str]]
hooks: dict[str, dict[str, str]]

def __init__(
self,
mxini: str,
override_args: typing.Dict = {},
hooks: typing.List["Hook"] = [],
override_args: dict = {},
hooks: list["Hook"] = [],
) -> None:
logger.debug("Read configuration")
data = read_with_included(mxini)
Expand Down Expand Up @@ -164,9 +164,9 @@ def out_constraints(self) -> str:
return self.settings.get("constraints-out", "constraints-mxdev.txt")

@property
def package_keys(self) -> typing.List[str]:
def package_keys(self) -> list[str]:
return [k.lower() for k in self.packages]

@property
def override_keys(self) -> typing.List[str]:
def override_keys(self) -> list[str]:
return [k.lower() for k in self.overrides]
4 changes: 2 additions & 2 deletions src/mxdev/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ def load_hooks() -> list:
return [ep.load()() for ep in load_eps_by_group("mxdev") if ep.name == "hook"]


def read_hooks(state: State, hooks: typing.List[Hook]) -> None:
def read_hooks(state: State, hooks: list[Hook]) -> None:
for hook in hooks:
hook.read(state)


def write_hooks(state: State, hooks: typing.List[Hook]) -> None:
def write_hooks(state: State, hooks: list[Hook]) -> None:
for hook in hooks:
hook.write(state)
6 changes: 3 additions & 3 deletions src/mxdev/including.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@


def resolve_dependencies(
file_or_url: typing.Union[str, Path],
file_or_url: str | Path,
tmpdir: str,
http_parent=None,
) -> typing.List[Path]:
) -> list[Path]:
"""Resolve dependencies of a file or url

The result is a list of Path objects, starting with the
Expand Down Expand Up @@ -69,7 +69,7 @@ def resolve_dependencies(
return file_list


def read_with_included(file_or_url: typing.Union[str, Path]) -> ConfigParser:
def read_with_included(file_or_url: str | Path) -> ConfigParser:
"""Read a file or url and include all referenced files,

Parse the result as a ConfigParser and return it.
Expand Down
38 changes: 18 additions & 20 deletions src/mxdev/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

def process_line(
line: str,
package_keys: typing.List[str],
override_keys: typing.List[str],
ignore_keys: typing.List[str],
package_keys: list[str],
override_keys: list[str],
ignore_keys: list[str],
variety: str,
) -> typing.Tuple[typing.List[str], typing.List[str]]:
) -> tuple[list[str], list[str]]:
"""Take line from a constraints or requirements file and process it recursively.

The line is taken as is unless one of the following cases matches:
Expand Down Expand Up @@ -69,11 +69,11 @@ def process_line(

def process_io(
fio: typing.IO,
requirements: typing.List[str],
constraints: typing.List[str],
package_keys: typing.List[str],
override_keys: typing.List[str],
ignore_keys: typing.List[str],
requirements: list[str],
constraints: list[str],
package_keys: list[str],
override_keys: list[str],
ignore_keys: list[str],
variety: str,
) -> None:
"""Read lines from an open file and trigger processing of each line
Expand All @@ -91,17 +91,17 @@ def process_io(

def resolve_dependencies(
file_or_url: str,
package_keys: typing.List[str],
override_keys: typing.List[str],
ignore_keys: typing.List[str],
package_keys: list[str],
override_keys: list[str],
ignore_keys: list[str],
variety: str = "r",
) -> typing.Tuple[typing.List[str], typing.List[str]]:
) -> tuple[list[str], list[str]]:
"""Takes a file or url, loads it and trigger to recursivly processes its content.

returns tuple of requirements and constraints
"""
requirements: typing.List[str] = []
constraints: typing.List[str] = []
requirements: list[str] = []
constraints: list[str] = []
if not file_or_url.strip():
logger.info("mxdev is configured to run without input requirements!")
return ([], [])
Expand Down Expand Up @@ -210,7 +210,7 @@ def fetch(state: State) -> None:
)


def write_dev_sources(fio, packages: typing.Dict[str, typing.Dict[str, typing.Any]]):
def write_dev_sources(fio, packages: dict[str, dict[str, typing.Any]]):
"""Create requirements configuration for fetched source packages."""
if not packages:
return
Expand All @@ -231,9 +231,7 @@ def write_dev_sources(fio, packages: typing.Dict[str, typing.Dict[str, typing.An
fio.write("\n\n")


def write_dev_overrides(
fio, overrides: typing.Dict[str, str], package_keys: typing.List[str]
):
def write_dev_overrides(fio, overrides: dict[str, str], package_keys: list[str]):
"""Create requirements configuration for overridden packages."""
fio.write("#" * 79 + "\n")
fio.write("# mxdev constraint overrides\n")
Expand All @@ -247,7 +245,7 @@ def write_dev_overrides(
fio.write("\n\n")


def write_main_package(fio, settings: typing.Dict[str, str]):
def write_main_package(fio, settings: dict[str, str]):
"""Write main package if configured."""
main_package = settings.get("main-package")
if main_package:
Expand Down
4 changes: 2 additions & 2 deletions src/mxdev/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
@dataclass
class State:
configuration: Configuration
requirements: typing.List[str] = field(default_factory=list)
constraints: typing.List[str] = field(default_factory=list)
requirements: list[str] = field(default_factory=list)
constraints: list[str] = field(default_factory=list)
16 changes: 8 additions & 8 deletions src/mxdev/vcs/bazaar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def bzr_branch(self, **kwargs):
path = self.source["path"]
url = self.source["url"]
if os.path.exists(path):
self.output((logger.info, "Skipped branching existing package %r." % name))
self.output((logger.info, f"Skipped branching existing package {name!r}."))
return
self.output((logger.info, "Branched %r with bazaar." % name))
self.output((logger.info, f"Branched {name!r} with bazaar."))
env = dict(os.environ)
env.pop("PYTHONPATH", None)
cmd = subprocess.Popen(
Expand All @@ -42,7 +42,7 @@ def bzr_pull(self, **kwargs):
name = self.source["name"]
path = self.source["path"]
url = self.source["url"]
self.output((logger.info, "Updated %r with bazaar." % name))
self.output((logger.info, f"Updated {name!r} with bazaar."))
env = dict(os.environ)
env.pop("PYTHONPATH", None)
cmd = subprocess.Popen(
Expand All @@ -67,12 +67,12 @@ def checkout(self, **kwargs):
self.update(**kwargs)
elif self.matches():
self.output(
(logger.info, "Skipped checkout of existing package %r." % name)
(logger.info, f"Skipped checkout of existing package {name!r}.")
)
else:
raise BazaarError(
"Source URL for existing package %r differs. "
"Expected %r." % (name, self.source["url"])
"Source URL for existing package {!r} differs. "
"Expected {!r}.".format(name, self.source["url"])
)
else:
return self.bzr_branch(**kwargs)
Expand Down Expand Up @@ -115,8 +115,8 @@ def update(self, **kwargs):
name = self.source["name"]
if not self.matches():
raise BazaarError(
"Can't update package %r because its URL doesn't match." % name
f"Can't update package {name!r} because its URL doesn't match."
)
if self.status() != "clean" and not kwargs.get("force", False):
raise BazaarError("Can't update package %r because it's dirty." % name)
raise BazaarError(f"Can't update package {name!r} because it's dirty.")
return self.bzr_pull(**kwargs)
Loading