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
21 changes: 11 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [master, main]
branches: [develop, master, main]
pull_request:
branches: [master, main]

Expand All @@ -27,21 +27,22 @@ jobs:
- name: Check imports with isort
run: isort --check-only --profile black configurize/

build:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.10', '3.11', '3.12']
python-version: ['3.10', '3.11', '3.12', '3.14']
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install package
run: pip install .
- name: Install dependencies
run: uv sync --all-extras --dev

- name: Test import
run: python -c "from configurize import Config, DataClass, Ref; print('Import successful')"
- name: Run tests
run: uv run pytest
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ __pycache__
*.egg-info
dist/
build/
.claude/settings.local.json

# coverage
htmlcov/
.coverage
coverage.xml
.pytest_cache/
16 changes: 14 additions & 2 deletions configurize/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import weakref
from contextlib import contextmanager
from functools import partial
from typing import Any, Callable, Optional, TypedDict
from types import UnionType
from typing import Any, Callable, Optional, TypedDict, Union, get_args, get_origin

from loguru import logger

Expand All @@ -17,6 +18,17 @@
_REPR_FLAG = False # when enabled, all getattr fail will be skip


def _is_optional(annotation) -> bool:
"""Check if a type annotation is Optional (Union with None or T | None)."""
origin = get_origin(annotation)
if origin is None:
return False
if origin is Union or origin is UnionType:
args = get_args(annotation)
return type(None) in args
return False


class Repr(str):
def __repr__(self):
return str(self)
Expand Down Expand Up @@ -325,7 +337,7 @@ def sanity_check(self):
missing_attrs = []
expected_attrs = self._get_class_annotations()
for k, t in expected_attrs.items():
if "Optional[" in str(t):
if _is_optional(t):
continue # pass check for Optional annotation
if not hasattr(self, k):
missing_attrs.append(k)
Expand Down
4 changes: 3 additions & 1 deletion configurize/data_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from functools import cached_property

from typing_extensions import get_annotations


class DataClass:
"""
Expand Down Expand Up @@ -30,7 +32,7 @@ def _get_class_attributes(cls):
@classmethod
def _get_class_annotations(cls):
attributes = {}
attributes.update(cls.__annotations__)
attributes.update(get_annotations(cls, eval_str=True))
for base_cls in cls.__bases__:
if issubclass(base_cls, DataClass):
for k, v in base_cls._get_class_annotations().items():
Expand Down
2 changes: 1 addition & 1 deletion examples/train_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ def __init__(self, cfg: "Exp"):
self.cfg = cfg

def train(self):
from tqdm import tqdm
import torch
from tqdm import tqdm

logger = self.cfg.logger_cfg.build_logger()
model = self.cfg.model_cfg.build_model()
Expand Down
17 changes: 12 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,16 @@ classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"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",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">=3.6"
requires-python = ">=3.10"
dependencies = [
"loguru",
"fire",
"typing-extensions>=4.15.0",
]

[project.urls]
Expand All @@ -37,3 +34,13 @@ cfshow = "configurize.cli:main"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[dependency-groups]
dev = [
"pytest>=7.0.1",
"pytest-cov>=4.0.0",
]

[tool.pytest]
testpaths = ["tests"]
addopts = ["--cov=configurize"]
Empty file added tests/__init__.py
Empty file.
53 changes: 53 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Basic tests for configurize.Config"""

from configurize import Config
from configurize.reference import Ref


def test_basic_config_creation():
"""Test creating a basic config"""

class SimpleConfig(Config):
name = "test"
value = 42

cfg = SimpleConfig()
assert cfg.name == "test"
assert cfg.value == 42


def test_config_merge():
"""Test merging configs"""

class BaseConfig(Config):
a = 1
b = 2

cfg = BaseConfig()
cfg.merge({"a": 10})
assert cfg.a == 10
assert cfg.b == 2


def test_config_references():
"""Test using Ref to reference other config values"""

class SubConfig(Config):
value = 100
self_ref = Ref(".value")
parent_ref = Ref("..base_value")

class ParentConfig(Config):
base_value = 42
optional: int | None
sub = SubConfig

cfg = ParentConfig()
cfg.sanity_check()
# Test self-reference
assert cfg.sub.self_ref == 100
# Test parent reference
assert cfg.sub.parent_ref == 42
# Verify references update when source changes
cfg.base_value = 99
assert cfg.sub.parent_ref == 99
7 changes: 2 additions & 5 deletions tools/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
warnings.filterwarnings("ignore", ".*")

from configurize import Config
from configurize.utils import compare_in_vscode, show_or_compare
from configurize.utils import get_object_from_file
from configurize.utils import compare_in_vscode, get_object_from_file, show_or_compare


def load_and_compare(
ref: str | Config, exp: str | Config = None, key=None, query=None
):
def load_and_compare(ref: str | Config, exp: str | Config = None, key=None, query=None):
"""Show an Exp or compare two Exps

Usage:
Expand Down
Loading