Skip to content

Commit

Permalink
YT-PYCC-14: Extend the const_class decorator with a new function
Browse files Browse the repository at this point in the history
Added a `new` function to the `const_class` decorator, which returns a new instance of the class and allows for modifying the individual members of the new instance using `**kwargs`
  • Loading branch information
SpectraL519 authored Sep 4, 2024
1 parent b2496a4 commit b606531
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 29 deletions.
10 changes: 4 additions & 6 deletions .github/workflows/examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches:
- '*'
paths:
- .github/workflows/examples.yaml
- src/**
- examples/**
- pyproject.toml
Expand All @@ -26,16 +27,13 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install build
run: python -m pip install build

- name: Build the library for ${{ matrix.python-version }}
run: |
python -m build
run: python -m build

- name: Install library
run: |
python -m pip install dist/pyconstclasses-*-py3-none-any.whl --force-reinstall
run: python -m pip install dist/pyconstclasses-*-py3-none-any.whl --force-reinstall

- name: Run the example programs
run: |
Expand Down
10 changes: 4 additions & 6 deletions .github/workflows/format.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches:
- '*'
paths:
- .github/workflows/format.yaml
- src/**
- test/**
- examples/**
Expand All @@ -24,13 +25,10 @@ jobs:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install black isort
run: python -m pip install black isort

- name: Run format check
run: |
python -m black . --check
run: python -m black . --check

- name: Run import check
run: |
python -m isort . --check
run: python -m isort . --check
10 changes: 4 additions & 6 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches:
- '*'
paths:
- .github/workflows/tests.yaml
- src/**
- test/**
- tox.ini
Expand All @@ -27,12 +28,10 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install tox tox-gh-actions
run: python -m pip install tox tox-gh-actions

- name: Run tox for ${{ matrix.python-version }}
run: |
python -m tox
run: python -m tox

- name: Upload coverage data
uses: actions/upload-artifact@v4
Expand All @@ -54,8 +53,7 @@ jobs:
python-version: 3.12

- name: Install dependencies
run: |
python -m pip install tox tox-gh-actions
run: python -m pip install tox tox-gh-actions

- name: Download coverage data
uses: actions/download-artifact@v4
Expand Down
53 changes: 50 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,53 @@ The core of the PyConstClasses package are the `const_class` and `static_const_c
Error: Cannot modify const attribute `last_name` of class `Person`
```

The `const_class` decorators also provides the `new` method which allows for the creationg of new instances of the class based on an existing instance with the option to modify individual fields:

```python
# const_class_new.py

@cc.const_class(with_kwargs=True)
class PersonKwargs:
first_name: str
last_name: str
age: int

def __repr__(self) -> str:
return f"(kwargs) {self.first_name} {self.last_name} [age: {self.age}]"


@cc.const_class(with_kwargs=False)
class PersonArgs:
first_name: str
last_name: str
age: int

def __repr__(self) -> str:
return f"(args) {self.first_name} {self.last_name} [age: {self.age}]"


if __name__ == "__main__":
john = PersonKwargs(first_name="John", last_name="Doe", age=21)
print(f"{john = }")

john_aged = john.new(age=22)
print(f"{john_aged = }")

john = PersonArgs("John", "Doe", 21)
print(f"{john = }")

john_aged = john.new(age=22)
print(f"{john_aged = }")
```

This program will produce the following output:
```
john = (kwargs) John Doe [age: 21]
john_aged = (kwargs) John Doe [age: 22]
john = (args) John Doe [age: 21]
john_aged = (args) John Doe [age: 22]
```

* The `static_const_class` deacorator allows you to define a pseudo-static resource with const members (it creates an instance of the decorated class):

```python
Expand Down Expand Up @@ -450,15 +497,15 @@ pytest -v --cov=constclasses --cov-report=xml --cov-report=html
```

> [!NOTE]
> When testing the project or generating coverate reports, python (or it's packages) will generate additional files (cache file, etc.). To easily clean those files from the working directory run `bash scripts/cleanup.sh`
> When testing the project or generating coverate reports, python (or it's packages) will generate additional files (cache file, etc.). To easily clean those files from the working directory run `./scripts/cleanup.sh`

### Formatting:

The project uses `black` and `isort` for formatting purposes. To format the source code use the prepared script:
```shell
bash scripts/format.sh
./scripts/format.sh
```
You can also use the `black` and `isort` packages directly, e.g.:
You can also use the `black` and `isort` packages directly, e.g.
```shell
python -m <black/isort> <path> (--check)
```
Expand Down
35 changes: 35 additions & 0 deletions examples/const_class_new.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import constclasses as cc


@cc.const_class(with_kwargs=True)
class PersonKwargs:
first_name: str
last_name: str
age: int

def __repr__(self) -> str:
return f"(kwargs) {self.first_name} {self.last_name} [age: {self.age}]"


@cc.const_class(with_kwargs=False)
class PersonArgs:
first_name: str
last_name: str
age: int

def __repr__(self) -> str:
return f"(args) {self.first_name} {self.last_name} [age: {self.age}]"


if __name__ == "__main__":
john = PersonKwargs(first_name="John", last_name="Doe", age=21)
print(f"{john = }")

john_aged = john.new(age=22)
print(f"{john_aged = }")

john = PersonArgs("John", "Doe", 21)
print(f"{john = }")

john_aged = john.new(age=22)
print(f"{john_aged = }")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pyconstclasses"
version = "1.0"
version = "1.0.4"
description = "Package with const class decoratos and utility"
authors = [
{name = "SpectraL519"}
Expand Down
Empty file modified scripts/cleanup.sh
100644 → 100755
Empty file.
Empty file modified scripts/format.sh
100644 → 100755
Empty file.
28 changes: 23 additions & 5 deletions src/constclasses/const_class.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from copy import deepcopy

from .ccerror import ConstError, InitializationError
from .const_class_base import (
CC_BASE_ATTR_NAME,
Expand All @@ -16,8 +18,13 @@ def const_class_impl(
):
class ConstClass(cls):
def __init__(self, *args, **kwargs):
cls_attrs = cls.__annotations__

self.__dict__[CC_BASE_ATTR_NAME] = ConstClassBase(
with_strict_types=with_strict_types, include=include, exclude=exclude
cls_attrs=cls_attrs.keys(),
with_strict_types=with_strict_types,
include=include,
exclude=exclude,
)
self.__dict__[CC_INITIALIZED_ATTR_NAME] = False

Expand All @@ -29,17 +36,17 @@ def __init__(self, *args, **kwargs):
super(ConstClass, self).__init__()

if with_kwargs:
for attr_name, attr_type in cls.__annotations__.items():
for attr_name, attr_type in cls_attrs.items():
self.__dict__[attr_name] = self._cc_base.process_attribute_type(
attr_name, attr_type, kwargs.get(attr_name)
)
else:
if len(args) != len(cls.__annotations__):
if len(args) != len(cls_attrs):
raise InitializationError.invalid_number_of_arguments(
len(cls.__annotations__), len(args)
len(cls_attrs), len(args)
)

for i, (attr_name, attr_type) in enumerate(cls.__annotations__.items()):
for i, (attr_name, attr_type) in enumerate(cls_attrs.items()):
self.__dict__[attr_name] = self._cc_base.process_attribute_type(
attr_name, attr_type, args[i]
)
Expand All @@ -53,6 +60,17 @@ def __setattr__(self, attr_name: str, attr_value) -> None:
attr_name, cls.__annotations__.get(attr_name), attr_value
)

def new(self, **kwargs):
def _get_value(key: str):
return kwargs.get(key, deepcopy(getattr(self, key)))

if with_kwargs:
init_params = {key: _get_value(key) for key in self.__annotations__}
return ConstClass(**init_params)
else:
init_params = [_get_value(key) for key in self.__annotations__]
return ConstClass(*init_params)

ConstClass.__name__ = cls.__name__
ConstClass.__module__ = cls.__module__

Expand Down
5 changes: 4 additions & 1 deletion src/constclasses/static_const_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ def static_const_class_impl(
class StaticConstClass(cls):
def __init__(self, *args, **kwargs):
self.__dict__[CC_BASE_ATTR_NAME] = ConstClassBase(
with_strict_types=with_strict_types, include=include, exclude=exclude
cls_attrs=cls.__annotations__.keys(),
with_strict_types=with_strict_types,
include=include,
exclude=exclude,
)
self.__dict__[CC_INITIALIZED_ATTR_NAME] = False
super(StaticConstClass, self).__init__()
Expand Down
50 changes: 50 additions & 0 deletions test/test_const_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,53 @@ def _initialize():
util.assert_does_not_throw(_initialize)
assert const_instance.x == x_value
assert const_instance.s == str(x_value)


def test_new_with_kwargs():
@const_class(with_kwargs=True)
class ConstClassWithKwargs:
x: int
s: str

def __eq__(self, other: object) -> bool:
return (
isinstance(other, self.__class__)
and other.x == self.x
and other.s == self.s
)

const_instance = ConstClassWithKwargs(**ATTR_VALS_1)

# should create an exact copy by default
const_instance_new_default = const_instance.new()
assert const_instance_new_default == const_instance

const_instance_new = const_instance.new(x=ATTR_VALS_2[X_ATTR_NAME])
assert isinstance(const_instance_new, ConstClassWithKwargs)
assert const_instance_new.s == const_instance.s
assert const_instance_new.x == ATTR_VALS_2[X_ATTR_NAME]


def test_new_with_args():
@const_class(with_kwargs=False)
class ConstClassWithKwargs:
x: int
s: str

def __eq__(self, other: object) -> bool:
return (
isinstance(other, self.__class__)
and other.x == self.x
and other.s == self.s
)

const_instance = ConstClassWithKwargs(*(ATTR_VALS_1.values()))

# should create an exact copy by default
const_instance_new_default = const_instance.new()
assert const_instance_new_default == const_instance

const_instance_new = const_instance.new(x=ATTR_VALS_2[X_ATTR_NAME])
assert isinstance(const_instance_new, ConstClassWithKwargs)
assert const_instance_new.s == const_instance.s
assert const_instance_new.x == ATTR_VALS_2[X_ATTR_NAME]
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ skip_missing_interpreters = True
deps =
pytest
pytest-cov
coverage[toml]
coverage
commands =
python -m coverage run -p -m pytest {posargs}

Expand Down

0 comments on commit b606531

Please sign in to comment.