Skip to content

Commit 98c0c83

Browse files
authored
Merge pull request #37 from mxstack/including
Including
2 parents d1a0881 + 162693b commit 98c0c83

23 files changed

+224
-38
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jobs:
2222
- "3.9"
2323
- "3.10"
2424
- "3.11"
25+
- "3.12"
2526
os:
2627
- ubuntu-latest
2728
- windows-latest

CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
## Changes
22

3-
### 3.0.1 (unreleased)
3+
### 3.1.0 (unreleased)
44

55
- Provide `directory` default setting [rnix]
6+
- Feature: Include other INI config files [jensens]
67

78
### 3.0.0 (2023-05-08)
89

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ recursive-include example *.ini
77
recursive-include example *.txt
88
recursive-include src *.md
99
recursive-exclude example *-outfile.txt
10+
recursive-include src *.ini
1011
recursive-exclude .vscode *
1112
graft mxdev

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ It builds on top of the idea to have stable version constraints and then develop
77
As part of the above use-case sometimes versions of the stable constraints need an override with a different (i.e. newer) version.
88
Other software follow the same idea are [mr.developer](https://pypi.org/project/mr.developer) for Python's *zc.buildout* or [mrs-developer](https://www.npmjs.com/package/mrs-developer) for NPM packages.
99

10-
**mxdev 2.0 needs pip version 22 at minimum to work properly**
10+
**mxdev >=2.0 needs pip version 22 at minimum to work properly**
1111

1212

1313
### Overview
@@ -53,6 +53,22 @@ Output of the combined constraints.
5353

5454
Default: `constraints-mxdev.txt`
5555

56+
#### `include`
57+
58+
Include one or more other INI files.
59+
60+
The included file is read before the main file, so the main file overrides included settings.
61+
Included files may include other files.
62+
Innermost inclusions are read first.
63+
64+
If an included file is an HTTP-URL, it is loaded from there.
65+
66+
If the included file is a relative path, it is loaded relative to the parents directory or URL.
67+
68+
The feature utilizes the [ConfigParser feature to read multiple files at once](https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.read).
69+
70+
Default: empty
71+
5672
#### `default-target`
5773

5874
Target directory for sources from VCS. Default: `./sources`

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "mxdev"
33
description = "Enable to work with Python projects containing lots of packages, of which you only want to develop some."
4-
version = "3.0.1.dev0"
4+
version = "3.1.0.dev0"
55
keywords = ["pip", "vcs", "git", "development"]
66
authors = [
77
{name = "MX Stack Developers", email = "dev@bluedynamics.com" }
@@ -20,6 +20,7 @@ classifiers = [
2020
"Programming Language :: Python :: 3.9",
2121
"Programming Language :: Python :: 3.10",
2222
"Programming Language :: Python :: 3.11",
23+
"Programming Language :: Python :: 3.12",
2324
]
2425
dependencies = []
2526
dynamic = ["readme"]
@@ -33,6 +34,7 @@ test = [
3334
"pytest",
3435
"pytest-cov",
3536
"pytest-mock",
37+
"httpretty",
3638
"types-setuptools",
3739
]
3840

src/mxdev/config.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
from .including import read_with_included
12
from .logging import logger
23

3-
import configparser
44
import os
55
import pkg_resources
66
import typing
@@ -13,9 +13,7 @@
1313
def to_bool(value):
1414
if not isinstance(value, str):
1515
return bool(value)
16-
if value.lower() in ("true", "on", "yes", "1"):
17-
return True
18-
return False
16+
return value.lower() in ("true", "on", "yes", "1")
1917

2018

2119
class Configuration:
@@ -27,22 +25,13 @@ class Configuration:
2725

2826
def __init__(
2927
self,
30-
tio: typing.TextIO,
28+
mxini: str,
3129
override_args: typing.Dict = {},
3230
hooks: typing.List["Hook"] = [],
3331
) -> None:
3432
logger.debug("Read configuration")
35-
data = configparser.ConfigParser(
36-
default_section="settings",
37-
interpolation=configparser.ExtendedInterpolation(),
38-
)
39-
data.optionxform = str # type: ignore
33+
data = read_with_included(mxini)
4034

41-
# default settings to be used in mx.ini config file
42-
data["settings"]["directory"] = os.getcwd()
43-
# end default settings
44-
45-
data.read_file(tio)
4635
settings = self.settings = dict(data["settings"].items())
4736

4837
logger.debug(f"infile={self.infile}")

src/mxdev/including.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from configparser import ConfigParser
2+
from configparser import ExtendedInterpolation
3+
from pathlib import Path
4+
from urllib import parse
5+
from urllib import request
6+
7+
import os
8+
import tempfile
9+
import typing
10+
11+
12+
def resolve_dependencies(
13+
file_or_url: typing.Union[str, Path],
14+
tmpdir: str,
15+
http_parent=None,
16+
) -> typing.List[Path]:
17+
"""Resolve dependencies of a file or url
18+
19+
The result is a list of Path objects, starting with the
20+
given file_or_url and followed by all file_or_urls referenced from it.
21+
22+
The file_or_url is assumed to be a ini file or url to such, with an option key "include"
23+
under the "[settings]" section.
24+
"""
25+
if isinstance(file_or_url, str):
26+
if http_parent:
27+
file_or_url = parse.urljoin(http_parent, file_or_url)
28+
parsed = parse.urlparse(str(file_or_url))
29+
if parsed.scheme:
30+
with request.urlopen(str(file_or_url)) as fio:
31+
tf = tempfile.NamedTemporaryFile(
32+
suffix=".ini",
33+
dir=str(tmpdir),
34+
delete=False,
35+
)
36+
tf.write(fio.read())
37+
tf.flush()
38+
file = Path(tf.name)
39+
parts = list(parsed)
40+
parts[2] = str(Path(parts[2]).parent)
41+
http_parent = parse.urlunparse(parts)
42+
else:
43+
file = Path(file_or_url)
44+
else:
45+
file = file_or_url
46+
if not file.exists():
47+
raise FileNotFoundError(file)
48+
cfg = ConfigParser()
49+
cfg.read(file)
50+
if not ("settings" in cfg and "include" in cfg["settings"]):
51+
return [file]
52+
file_list = []
53+
for include in cfg["settings"]["include"].split("\n"):
54+
include = include.strip()
55+
if not include:
56+
continue
57+
if http_parent or parse.urlparse(include).scheme:
58+
file_list += resolve_dependencies(include, tmpdir, http_parent)
59+
else:
60+
file_list += resolve_dependencies(file.parent / include, tmpdir)
61+
62+
file_list.append(file)
63+
return file_list
64+
65+
66+
def read_with_included(file_or_url: typing.Union[str, Path]) -> ConfigParser:
67+
"""Read a file or url and include all referenced files,
68+
69+
Parse the result as a ConfigParser and return it.
70+
"""
71+
cfg = ConfigParser(
72+
default_section="settings",
73+
interpolation=ExtendedInterpolation(),
74+
)
75+
cfg.optionxform = str # type: ignore
76+
cfg["settings"]["directory"] = os.getcwd()
77+
with tempfile.TemporaryDirectory() as tmpdir:
78+
resolved = resolve_dependencies(file_or_url, tmpdir)
79+
cfg.read(resolved)
80+
return cfg

src/mxdev/main.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
"-c",
2323
"--configuration",
2424
help="configuration file in INI format",
25-
nargs="?",
26-
type=argparse.FileType("r"),
25+
type=str,
2726
default="mx.ini",
2827
)
2928
parser.add_argument(
@@ -62,7 +61,9 @@ def main() -> None:
6261
if args.threads:
6362
override_args["threads"] = args.threads
6463
configuration = Configuration(
65-
tio=args.configuration, override_args=override_args, hooks=hooks
64+
mxini=args.configuration,
65+
override_args=override_args,
66+
hooks=hooks,
6667
)
6768
state = State(configuration=configuration)
6869
logger.info("#" * 79)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,13 @@ def develop(src):
4040
develop = MockDevelop()
4141
develop.sources_dir = src
4242
return develop
43+
44+
45+
@pytest.fixture
46+
def httpretty():
47+
import httpretty
48+
49+
httpretty.enable()
50+
yield httpretty
51+
httpretty.disable()
52+
httpretty.reset()

src/mxdev/tests/data/file01.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[settings]
2+
include =
3+
file02.ini
4+
file04.ini
5+
test = 1
6+
unique_1 = 1

0 commit comments

Comments
 (0)