/pyconstclasses

Python package for const class decorators and utility

Primary LanguagePythonMIT LicenseMIT

PyConstClasses

tests examples format coverage


Overview

PyConstClasses is a python package containing const class decorators and utility. It allows for the creation constant and static constant classes by utilizing the annotations of the class definition.



Table of contents



Installation

To install the PyConstClasses package in your python environment run:

pip install pyconstclasses

or

python -m pip install pyconstclasses


Tutorial

After installing the package, you have to import it to your python program:

import constclasses as cc

Basic usage

The core of the PyConstClasses package are the const_class and static_const_class decorators. Both of these decorators override the default behaviour of the __setattr__ magic method for the decorated class so that it thors cc.ConstError when trying to modify the constant attribute of an instance.

  • The const_class decorator allows you to define a class structure and create constant instances of the defined class:

    # const_class_basic.py
    
    @cc.const_class
    class Person:
        first_name: str
        last_name: str
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name}"
    
    
    if __name__ == "__main__":
        john = Person("John", "Doe")
        print(f"{john = }")
    
        try:
            john.first_name = "Bob"
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        try:
            john.last_name = "Smith"
        except cc.ConstError as err:
            print(f"Error: {err}")

    This program will produce the following output:

    john = John Doe
    Error: Cannot modify const attribute `first_name` of class `Person`
    Error: Cannot modify const attribute `last_name` of class `Person`
    
  • The static_const_class deacorator allows you to define a pseudo-static resource with const members (it creates an instance of the decorated class):

    # static_const_class_basic.py
    
    @cc.static_const_class
    class ProjectConfiguration:
        name: str = "MyProject"
        version: str = "alpha"
    
        def __repr__(self):
            return f"Project: {self.name}\nVersion: {self.version}"
    
    
    if __name__ == "__main__":
        print(f"Project configuration:\n{ProjectConfiguration}")
    
        try:
            ProjectConfiguration.name = "NewProjectName"
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        try:
            ProjectConfiguration.version = "beta"
        except cc.ConstError as err:
            print(f"Error: {err}")

    This program will produce the following output:

    Project configuration:
    Project: MyProject
    Version: alpha
    Error: Cannot modify const attribute `name` of class `ProjectConfiguration`
    Error: Cannot modify const attribute `version` of class `ProjectConfiguration`
    

    Although the static_const_class decorator prevents "standard" class instantiation, you can create mutable instances of such classes:

    @cc.static_const_class
    class DatabaseConfiguration:
        host: str = "localhost"
        port: int = 5432
        username: str = "admin"
        password: str = "secret"
    
        def __repr__(self):
            return (
                f"DatabaseConfiguration:\n"
                f"Host: {self.host}\n"
                f"Port: {self.port}\n"
                f"Username: {self.username}\n"
                f"Password: {self.password}"
            )
    
    
    if __name__ == "__main__":
        print(f"Database configuration:\n{DatabaseConfiguration}")
    
        try:
            DatabaseConfiguration.host = "remotehost"
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        try:
            DatabaseConfiguration.port = 3306
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        # Create a mutable instance for testing or development
        mutable_config = cc.mutable_instance(DatabaseConfiguration)
        mutable_config.host = "testhost"
        mutable_config.username = "testuser"
        mutable_config.password = "testpassword"
    
        print("\nMutable configuration for testing:")
        print(mutable_config)

Important

In the current version of the package the constant attributes have to be defined using annotations, i.e. the member: type (= value) syntax of the class member declaration is required


Common parameters

Both const decorators - const_class and static_const_class - have the following parameters:

  • with_strict_types: bool

    If this parameter's value is set to

    • False - the decorators will use the attribute_type(given_value) conversion, so as long as the given value's type is convertible to the desired type, the decorators will not raise any errors.
    • True - the decorators will perform an isinstance(given_value, attribute_type) check, the failure of which will result in raising a TypeError

    Example:

    # common_with_strict_types.py
    
    @cc.const_class
    class Person:
        first_name: str
        last_name: str
        age: int
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name} [age: {self.age}]"
    
    @cc.const_class(with_strict_types=True)
    class PersonStrictTypes:
        first_name: str
        last_name: str
        age: int
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name} [age: {self.age}]"
    
    
    if __name__ == "__main__":
        john = Person("John", "Doe", 21.5)
        print(john)
    
        try:
            # invalid as 21.5 is not an instance of int
            john_strict = PersonStrictTypes("John", "Doe", 21.5)
        except TypeError as err:
            print(f"Error:\n{err}")
    
        john_strict = PersonStrictTypes("John", "Doe", 21)
        print(john_strict)

    This program will produce the following output:

    John Doe [age: 21]
    Error:
    Attribute value does not match the declared type:
            attribute: age, declared type: <class 'int'>, actual type: <class 'float'>
    John Doe [age: 21]
    
  • include: set[str] and exclude: set[str]

    These parameters are used to define which class attributes are supposed to be treated as constant. If they are not set (or explicitly set to None) all attributes will be treaded as constant.

    Example:

    # common_include.py
    
    @cc.const_class(include=["first_name", "last_name"])
    class Person:
        first_name: str
        last_name: str
        age: int
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name} [age: {self.age}]"
    
    
    if __name__ == "__main__":
        john = Person("John", "Doe", 21)
        print(f"{john = }")
    
        try:
            john.first_name = "Bob"
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        try:
            john.last_name = "Smith"
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        # valid modification as the `age` parameter is not in the include set
        john.age = 22
        print(f"{john = }")

    This program will produce the followig output:

    john = John Doe [age: 21]
    Error: Cannot modify const attribute `first_name` of class `Person`
    Error: Cannot modify const attribute `last_name` of class `Person`
    john = John Doe [age: 22]
    

    The same can be achieved using the exclude parameter:

    Example:

    # common_exclude.py
    
    @cc.const_class(exclude=["age"])
    class Person:
        first_name: str
        last_name: str
        age: int
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name} [age: {self.age}]"

    The class defined in this example has the behaviour equivalent to the earlier include example.

