Skip to content

Commit e92d268

Browse files
release: v0.0.1
0 parents  commit e92d268

File tree

8 files changed

+294
-0
lines changed

8 files changed

+294
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__/
2+
/dist
3+
/*.egg-info/
4+
/build

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Shangyu Liu
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# xparameter
2+
A portable configuration tool to manage hyperparameters and settings of your experiments.
3+
4+
## Features
5+
:white_check_mark: Support syntax highlighting and autocompletion for attributes
6+
:white_check_mark: Support arguments control through command line
7+
:white_check_mark: Support nested definition (namespaces) to isolate parameters of the same name
8+
:white_check_mark: Inherent Singleton ensuring consistent revision across files
9+
:white_check_mark: Support basic type checking
10+
11+
## Quickstart
12+
You can install the latest official version of xparameter from PyPI:
13+
14+
```haskell
15+
pip install xparameter
16+
```
17+
18+
### Define Custom Configuration
19+
```python
20+
# config.py
21+
from xparameter import Configure as C
22+
from xparameter import Field as F
23+
24+
class Config(C):
25+
"""Define your overall description here"""
26+
27+
class checkpoint(C):
28+
"""Define group description here"""
29+
directory = F(str, default="./checkpoints", help="Directory to save model checkpoints")
30+
31+
```
32+
33+
### Parse Command-line Arguments
34+
```haskell
35+
Config.parse_sys_args()
36+
```
37+
38+
Parameters in `Config` class will be automatically registered onto a `argparse.ArgumentParser()`, which acts as a parser to extract parameter values from command line. In this example case, we use
39+
40+
```haskell
41+
python test.py --checkpoint.directory "/path/to/checkpoints"
42+
```
43+
44+
The argument `Config.checkpoint.directory` will be set to `"/path/to/checkpoints"`. This parsing process and the following manual revision will trigger an inherent type check.
45+
46+
Use the `help` command to print automatically generated help menu:
47+
```haskell
48+
python test.py --help
49+
```
50+
```yaml
51+
Usage: config_01.py [-h] [--checkpoint.directory str]
52+
53+
Define your overall description here
54+
55+
Options:
56+
-h, --help show this help message and exit
57+
58+
Checkpoint:
59+
Define group description here
60+
61+
--checkpoint.directory str Directory to save model checkpoints
62+
```
63+
64+
### Revise Arguments Manually
65+
Manual revision of the argument values can be performed either independently or as a post-processing step following the aforementioned command-line parsing, e.g.,
66+
67+
```haskell
68+
# Config.parse_sys_args()
69+
Config.checkpoint.directory = "/path/to/checkpoints"
70+
```
71+
72+
### Export as Different Formats
73+
Export as python dict:
74+
```haskell
75+
Config.as_dict()
76+
```
77+
Export as json string:
78+
```haskell
79+
Config.as_json()
80+
```

examples/config_00.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from xparameter.xparameter import Configure as C
2+
from xparameter.xparameter import Field as F
3+
4+
5+
class ObjectDetectionConfig(C):
6+
"""Configurations for experiments on object detection"""
7+
8+
class checkpoint(C):
9+
"""Checkpoint configuration"""
10+
directory = F(str, default="./checkpoints", help="Directory to save model checkpoints")
11+
12+
class logging(C):
13+
"""Logging configuration"""
14+
directory = F(str, default="./log", help="Log file directory")
15+
16+
class train(C):
17+
"""Training configuration"""
18+
optimizer = F(str, default='SGD', help="Optimizer class for model training")
19+
learning_rate = F(float, default=0.5, help="Learning rate")
20+
gamma = F(float, default=0.2, help="Reduction factor of learning rate")
21+
num_epochs = F(int, default=100, help="Number of epochs")
22+
milestones = F(tuple, default=(50, 70, 80), help="Epoch milestones at which the learning rate is reduced by gamma")
23+
weight_decay = F(float, default=1e-5, help="Weight decay for Adam optimizer")
24+
momentum = F(float, default=0.9, help="Momentum for Adam optimizer")
25+
batch_size = F(int, default=256, help="Batch size of training samples")
26+
27+
class test(C):
28+
"""Testing configuration"""
29+
batch_size = F(int, default=256, help="Batch size")
30+
num_samples = F(int, default=1000, help="Number of samples for testing")
31+
dataset = F(str, default="coco", help="Dataset name")
32+
33+
class common(C):
34+
"""Shared configuration"""
35+
device = F(str, default="cuda:0", help="Device to use: cpu or cuda:0/cuda:1")
36+
random_seed = F(int, default=0, help="Random seed to enhance reproducibility")
37+
38+
if __name__ == "__main__":
39+
config = ObjectDetectionConfig
40+
config.parse_sys_args()
41+
config.common.random_seed = 42
42+
print(config.as_json())

examples/config_01.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# config.py
2+
from xparameter.xparameter import Configure as C
3+
from xparameter.xparameter import Field as F
4+
5+
class Config(C):
6+
"""Define your overall description here"""
7+
8+
class checkpoint(C):
9+
"""Define group description here"""
10+
directory = F(str, default="./checkpoints", help="Directory to save model checkpoints")
11+
12+
if __name__ == "__main__":
13+
Config.parse_sys_args()

setup.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="xparameter",
8+
version="0.0.1",
9+
author="Shangyu Liu",
10+
author_email="[email protected]",
11+
description="A portable configuration tool to manage hyperparameters and settings of your experiments.",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
url="https://github.com/ThomasAtlantis/xparameter",
15+
python_requires='>=3.7',
16+
install_requires=[
17+
"rich-argparse>=1.7.0",
18+
],
19+
packages=setuptools.find_packages(),
20+
classifiers=[
21+
"Programming Language :: Python :: 3",
22+
"License :: OSI Approved :: MIT License",
23+
"Operating System :: OS Independent",
24+
],
25+
)

xparameter/__init__.py

Whitespace-only changes.

xparameter/xparameter.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from ast import literal_eval
2+
from rich_argparse import RichHelpFormatter
3+
from argparse import ArgumentParser
4+
from functools import partial
5+
6+
7+
class Field:
8+
def __init__(self, type_, default=None, required=False, help="", freeze=False, alias=None):
9+
self.type_ = type_
10+
self.default = default
11+
self.data = default
12+
self.required = required
13+
self.help = help
14+
self.freeze = freeze
15+
self._init_alias(alias)
16+
17+
def _init_alias(self, alias):
18+
if alias is None:
19+
self.alias = []
20+
elif type(alias) == str:
21+
self.alias = [alias]
22+
else:
23+
self.alias = alias
24+
25+
def __get__(self, instance, owner):
26+
return self.data
27+
28+
def set_value(self, value):
29+
if not isinstance(value, self.type_):
30+
raise ValueError(f"Expecting {self.type_}, got {type(value)}")
31+
self.data = value
32+
33+
def as_args_kwargs(self):
34+
args = self.alias
35+
kwargs = dict(
36+
default=self.default, required=self.required, help=self.help,
37+
)
38+
if self.type_ == bool:
39+
kwargs["action"] = "store_false" if self.default else "store_true"
40+
elif self.type_ in [list, tuple]:
41+
kwargs["type"] = lambda value: literal_eval(value)
42+
kwargs["metavar"] = self.type_.__name__
43+
elif hasattr(self.type_, '_name'):
44+
kwargs['type'] = lambda value: literal_eval(value)
45+
kwargs['metavar'] = self.type_._name
46+
else:
47+
kwargs['type'] = self.type_
48+
kwargs['metavar'] = self.type_.__name__
49+
return args, kwargs
50+
51+
class Meta(type):
52+
def __setattr__(cls, name, value):
53+
if name in cls.__dict__:
54+
attr = cls.__dict__[name]
55+
if isinstance(attr, Field):
56+
attr.set_value(value)
57+
else:
58+
super().__setattr__(name, value)
59+
else:
60+
raise AttributeError(f"Cannot set new attribute {name} on {cls.__class__.__name__}")
61+
62+
class Configure(metaclass=Meta):
63+
64+
@classmethod
65+
def as_dict(cls):
66+
dict_to_return = {}
67+
for attr_name, value in cls.__dict__.items():
68+
if isinstance(value, Field):
69+
dict_to_return[attr_name] = value.data
70+
elif isinstance(value, type) and issubclass(value, Configure):
71+
dict_to_return[attr_name] = value.as_dict()
72+
return dict_to_return
73+
74+
@classmethod
75+
def as_json(cls):
76+
import json
77+
return json.dumps(cls.as_dict(), indent=2)
78+
79+
@classmethod
80+
def _parse_sys_args(cls, parser: ArgumentParser):
81+
parser.description = cls.__doc__
82+
prefix = f"{parser.title}." if hasattr(parser, "title") else ""
83+
for attr_name, value in cls.__dict__.items():
84+
if isinstance(value, Field) and not value.freeze:
85+
args, kwargs = value.as_args_kwargs()
86+
parser.add_argument(f"--{prefix}{attr_name}", *args, **kwargs)
87+
elif isinstance(value, type) and issubclass(value, Configure):
88+
group = parser.add_argument_group(title=prefix + value.__name__)
89+
group = value._parse_sys_args(group)
90+
return parser
91+
92+
@classmethod
93+
def _extract_sys_args(cls, parser: ArgumentParser):
94+
args = parser.parse_args()
95+
for key, value in vars(args).items():
96+
*namespaces, attr_name = key.split(".")
97+
cls_temp = cls
98+
for cls_name in namespaces:
99+
cls_temp = getattr(cls_temp, cls_name)
100+
cls_temp.__dict__[attr_name].set_value(value)
101+
102+
@classmethod
103+
def parse_sys_args(cls):
104+
parser = ArgumentParser(formatter_class=partial(RichHelpFormatter, max_help_position=80))
105+
cls._extract_sys_args(cls._parse_sys_args(parser))
106+
107+
@classmethod
108+
def update(cls, name, value):
109+
cls.__dict__[name].set_value(value)

0 commit comments

Comments
 (0)