Important

Simultaneous usage of the incldue and exclude parameters will result in raising a configuration error.


Decorator-specific parameters

Note

In the current version of the package only the const_class decorator has it's own specific parameters.

  • with_kwargs: bool

    By default the const_class decorator adds a constructor which uses positional arguments to create a constant instance of the class. However if this parameter is set to True, the decorator will use the keyword arguments for this purpose.

    Example:

    # const_class_with_kwargs.py
    
    @cc.const_class
    class PersonArgs:
        first_name: str
        last_name: str
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name}"
    
    
    @cc.const_class(with_kwargs=True)
    class PersonKwargs:
        first_name: str
        last_name: str
    
        def __repr__(self) -> str:
            return f"{self.first_name} {self.last_name}"
    
    
    if __name__ == "__main__":
        john_args = PersonArgs("John", "Doe")
        print(f"{john_args = }")
    
        try:
            john_args = PersonArgs(first_name="John", last_name="Doe")
        except cc.InitializationError as err:
            print(f"Error: {err}")
    
        john_kwargs = PersonKwargs(first_name="John", last_name="Doe")
        print(f"{john_kwargs = }")

    This program will produce the following output:

    john_args = John Doe
    Error: Invalid number of arguments: expected 2 - got 0
    john_kwargs = John Doe
    
  • inherit_constructor: bool

    By default the const_class decorator defines a constructor which manually assigns values to attributes. However if this parameter is set to True the class will be initialized using the user defined __init__ function of the decorated class.

    Example:

    # const_class_inherit_constructor.py
    
    @cc.static_const_class
    class ProjectConfiguration:
        name: str = "MyProject"
        version: str = "alpha"
    
        def __repr__(self):
            return f"Project: {self.name}\nVersion: {self.version}"
    
    
    if __name__ == "__main__":
        print(f"Project configuration:\n{ProjectConfiguration}")
    
        try:
            ProjectConfiguration.name = "NewProjectName"
        except cc.ConstError as err:
            print(f"Error: {err}")
    
        try:
            ProjectConfiguration.version = "beta"
        except cc.ConstError as err:
            print(f"Error: {err}")

    This program will produce the following output:

    john = John Doe
    Error: Cannot modify const attribute `first_name` of class `Person`
    Error: Cannot modify const attribute `last_name` of class `Person`
    


Examples

The project examples shown in the Tutorial section can be found in the examples directory.

To run the example programs you need to install the PyConstClasses package into your python environment. You can install it via pip of using a local distribution build (the process is described in the Dev notes section).



Dev notes

Environment setup:

To be able to build or test the project create a python virtual environment

python -m venv cc_venv
source cc_venv/bin/activate
pip install -r requirements-dev.txt

Note

If any package listed in requirements-dev.txt is no longer required or if there is a new required package, update the requirements file using: pip freeze > requirements-dev.txt

Building

To build the package, run:

python -m build

This will generate the dist directory with the .whl file. To locally test if the distribution is correct, you can run:

pip install dist/pyconstclasses-<version>-py3-none-any.whl --force-reinstall

Note

To test the package build locally it is recommended to use a clean virtual environment.

Testing:

The project uses pytest and tox for testing purposes.

  • To run the tests for the current python interpreter run: pytest -v
  • To run tests for all supported python versions run: tox

Coverage

To generate a test coverage report you can run tox which will automatically generate json and xml reports (the types of coverage reports can be adjusted in tox.ini).

You can also generate the coverage reports manually with pytest, e.g.:

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

Formatting:

The project uses black and isort for formatting purposes. To format the source code use the prepared script:

bash scripts/format.sh

You can also use the black and isort packages directly, e.g.:

python -m <black/isort> <path> (--check)


Licence

The PyConstClasses project uses the MIT Licence